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



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

1:应用程序和插件模块都包含一个核心标头,其中包含一个纯虚拟类IPlugin和一个方法run()。然后插件模块实现类,定义run():

2:插件模块使用 "extern c" 定义了一个函数IPlugin* GetPlugin()来确保 ABI 兼容性

3:应用程序需要使用loadlibrary()的插件模块,使用getprocaddress()

4从GetPlugin()中检索IPlugin应用程序调用方法run()来运行插件

这一切都有效,但是如何创建一个可以访问我的完整应用程序代码的插件接口?如果我有一个创建窗口或初始化按钮的类,我想通过插件接口访问它们并能够操作它们并接收事件,同时仍然由主应用程序端管理内存。

1. 我将如何做到这一点?

2. 最佳解决方案是否与 ABI 兼容?

API 要么使用C++,要么使用 C,没有中间地带。如果你想强制插件开发人员使用ABI兼容的编译器,那么你可以有一个处理类和虚拟方法的接口,不需要任何"extern C"的喧嚣。

否则,您仅提供 C API 和 C API。当然,您可以使用函数指针的结构来实现虚拟函数表,并且在内部您可以将其包装在C++类中等等。但这是您的实现细节,并没有出现在插件接口本身的设计中。

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

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

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

您当然可以为用户进行仅标头 C 到C++的桥接,但令人惊讶的是,有些用户不喜欢仅标头解决方案。叹息。我怎么知道?我不得不摆脱仅标题界面以提高客户的坚持......那天我一下子哭了,笑得像个疯子。

最新更新