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;
return it->second();
}
private:
PluginFactory(const PluginFactory&) = delete;
PluginFactory& operator=(const PluginFactory&) = delete;
// 构造各个类实例的工厂函数
std::unordered_map<std::string, std::function<std::shared_ptr<IBase>()>> constructors;
};
extern "C" IPluginFactory* getPluginFactory()
{
static PluginFactory instance;
return &instance;
}
// IMyPlugin.h
// 类接口
struct IMyClassA : public IBase
{
virtual void func() const = 0;
};
// MyPlugin.cpp
// 类实例
class MyClassA : public IMyClassA
{
public:
MyClassA() = default;
virtual ~MyClassA() = default;
virtual void func() const { std::cout << "hello world" << std::endl; }
};
// 插件自动注册
struct MyPlugin
{
MyPlugin()
{
PluginFactory* factory = dynamic_cast<PluginFactory*>(getPluginFactory());
factory->registerClass("MyPlugin.MyClassA",
[]{ return std::static_pointer_cast<IBase>(std::shared_ptr<MyClassA>::create()); });
}
~MyPlugin()
{
PluginFactory* factory = dynamic_cast<PluginFactory*>(getPluginFactory());
factory->unregisterClass("MyPlugin.MyClassA");
}
}
static MyPlugin myPlugin;
// main.cpp
int main(int argc, char* argv[])
{
(void)argc;
(void) argv;
// 加载插件
HANDLE hModule = ::LoadLibrary("MyPlugin.dll");
// 获取实例
IPluginFactory* factory = getPluginFactory();
auto myClassA = factory->createInstance<IMyClassA>("MyPlugin.MyClassA");
if (myClassA)
myClassA->func();
// 释放插件
myClassA.reset();
::FreeLibrary (hModule);
return 0;
}
实际实现中,各插件框架,都会有个宏用于声明插件,这个宏有一般是放置在全局域,展开后自动生成类似MyPlugin类的代码和对象声明。
这个宏的输入参数里至少会包含插件类的ID和插件类名。
虽然全局静态对象的初始化顺序是未知的,但这里的依赖是安全的——工厂对象是函数内的静态对象,只有在函数调用时才会完成初始化,所以可以确保,宏定义出的这个全局对象构造时,通过getPlugin()肯定能获取到已经初始化完成的插件工厂。
对了,如果你约束好所有插件接口函数的输入输出类型,通过可以跨编译器平台的数据结构实现,如C++11的标准布局(Standard Layout),那么你这个插件哪怕是用MinGW编译的,也能拿给MSVC使用——.COM就是这么实现的。
最后的就是一些扩展功能了,比如运行时热更新:
- 为IBase提供start()/stop()两个方法,并且在IBase的构造/析构中,更新框架内部持有的对象列表;
- 热更新插件时,根据对象列表,stop()该插件所有类实例;
- 就地析构(显示调用构造函数),销毁该插件所有类实例;
- 卸载插件,更新动态库,重新加载插件;
- 就地构造(placement new),在原有插件对象的指针地址上重新创建类实例,以避免用户代码里持有的插件对象指针失效。
如果再通过命令模式/状态机等约束插件行为的话,甚至可以还原卸载前的现场。
================ End
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
2021-04-05 Hackintosh OpenCore
2020-04-05 Q_INVOKABLE与invokeMethod用法全解
2020-04-05 Qt元对象系统源码解析
2020-04-05 Qt信号槽机制源码解析
2020-04-05 C++ 中的 mutable 关键字
2020-04-05 浅析Qt(C++),QML与HTML之间的交互
2020-04-05 QML中的信号和函数