BBYR Achieve
返回信息流
这是一条镜像帖。来源:北邮人论坛 / soft-design / #35847同步于 2009/10/1
该镜像源已超过 30 天没有更新,可能在源站已被删除。
SoftDesign机器人发帖

[脑残的回答]问题2

wks
2009/10/1镜像同步3 回复
2. 现有一个主程序用C++语言写成。现在要允许第三方开发人员编写扩展的类,约定第三方开发的类必须包含一个继承自某个已知类(如class FooPlugin)的子类,名称不限。如果要求第三方的类必须与主程序的二进制代码分开发布,把dll或so丢在某个文件夹内即可被动态装载使用,应如何实现? 回答: 和C一样,使用shared object和动态装载。但是不同的是,由于需求中以“类”为插件的单位,类相当于一种数据类型(和算法),而数据类型并不是so中可以储存的东西。所以,我们必须转而储存过程。实现中,在插件中使用“工厂函数”实现对象的创建。工厂函数是一个过程,返回的是已经创建好的对象,这样隐藏了对象的实现细节。而对象的接口定义在.h头文件中,在编译期已经确定了调用方法。 适用范围: 思想适用于任何系统。但,由于C++虚函数表和name mangling的实现问题,要求主程序和插件使用相同编译器的相同版本编译。本示例在Linux+GCC(g++)4.4.1下编译通过。 实现: 下面是目录结构: . |-- Makefile |-- main |-- main.cpp |-- plugin-interface.h `-- plugins |-- Makefile |-- goodbyeworld.cpp |-- goodbyeworld.o |-- goodbyeworld.so |-- helloworld.cpp |-- helloworld.o `-- helloworld.so plugin-interface.h中定义了接口。 /* plugin-interface.h */ #ifndef _PLUGIN_INTERFACE_H_ #define _PLUGIN_INTERFACE_H_ #include <string> class IPlugin { 这个虚基类是接口。 public: virtual void setName(std::string name)=0; // 设置名字 virtual void greet()=0; // 打招呼 }; extern "C" { typedef IPlugin* (*PluginFactoryFunc)(); // 工厂函数,创造一个IPlugin实例 } #endif /* end of plugin-interface.h */ plugin目录中放置多个插件。插件的实现方法如下: /* plugins/helloworld.cpp */ #include <iostream> #include <string> #include "../plugin-interface.h" using namespace std; class HelloWorldPlugin : IPlugin { // 一个插件的具体实现 string name; public: void setName(string name) { // 重写(override)了虚函数 this->name = name; } void greet() { cout<<"Hello, "<<name<<endl; } }; extern "C" { IPlugin* factory() { // 工厂函数。 return (IPlugin*)(new HelloWorldPlugin()); } } /* end of plugins/helloworld.cpp */ 另一个插件类似: /* plugins/goodbyeworld.cpp */ #include <iostream> #include <string> #include "../plugin-interface.h" using namespace std; class GoodbyeWorld : IPlugin { string name; public: void setName(string name) { this->name = name; } void greet() { cout<<"Goodbye, "<<name<<endl; } }; extern "C" { IPlugin* factory() { return (IPlugin*)(new GoodbyeWorld()); // 工厂创建不同的对象 } } /* end of plugins/goodbyeworld.cpp */ 主程序如下: /* main.cpp */ #include <iostream> #include <cstdlib> #include <string> #include <vector> #include <dlfcn.h> #include <boost/filesystem.hpp> // 使用boost_filesystem库代,更符合c++风格。 #include "plugin-interface.h" using namespace std; namespace fs = boost::filesystem; const int MAX_PLUGINS=10; fs::path PLUGINS_PATH("plugins"); struct PluginInfo { // 插件记录。每个插件对应一个 string path; // 路径/文件名 void* lib_handle; // 库句柄 PluginFactoryFunc factory; // 工厂函数 }; vector<PluginInfo> plugins; void load_plugin(string path) { PluginInfo pi; char* err; pi.path = path; pi.lib_handle = dlopen(path.c_str(), RTLD_LAZY); // 仍然使用dlopen打开so库 err = dlerror(); if(pi.lib_handle==NULL) { cerr<<"Cannot open "<<path<<": "<<err<<endl; return; } pi.factory = (PluginFactoryFunc)dlsym(pi.lib_handle, "factory"); // 取出工厂函数 err = dlerror(); if(err != NULL) { cerr<<"Cannot find function 'factory' in "<<path<<": "<<err<<endl; dlclose(pi.lib_handle); return; } plugins.push_back(pi); // 储存对象记录 cerr<<"Plugin successfully loaded: "<<path<<endl; } int main() { fs::directory_iterator end_iter; for(fs::directory_iterator dir_iter(PLUGINS_PATH); dir_iter!=end_iter; ++dir_iter) { // 遍历plugins/* string filename; string pathname; filename = dir_iter->path().filename(); if(filename.length()<3) continue; if(filename.substr(filename.length()-3,3)!=".so") continue; //检查后缀 pathname = PLUGINS_PATH.filename() + "/" + filename; load_plugin(pathname); // 装载插件 } vector<PluginInfo>::iterator it; for(it=plugins.begin();it!=plugins.end();++it) { // 遍历测试插件 cerr<<"Testing "<<it->path<<" ..."<<endl; IPlugin *plugin = it->factory(); // 创建实例 plugin->setName("wks"); // 设置名字 plugin->greet(); // 打招呼 delete plugin; // 析构插件对象的实例 } for(it=plugins.begin();it!=plugins.end();++it) { // 卸载库 dlclose(it->lib_handle); } return 0; } /* end of main.cpp */ 编译: 编译过程和C语言版本类似。 # Makefile all: main main: main.cpp plugin-interface.h g++ -rdynamic -ldl -lboost_filesystem -o $@ $^ # End of Makefile 需要注意的是这里用到了boost_filesystem库 下面是插件的Makefile # plugins/Makefile all: helloworld.so goodbyeworld.so helloworld.so: helloworld.cpp g++ -c -fPIC helloworld.cpp g++ -shared -o helloworld.so helloworld.o goodbyeworld.so: goodbyeworld.cpp g++ -c -fPIC goodbyeworld.cpp g++ -shared -o goodbyeworld.so goodbyeworld.o # end of plugins/Makefile 仅仅换了编译器而已。 执行: 执行需要的最少文件如下: . |-- 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, wks Testing plugins/helloworld.so ... Hello, wks 总结: 1. main程序并不了解plugins目录中有多少插件。在运行时列举目录。 2. main程序对每个plugins文件(比如叫helloworld.so)的了解只有: - helloworld.so中有一个函数叫factory,可以填创建IPlugin实例。 - helloworld.so中实现的该类,实现了IPlugin定义的setName和greet两个方法。其调用通过C++类的虚函数表查询得到,涉及到C++的运行时实现细节。 - 将对象转换成IPlugin类实例的指针,即可利用多态性进行操作,不必关心具体类的实现细节。
订阅后,新回复会通过你的通知中心匿名送达。
3 条回复
windam机器人#1 · 2009/10/5
补充个问题: 跨模块的接口最好不要包含stl容器。 具体来说:如果在exe中调用dll的代码,这个dll接口中不应当有string或者vector这类容器。 class IPlugin { 这个虚基类是接口。 public: virtual void setName(std::string name)=0; // 设置名字 virtual void greet()=0; // 打招呼 }; 也就是说,这个setName存在隐患: 如果dll和exe各自链接了静态的crt库,就会在跨模块传递对象的时候产生crt冲突。(vc下会崩溃,不知gcc下的情况咋样) 替代的方法是使用自行定义的allocator的容器,自己指明使用某个特定的crt堆。 或者是只用char数组/指针。 p.s.所有二进制模块都链接动态crt的情况下则该问题不会暴露出来。
wks机器人#2 · 2009/10/5
用g++ 4.4.1工作“正常”。主程序和插件使用同样的编译器编译的。 我早就想过这个问题。不同的编译器(确切的说是stl库)的实现可能会不同,传入的string的内部结构不一定是相同的。 不知道java会不会出现类似的问题。没试过gcj和sun java编译出来的String对象兼容不兼容。 【 在 windam 的大作中提到: 】 : 补充个问题: : 跨模块的接口最好不要包含stl容器。 : 具体来说:如果在exe中调用dll的代码,这个dll接口中不应当有string或者vector这类容器。 : ...................
FadeToBlack机器人#3 · 2009/10/5
关于不同编译器的编译后相同类的二进制布局的问题,在《COM本质论》一书中前几章有提到过一种解决方案。 对每一个接口设置一个ID,然后通过GetInterface(UINT ID)来获得接口。在类内部通过引用计数来进行管理。 大致就是这个意思。 【 在 wks (cloverprince) 的大作中提到: 】 : 2. 现有一个主程序用C++语言写成。现在要允许第三方开发人员编写扩展的类,约定第三方开发的类必须包含一个继承自某个已知类(如class FooPlugin)的子类,名称不限。如果要求第三方的类必须与主程序的二进制代码分开发布,把dll或so丢在某个文件夹内即可被动态装载使用 : 回答: : 和C一样,使用shared object和动态装载。但是不同的是,由于需求中以“类”为插件的单位,类相当于一种数据类型(和算法),而数据类型并不是so中可以储存的东西。所以,我们必须转而储存过程。实现中,在插件中使用“工厂函数”实现对象的创建。工厂函数是一个过程,返 : ...................