插件问题回答第1题

问题贴: http://cloverprince.iteye.com/blog/481307

引用
1. 现有一个主程序用C语言写成。现在要允许第三方开发人员编写扩展的模块,约定第三方开发的模块必须提供一系列已知名称的函数(如 foo(),bar(),baz())。如果要求第三方的模块必须与主程序的二进制代码分开发布,把dll或so丢在某个文件夹内即可被动态装载并使用,应如何实现?



回答:

操作系统提供了shared object的动态装载功能。定义在dlfcn.h中。调用dlopen()打开插件,dlsym()获取函数,dlclose()关闭插件。


适用于:

*nix。在Linux中实验成功。


实现:

我们假定每个插件提供两个函数:
void hello(void);  // 显示Hello world
void greet(char* name);  // 给你打招呼


为了简化装载,我们用一个struct储存以上两个函数的指针,并要求每个插件内提供一个叫init_module的函数,填充该struct。

接口头文件如下:
/* plugin-interface.h */
#ifndef _PLUGIN_INTERFACE_H_
#define _PLUGIN_INTERFACE_H_
 
#ifdef __cplusplus
extern "C" {
#endif
 
    typedef struct _PluginInterface { // 这个结构储存了插件需要提供的所有函数的指针
        void (*hello)(void);
        void (*greet)(char* name);
    } PluginInterface;
 
    typedef void (*InitModuleFunc)(PluginInterface* iface); // 这个函数填充上述结构。
 
#ifdef __cplusplus
}
#endif
 
#endif
/* end of plugin-interface.h */


总体的目录结构如下:
引用
.
|-- Makefile
|-- main
|-- main.c
|-- plugin-interface.h
`-- plugins
    |-- Makefile
    |-- goodbyeworld.c
    |-- goodbyeworld.o
    |-- goodbyeworld.so
    |-- helloworld.c
    |-- helloworld.o
    `-- helloworld.so


plugins里的.so将被主程序main装载。
看看plugins/helloworld.c:
/* plugins/helloworld.c */
#include <stdio.h>
#include "../plugin-interface.h"
 
// 两个功能函数hello, greet的名称随便。我们只关心它们的指针。
void hw_hello() {
    printf("Hello world!\n");
}
 
void hw_greet(char* name) {
    printf("Hello, %s\n",name);
}
 
// 填充PluginInterface结构。
void init_module(PluginInterface *iface) {
    iface->hello = hw_hello;
    iface->greet = hw_greet;
}
/* end of plugins/helloworld.c */


plugins/goodbyeworld.c是另一个插件

/* plugins/goodbyeworld.c */
#include <stdio.h>
#include "../plugin-interface.h"
 
void gw_hello() {
    printf("Goodbye world!\n");
}
 
void gw_greet(char* name) {
    printf("Goodbye, %s\n",name);
}
 
void init_module(PluginInterface *iface) {
    iface->hello = gw_hello;
    iface->greet = gw_greet;
}
/* end of plugins/goodbyeworld.c */


最后,main.c是主程序。
/* main.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
#include <dlfcn.h> // dlopen(), dlsym(), dlclose()
 
#include <sys/types.h>
#include <dirent.h>
 
#include "plugin-interface.h"
 
#define MAX_PLUGINS 10
char PLUGINS_PATH[] = "plugins";
 
struct {
    char path[256];  // 插件文件所在路径(仅供显示用)
    void* lib_handle;  // 库句柄
    PluginInterface iface;  // 接口结构,在plugin-interface.h中定义
} plugins[MAX_PLUGINS];  // 每个struct对应一个插件
 
int n_plugins;
 
void load_plugin(char* path) {
    void* lib_handle;
    InitModuleFunc init_func;
    char* err;
 
    lib_handle = dlopen(path, RTLD_LAZY); // 打开库
    err = dlerror();
    if(lib_handle==NULL) {
        fprintf(stderr,"Cannot open %s: %s\n",path,err);
        return;
    }
 
    init_func = dlsym(lib_handle, "init_module"); // 找到init_module函数。
    err = dlerror();
    if(err != NULL) {
        fprintf(stderr,"Cannot find function 'init_module' in %s: %s\n",path,err);
        dlclose(lib_handle);
        return;
    }
 
    strcpy(plugins[n_plugins].path,path);
    plugins[n_plugins].lib_handle=lib_handle;
    init_func(&plugins[n_plugins].iface); // 利用插件中的"init_module"函数,填充iface结构
    n_plugins++;
 
    fprintf(stderr,"Plugin successfully loaded: %s\n",path);
}
 
int main() {
    DIR *dir;
    struct dirent *dent;
 
    int i;
 
    // 读取目录,装载所有的库
 
    dir = opendir(PLUGINS_PATH);
    if(dir==NULL) {
        perror("opendir");
        exit(1);
    }
 
    while((dent=readdir(dir))!=NULL) {
        int name_len;
        char plugin_path[256];
 
        name_len = strlen(dent->d_name);
        if(name_len<3) continue;
        if(strcmp(dent->d_name+name_len-3,".so")!=0) continue;  // 找到所有.so的文件
 
        sprintf(plugin_path,"%s/%s",PLUGINS_PATH,dent->d_name);
 
        load_plugin(plugin_path); // 尝试装载这个.so的文件
    }
 
    closedir(dir);
 
    // 测试每个插件
 
    for(i=0;i<n_plugins;i++) {
        fprintf(stderr, "Testing %s ...\n",plugins[i].path);
        plugins[i].iface.hello();
        plugins[i].iface.greet("wks");
    }
 
    // 卸载
 
    for(i=0;i<n_plugins;i++) {
        dlclose(plugins[i].lib_handle);
    }
 
    return 0;
}
/* end of main.c */


编译:
# Makefile
all: main
 
main: main.c plugin-interface.h
    gcc -rdynamic -ldl -o $@ $^
# end of Makefile


main.c的dlopen()等函数需要-ldl选项。-rdynamic选项也是dlopen()等函数需要的。

# plugins/Makefile
all: helloworld.so goodbyeworld.so
 
helloworld.so: helloworld.c
    gcc -c -fPIC helloworld.c
    gcc -shared -o helloworld.so helloworld.o
 
goodbyeworld.so: goodbyeworld.c
    gcc -c -fPIC goodbyeworld.c
    gcc -shared -o goodbyeworld.so goodbyeworld.o
# end of plugins/Makefile


这些是插件的编译方法。-fPIC是构造so的必要条件。
另一个选项是-Wl,-soname,xxxxxxx.so.x,这对动态链接(静态装载)有用,但是不加这个选项仍然可以动态装载。

执行时,需要的最少的目录结构如下:
引用
.
|-- main
`-- plugins
    |-- goodbyeworld.so
    `-- helloworld.so


执行:
引用
[wks@localhost out]$ ./main
Plugin successfully loaded: plugins/goodbyeworld.so
Plugin successfully loaded: plugins/helloworld.so
Testing plugins/goodbyeworld.so ...
Goodbye world!
Goodbye, wks
Testing plugins/helloworld.so ...
Hello world!
Hello, wks


总结:
1. main程序并不了解plugins目录中有多少插件。在运行时列举目录。
2. main程序对每个plugins文件(比如叫helloworld.so)的了解只有:
- helloworld.so中有一个函数叫init_module,可以填充PluginInterface结构。
- helloworld.so将实现hello和greet两个函数,但函数名可以不知道。函数指针被init_module提供。
- 用PluginInterface结构中的函数操作插件。


参考:
http://www.yolinux.com/TUTORIALS/LibraryArchives-StaticAndDynamic.html

你可能感兴趣的:(C++,c,linux,C#,gcc)