问题贴:
http://cloverprince.iteye.com/blog/481307
引用
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类实例的指针,即可利用多态性进行操作,不必关心具体类的实现细节。