前言
插件机制可以使得应用程序在发布之后,在不经过重新编译的情况下修改应用程序的行为,这种形式使得应用的框架比较小巧,也可以给用户一些自由(不是完全的自由,有一定的限制)。Java中,使用*.jar或者其他的脚本引擎都可以完成这样的工作,如Eclipse那样。在C语言中,当然可以使用脚本引擎来实现,比如emacs,内置一个lisp的引擎,用户可以自己为emacs写脚本,访问emacs环境的一些组件,从而定制emacs.这里要讨论的无需使用脚本引擎,而是用C语言访问动态链接库来实现。
Linux下的动态库
Linux环境中,与windows下一样,函数库有两种方式:静态库和动态库,静态库参与连接,由于要将目标代码.o与lib中的函数符号合并在一起,所以最终生成的可执行文件较大。一般以.a结尾。如libxxx.a。而动态库(共享库)则不参与编译,只是在运行时才加载如内存,且仅加载一次,因此最终的可执行文件较小。事实上,当一个可执行文件需要运行动态库中的函数时,系统会在内存中查找,如果已经加载,则直接调用,否则才做一次加载,动态库的结尾一般为.so,如libxxx.so。
Linux为动态库的访问提供了4个API,分别为dlopen
, dlerror
, dlsym
和 dlclose。这些API的原型定义在文件
dlfcn.h中,其实现则分别对应有两个库文件(静态库 libdl.a
和动态库 libdl.so
)。
- dlopen加载动态库,并返回句柄
- dlerror如果加载,访问符号出错,可以通过此接口获得详细的描述
- dlsym返回一个动态库中的符号,即通过函数名获得此函数的指针
- dlclose完成之后,释放dlopen返回的句柄
- int func(int a, int b);
int func(int a, int b);
- #include <dlfcn.h>
- //句柄
- void *flib;
- //入口函数原型
- int (*pfunc)(int a, int b);
- //错误信息字符串
- char *error_message;
- int plugin_test(){
- int a = 1, b = 4;
- int result = 0;
- //加载plugina.so,以RTLD_LAZY方式
- flib = dlopen("/home/juntao/.libs/plugina.so", RTLD_LAZY);
- error_message = dlerror();
- if(error_message){
- return (-1);
- }
- //找到函数名为func的函数,返回其指针
- *(void **)(&pfunc) = dlsym(flib, "func");
- error_message = dlerror();
- if(error_message){
- return (-1);
- }
- //调用pfunc指向的指针,及func函数
- result = pfunc(a, b);
- //释放
- int code = dlclose(flib);
- error_message = dlerror();
- if(error_message){
- return (-1);
- }
- return 0;
- }
#include <dlfcn.h> //句柄 void *flib; //入口函数原型 int (*pfunc)(int a, int b); //错误信息字符串 char *error_message; int plugin_test(){ int a = 1, b = 4; int result = 0; //加载plugina.so,以RTLD_LAZY方式 flib = dlopen("/home/juntao/.libs/plugina.so", RTLD_LAZY); error_message = dlerror(); if(error_message){ return (-1); } //找到函数名为func的函数,返回其指针 *(void **)(&pfunc) = dlsym(flib, "func"); error_message = dlerror(); if(error_message){ return (-1); } //调用pfunc指向的指针,及func函数 result = pfunc(a, b); //释放 int code = dlclose(flib); error_message = dlerror(); if(error_message){ return (-1); } return 0; }
编译运行
假设plugina.so的源文件为plugina.c,内容为:
- //file plugina.c
- int func(int a, int b){
- int c = 0;
- c = 3*a + 4*b + 6;
- return c;
- }
//file plugina.c int func(int a, int b){ int c = 0; c = 3*a + 4*b + 6; return c; }
我们将这个.c文件编译为.so,命令如下:
$gcc -c -fpic plugina.c
//生成plugina.so
$gcc -shared -lc -o plugina.so plugina.o
将动态库访问部分的代码存为plugintest.c,然后使用下列命令编译:
- $gcc -o plugintest plugintest.c -ldl
$gcc -o plugintest plugintest.c -ldl
-ldl意思是,使用库libdl.so,linux下访问搜索路径内的库文件无需加lib前缀。
将生成的plugina.so放入路径/home/juntao/.libs/,然后运行plugintest。
$./plugintest
result = 25
好了,这一次先介绍一些基础知识,相信在此基础上,很多朋友都可以自己设计出一些简单实用的支持“插件”的应用来了,我们下一次详细讨论一个更实际一些的例子,一个计算器的实现,这个计算器只有简单的框剪,所有的运算都通过插件来实现。用户可以通过配置文件来定制插件的路径,入口等信息。
简单介绍了*NIX下的动态库的使用,我们在这篇文章中实现一个计算器,计算器程序calc本身不做运算,只是将操作数传递给具体的插件(adder, suber, muler, diver)来完成实际运算。首先,计算器根据插件配置文件plugin.xml来确定插件的位置,名称,入口符号的定义,然后依次调用各个插件完成计算。
插件列表
文中涉及到的插件定义在plugin.xml中,文档结构如下:
- <plugins>
- <plugin name="adder">
- <library path="/home/juntao/.libs/adder.so">
- </library>
- <entry name="add">
- </entry>
- </plugin>
- <plugin name="suber">
- <library path="/home/juntao/.libs/suber.so">
- </library>
- <entry name="sub">
- </entry>
- </plugin>
- <plugin name="muler">
- <library path="/home/juntao/.libs/muler.so">
- </library>
- <entry name="mul">
- </entry>
- </plugin>
- <plugin name="diver">
- <library path="/home/juntao/.libs/diver.so">
- </library>
- <entry name="div">
- </entry>
- </plugin>
- </plugins>
<plugins> <plugin name="adder"> <library path="/home/juntao/.libs/adder.so"> </library> <entry name="add"> </entry> </plugin> <plugin name="suber"> <library path="/home/juntao/.libs/suber.so"> </library> <entry name="sub"> </entry> </plugin> <plugin name="muler"> <library path="/home/juntao/.libs/muler.so"> </library> <entry name="mul"> </entry> </plugin> <plugin name="diver"> <library path="/home/juntao/.libs/diver.so"> </library> <entry name="div"> </entry> </plugin> </plugins>
每个插件为一个plugin标签,plugin标签中包含library, entry两个字标签,分别定义动态库文件的路径及名称和插件函数的入口。为了简便,我们不重复设计list及xml解析,这里使用libxml2作为xml的分析器,GLIB中的GSList(单链表)来作为插件列表的链表对象。
每一个插件在C语言中的定义如下,非常简单(plugin.h)
- #ifndef _PLUGIN_H_
- #define _PLUGIN_H_
- typedef struct{
- char name[64];
- char path[256];
- char entry[128];
- int version;
- }Plugin;
- #endif
#ifndef _PLUGIN_H_ #define _PLUGIN_H_ typedef struct{ char name[64]; char path[256]; char entry[128]; int version; }Plugin; #endif
这里为了行文方便,Plugin结构中的字符串为静态尺寸。
计算器
计算器调用parseconf模块中的load_plugins将plugin.xml中定义的Plugin加载进一个GSList,以备后用:
- #include "plugin.h"
- extern int load_plugins(char *config, GSList **list);
#include "plugin.h" extern int load_plugins(char *config, GSList **list);
插件中的函数原型应该符合接口定义:
- //pointer to function, which return a double, and get 2 double as input
- double (*pfunc)(double a, double b);
//pointer to function, which return a double, and get 2 double as input double (*pfunc)(double a, double b);
计算器的主要代码如下:
- int calc_test(double a, double b){
- GSList *list = NULL, *it = NULL;
- Plugin *pl = NULL;
- //insert a null node into list at first
- list = g_slist_append(list, NULL);
- int code = 0;
- double result;
- //load plugin defined in plugin.xml into list
- load_plugins("plugin.xml", &list);
- for(it = list; it != NULL; it = it->next){
- pl = (Plugin *)it->data;
- if(pl == NULL){
- continue;
- }else{
- //open the library
- flib = dlopen(pl->path, RTLD_LAZY);
- dlError = dlerror();
- if(dlError){
- fprintf(stderr, "open %s failed\n", pl->name);
- g_slist_free(list);
- return -1;
- }
- //get the entry
- *(void **)(&pfunc) = dlsym(flib, pl->entry);
- dlError = dlerror();
- if(dlError){
- fprintf(stderr, "find symbol %s failed\n", pl->entry);
- g_slist_free(list);
- return -1;
- }
- //call the function
- result = (*pfunc)(a, b);
- printf("%s(%f, %f) = %f\n", pl->entry, a, b, result);
- //then close it
- code = dlclose(flib);
- dlError = dlerror();
- if(code){
- fprintf(stderr, "close lib error\n");
- g_slist_free(list);
- return -1;
- }
- }
- }
- g_slist_free(list);
- return 0;
- }
int calc_test(double a, double b){ GSList *list = NULL, *it = NULL; Plugin *pl = NULL; //insert a null node into list at first list = g_slist_append(list, NULL); int code = 0; double result; //load plugin defined in plugin.xml into list load_plugins("plugin.xml", &list); for(it = list; it != NULL; it = it->next){ pl = (Plugin *)it->data; if(pl == NULL){ continue; }else{ //open the library flib = dlopen(pl->path, RTLD_LAZY); dlError = dlerror(); if(dlError){ fprintf(stderr, "open %s failed\n", pl->name); g_slist_free(list); return -1; } //get the entry *(void **)(&pfunc) = dlsym(flib, pl->entry); dlError = dlerror(); if(dlError){ fprintf(stderr, "find symbol %s failed\n", pl->entry); g_slist_free(list); return -1; } //call the function result = (*pfunc)(a, b); printf("%s(%f, %f) = %f\n", pl->entry, a, b, result); //then close it code = dlclose(flib); dlError = dlerror(); if(code){ fprintf(stderr, "close lib error\n"); g_slist_free(list); return -1; } } } g_slist_free(list); return 0; }
首先,定义一个GSList,然后将其传递给load_plugins,load_plugins解析plugin.xml,然后填充list返回,calc_test遍历插件列表,并调用每一个插件定义的entry.
除法器
我们来看一个具体的插件:做除法的模块
- #include <stdio.h>
- double div(double a, double b){
- if(b == 0){
- fprintf(stderr, "div zero error\n");
- return -1;
- }else{
- return a / b;
- }
- }
#include <stdio.h> double div(double a, double b){ if(b == 0){ fprintf(stderr, "div zero error\n"); return -1; }else{ return a / b; } }
diver.c在编译之后,生成diver.so,将其置于plugin.xml定义的位置处即可。
运行结果如下图所示:
其他代码如xml的解析,GSList的使用等与插件机制关系不大,感兴趣的朋友可以在附件中查看。