如何为模块化应用程序实现 C++ 插件系统?

How to Implement a C++ Plugin System for a Modular Application?

我正在尝试设计一个模块化的应用程序;开发人员将插件创建为 dll,使用 loadlibrary() 或 dlopen() 链接到主应用程序。我目前对此的想法是:

1: Both the application and plugin module include a core header with a pure virtual class IPlugin with a method run(). The plugin module then implements the class, defining run():

2: The plugin module defines a function IPlugin* GetPlugin() using "extern c" to ensure ABI compatibility

3: The application requires the plugin module using loadlibrary(), retrieves IPlugin from GetPlugin() using getprocaddress()

4: The application calls method run() to run the plugin

一切正常,但我如何创建一个可以访问我的完整应用程序代码的插件界面?如果我有一个 class 创建一个 window 或初始化一个按钮,我想通过插件界面访问它们并能够操纵它们并接收事件,同时仍然让内存由主要应用端。

1.我该怎么做呢?

2。最好的解决方案是否与 ABI 兼容?

API 要么使用 C++,要么使用 C,没有中间立场。如果你想强制插件开发人员使用 ABI 兼容的编译器,那么你可以有一个处理 classes 和虚方法的接口,并且不需要任何“extern C” brouhaha。

否则,您只能提供 C API 和 C API。当然,您可以使用函数指针结构实现虚函数 table,并且在内部可以将其包装在 C++ class 中等等。但那是你的实现细节,并没有体现在插件界面本身的设计中。

差不多就是这样。在 Windows 上没有免费的 C++ API 兼容性。人们使用至少 5 个不兼容的编译器——MSVC 2017+、2015、2012、mingw g++ 和 clang。一些特别落后的公司机构有时会坚持使用甚至更旧的 MSVC。因此,除非您为所有这些用户提供垫片,否则 C++ 接口基本上是一个失败的原因。在持续集成 (CI) 的这些日子里,这并非不可想象 - 您可以轻松构建使用 C API 的包装器,并通过与用户首选开发系统兼容的 C++ 接口将其公开给用户。但这并不意味着您可以直接从您的 C++ 代码中弄乱他们的 C++ object。仍然有 C 中介,您仍然需要在适配器代码中使用帮助程序。例如。您不能直接删除提供的用户 object - 您只能通过调用适配器 DLL 中的帮助程序来实现 - 一个使用相同运行时并且与用户的 C++ 代码 ABI 兼容的帮助程序。

并且不要忘记对于任何给定的编译器版本都存在二进制不兼容的运行时库 - 例如调试与发布以及单线程与多线程。因此,对于任何 MSVC 版本,您都必须提供插件开发人员 link 使用的四个适配器 DLL。然后将您的代码 link 也发送到该适配器,以访问用户的插件。因此,您将首先解析插件中的二进制 headers 以确定它正在使用的适配器 DLL 和运行时,如果它们不匹配则发出错误消息(插件开发人员很可能会搞砸)。然后,如果匹配,则加载插件 DLL。动态 linker 将引入必需的适配器 DLL。然后就可以使用适配器DLL来访问插件了。

我之前做过这个,我的建议是让每个适配器 dll 为您的主机程序提供不同的 C 符号,因为总是会有多个插件,每个插件使用不同的适配器,这只会使事情复杂化。相反,您需要通过在 Windows 上按需加载 link 到所有适配器,并且仅在解析插件 DLL 以了解其使用的内容后才访问特定的适配器。然后,当您调用适配器时,动态 linker 将为真正的适配器函数解析需求负载存根。可以在 non-Windows 平台上使用类似的方法,但需要编写辅助代码来提供需求 link 功能 - 因此在 Unix 上显式使用 dlopen 可能是最简单的。您仍然需要解析插件的 ELF header 以找出它使用的 C++ 运行时和它期望的适配器库,验证组合;然后才加载它。然后你会 dlopen 适配器与插件对话。在任何情况下,您都不会直接调用插件本身的任何函数——只有当您需要跨越 C++ 运行时边界时,适配器才能安全地执行此操作。可能有更简单的方法来完成所有这些,但我的经验是它们只能完成 99% 的工作,所以最终这里没有廉价的解决方案——除非有人编写了一个开源项目(或产品)来完成所有这些。如果你涉足其中,你将被期望理解肮脏的实现细节并处理 C++ 运行时错误。因此,如果您宁愿避免所有这些 - 不要混淆需要可定位库的 C++ 用户可见 API。

你当然可以为用户做一个 header only C-to-C++ 桥,但令人惊讶的是一些用户不喜欢 header only 解决方案。叹。我怎么知道?我不得不摆脱一个 header-only 界面来提高客户的坚持……那天我像个疯子一样又哭又笑。