c++中如何为一个程序写扩展
c++中如何为一个程序写扩展
https://www.zhihu.com/question/52538590
作者:mnzn2530
链接:https://www.zhihu.com/question/52538590/answer/2421788573
首先,不同的范围选择是不一样的。
1。 如果是同一个编译器,同一个版本,你可以直接用动态库,导出函数,变量,类都是可以的。
2。 如果不同的编译器,那么COM基本上是唯一的选择,当然COM的强大不止这一点,遵守com规范,你的插件可以给vba用,给微软的wscript用,可以直接嵌入到office里用。
3。 如果跨平台,那么qt的plugin很不错,但是plugin的实现感觉和com还不大一样,qt的plugin只导出了两个函数,但是它也可以直接使用导出类。。非常方便。。。。
当然,这都是在有头文件的情况下。。。如果没有头文件,你完全不知道dll里在干啥。。。那就要参考以前的外挂怎么写了。。。。也不是不行。。。。
链接:https://www.zhihu.com/question/52538590/answer/137136003
我简单的说明下c/c++里的插件系统是怎么运行的吧。包括.COM、Qt Plugin等各种框架的插件机制,基本都是这样的原理。
Windows/Linux均支持通过文件名运行时加载动态链接库,通过函数符号名称获得函数指针,故:
- 定义纯虚基类作为Interface(如果有Java基础比较好理解)。
- 把实现类封装为dll文件,用LoadLibrary运行时载入。
- 通过C API获取插件对象实例。因为C++ ABI在不同编译器、不同编译器版本之间有差异,而的C ABI是稳定的。
所以就可以这么做了——
- 写一个接口类,内部都是纯虚函数,用作定义对外接口。
- 写一个实例类,继承实现这个接口。这个类不用导出。
- 导出一个C函数getInstance如下。
- 使用插件者,通过文件名在运行时加载dll;
- 使用插件者,通过字符串"getInstance"获取到函数指针;
- 运行函数指针,得到对象实例。然后就可以通过接口调用了。
extern "C" std::shared_ptr<ISomeInterface> getInstance()
{
return std::dynamic_pointer_cast<ISomeInterface>(std::make_shared<MyImplementClass>());
}
上述make_shared这一步,需要封装在库里,暴露一个函数接口,然后可以用上面那个模板函数进一步封装以方便使用。
注:
对纯虚方法的引用,可以直接编译通过,不需要链接方法实现。
所以使用者(程序本身)只需要include接口描述,就可以在代码里使用该接口类型的指针对象了,可以顺利编译通过,不需要链接。
然后实际运行时,就可以随意替换实现类(替换插件dll库),然后通过配置文件或其他手段,通知程序从某个dll插件加载实例即可。
使用者代码如下:
// 加载dll
HMODULE lib = LoadLibrary("xxx.dll");
// 解析函数指针
std::function<std::shared_ptr<ISomeInterface>(void)> getInstanceFunc = GetProcAddress(lib, "getInstance");
// 获得对象
this->myPlugInstance = getInstanceFunc();
// 通过接口随便操作咯
this->myPlugInstance->doSomething();
程序退出前别忘了通过FreeLibrary卸载dll库。
Linux下同理,只不过变成了.so库,同样有对应的系统API完成这些操作。
======== 更新 ========
前面的写法是手敲的,没考虑是否能编译,当伪码看就行。
评论中就遇到了问题——VC编译器给extern "C"添加了额外的约束,不能用来传递类对象。
所以我更新下实际业界中的实现,比上面的复杂一些,但是更实用。
实际实现中,使用了抽象工厂+单例两个设计模式:
- 软件框架统一提供一个插件工厂,用户通过约定的插件id(比如.COM的GUID,比如company.product.module.class这样的字符串标识)创建插件实例。
- 插件工厂提供注册接口,插件加载进内存后将各类型的构造器和id注册进去。
- 插件动态库制作一个static全局静态对象,构造函数里注册插件类,析构函数里取消注册,这样可以在动态库加载时自动注册,卸载时自动取消注册。
- 整个插件库不需要导出任何接口,因为纯虚接口无需链接,对象则是统一从框架的插件工厂获取。
代码如下,在MinGW和MSVC编译器上都可通过。
本方法需要开启RTII和C++11,实际上几乎所有C++插件框架,都依赖于RTII。
手机慎入,因为有大量模板。电脑可流畅阅读,已尽量控制行宽80字符。
// IPluginFactory.h 框架接口,所有用户代码/插件代码均链接这个框架库,类似Qt里的Qt5Core.dll
// IBase: 所有插件接口的基类,可以使用各类框架的Object类型,比如Qt的QObject
// 最好内置引用计数,如此处
struct IBase : public std::enable_shared_from_this<IBase>
{
virtual ~IBase() = default;
};
// 插件工厂接口
struct IPluginFactory
{
virtual ~IPluginFactory() = default;
template<typename T>
std::shared_ptr<T> createInstance(const std::string id)
{ return std::static_pointer_cast<T>(createInstanceWithBase(id); }
protected:
virtual std::shared_ptr<IBase> createInstanceWithBase(const std::string& id) = 0;
};
// 整个dll,只需要导出这唯一一个符号,其他所有类都不需要导出
extern "C" IPluginFactory* getPluginFactory();
// PluginFactory.cpp
// 插件工厂实例,此处使用std::string作为类标识,便于使用
class PluginFactory : public IPluginFactory
{
public:
PluginFactory() {}
virtual ~MyPlugin() = default;
bool registerClass(const std::string& id,
std::function<std::shared_ptr<IBase>()> constructor)
{
if (constructors.find(id) != constructors.end())
return false;
constructors[id] = constructor;
}
void unregisterClass(const std::string& id)
{
auto it = constructors.find(id);
if (it != constructors.end())
constructors.erase(it);
}
protected:
virtual std::shared_ptr<IBase> createInstanceWithBase(const std::string& id)
{
auto it = factories.find(id);
if(it == factories.end())
return nullptr