我正在开发一个应用程序,它为客户提供插件接口,以便在应用程序内开发他们的逻辑。然后在运行时动态加载插件。我们为插件提供了一个干净的 C 接口,使事情尽可能可移植。然而,我们最近发现了过渡依赖项的一个问题:当插件链接到它自己的依赖项时,这恰好也是应用程序的依赖项,仅加载应用程序附带的版本。
所以在下面的配置中,lib_b.dll是插件,它用作lib_a.dll私有依赖项。尽管因为Executable还链接到同一库的不同版本,但未选择它们的版本。
+----------------------+ +-------------------------------+
| | LoadLibrary | |
| Executable.exe ------+--------------+--> plugins |
| | | | | |
| +--> lib_a.dll (v1) | | +--> lib_b.dll |
| | | | |
+----------------------+ | +--> lib_a.dll (v2) |
| |
+-------------------------------+
Run Code Online (Sandbox Code Playgroud)
我正在寻找一种解决方案来将地址空间和依赖符号与我的应用程序隔离。这个想法是Executable只关心运行时从插件加载的符号,而不是插件内部使用的符号。
我们像这样加载_library:
HMODULE h = ::LoadLibraryExA(".../plugins/library_b.dll",
NULL, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR);
Run Code Online (Sandbox Code Playgroud)
该插件中唯一有趣的一点是这样实现的:
callback_type do_the_thing_in_b = GetProcAddress(h, "do_the_thing_in_b");
int answer = do_the_thing_in_b(3.14, 42);
Run Code Online (Sandbox Code Playgroud)
更新
应用程序可以随时重新编译或更改,但在编译时我们没有插件的信息。这个想法是客户可以创建自己的插件并将其放在那里。
我们也无法修改插件。我们可以扫描该目录并执行一些操作,但这就是我们可以执行的更改范围。我们无法重新编译它们或决定它们的依赖结构。
插件通过接口库再次链接我们的可执行文件,并直接从可执行文件调用函数
总之,
我认为您已经用“隔离地址空间”击中要害,就像 Daniil Vlasenko 在评论中建议的那样。这也是 FireFox 现在使用选项卡所做的事情;每一个都是一个单独的过程。
如果您可以控制界面,那么您就可以将主可执行文件分离到一个进程中,并拥有一个可以加载单个插件的 DLL(及其依赖项)的精简填充进程。您将为每个插件启动其中一个垫片。这些进程将通过某种机器内 RPC 进行通信。这一切都假设可执行文件和插件之间不共享数据结构,除了通过函数调用和返回之外。
那会是什么样子?
RPC(例如 GPB-RPC)感觉不太正确,因为您需要一个 RPC 以一种方式让可执行文件调用插件,但需要考虑到插件如何依次调用调用可执行文件中的函数。
我将使用 ZeroMQ 之类的东西作为主可执行文件和插件之间的传输来解决这个问题,并使用 GPB 之类的东西来制定消息,1)指示要调用什么函数,2)它的参数是什么。该序列将类似于:
这允许的目的是
ZMQ 将会很有用,因为听起来您在主可执行文件和插件之间没有直接的客户端/服务器或 RPC 关系;他们更加相互依赖。ZMQ 是 Actor 模型,它允许如上所述的这种模式。
ZeroMQ 的一个免费赠品是,掌握了这一点后,插件可以轻松地完全安装在另一台机器上,或者组合起来(即一些本地的、一些远程的、一些在世界另一端的 Linux 上运行等) )。
显然,如果存在共享数据结构,那么拥有两个独立的进程将无济于事,尽管我认为可以通过将它们放置在共享内存中来克服这个问题。但是所有插件 shim 进程都必须与主可执行文件位于同一台计算机上。
如果事情更加多线程,我认为上面概述的模式实际上不会有太大变化。您可能希望消息中的字段指示正在发生的情况。如果插件正在操作由主可执行文件创建的信号量、互斥体等,则可能会变得更加困难。