如何隔离依赖关系与传递依赖解析?

sor*_*h-r 9 c++ windows dll

我正在开发一个应用程序,它为客户提供插件接口,以便在应用程序内开发他们的逻辑。然后在运行时动态加载插件。我们为插件提供了一个干净的 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)

更新

应用程序可以随时重新编译或更改,但在编译时我们没有插件的信息。这个想法是客户可以创建自己的插件并将其放在那里。

我们也无法修改插件。我们可以扫描该目录并执行一些操作,但这就是我们可以执行的更改范围。我们无法重新编译它们或决定它们的依赖结构。

插件通过接口库再次链接我们的可执行文件,并直接从可执行文件调用函数

baz*_*zza 0

总之,

  • 您有一个应用程序,带有定义的插件接口
  • 您定义并控制该接口
  • 插件依赖于能够调用应用程序内部的某些函数
  • 插件和应用程序可能具有冲突的视图,其中依赖项的正确版本是正确的,并且应用程序的视图获胜
  • 插件不会调用其他插件中的函数

我认为您已经用“隔离地址空间”击中要害,就像 Daniil Vlasenko 在评论中建议的那样。这也是 FireFox 现在使用选项卡所做的事情;每一个都是一个单独的过程。

如果您可以控制界面,那么您就可以将主可执行文件分离到一个进程中,并拥有一个可以加载单个插件的 DLL(及其依赖项)的精简填充进程。您将为每个插件启动其中一个垫片。这些进程将通过某种机器内 RPC 进行通信。这一切都假设可执行文件和插件之间不共享数据结构,除了通过函数调用和返回之外。

那会是什么样子?

RPC(例如 GPB-RPC)感觉不太正确,因为您需要一个 RPC 以一种方式让可执行文件调用插件,但需要考虑到插件如何依次调用调用可执行文件中的函数。

我将使用 ZeroMQ 之类的东西作为主可执行文件和插件之间的传输来解决这个问题,并使用 GPB 之类的东西来制定消息,1)指示要调用什么函数,2)它的参数是什么。该序列将类似于:

  1. 每个插件有 2 个 ZMQ REQ/REP 套接字对,一对从可执行文件到插件 shim 进程(呼叫转发),另一对在相反方向运行(回调)。每个进程都以 REQ 和 REP 套接字结束。
  2. 在主可执行文件中制定一条消息,发送到插件填充程序以调用提供的函数
  3. 通过主可执行文件的呼叫前转 REQ 套接字发送该消息,然后继续轮询呼叫前转 REQ 套接字和回调 REP 套接字。这允许主可执行文件接收返回值,或接收执行其自己的函数之一的请求
  4. shim 进程在其调用转发 REP 套接字上接收该数据,对其进行解码,并调用插件 DLL 中的相关函数
  5. 插件函数可能想要调用主可执行文件中的函数。您必须在 shim 进程中实现所有此类函数的存根版本。Thin shim 中的存根函数制定一条包含调用参数的消息,并通过其自己的回调 REQ 套接字将其发送回主可执行文件,然后阻塞在回调 REQ 套接字上的 zmq_recv() 上。
  6. 主可执行文件同时轮询其呼叫转发 REQ 和回调 REP 套接字,ZMQ 告知回调 REP 套接字上有一条消息。它读取此消息,执行指定的函数,收集结果。
  7. 结果通过回调REP套接字发送回shim进程。主可执行文件返回轮询两个套接字。
  8. shim 进程在 zmq_recv 上阻塞,接收结果消息,并将结果返回给 DLL 插件中的调用函数。
  9. 插件函数本身最终完成,并返回结果给shim进程
  10. 最终结果被打包成一条消息,通过垫片中的呼叫转发 REP 套接字发送回主可执行文件
  11. 这次主可执行文件被告知其呼叫转发 REQ 套接字上有一条消息已准备好 - 来自插件的回复。
  12. 该消息被读取,并且数据返回到想要调用插件的主可执行文件中的任何内容。

这允许的目的是

  • 调用插件中函数的主要可执行文件
  • 对于被调用的函数,可以多次调用主可执行文件提供的函数(从 0 次到多次)
  • 用于将插件函数结果返回到主可执行文件
  • 让插件 DLL 和主可执行文件存在于不同的进程中,并加载它们自己的首选依赖项。

ZMQ 将会很有用,因为听起来您在主可执行文件和插件之间没有直接的客户端/服务器或 RPC 关系;他们更加相互依赖。ZMQ 是 Actor 模型,它允许如上所述的这种模式。

ZeroMQ 的一个免费赠品是,掌握了这一点后,插件可以轻松地完全安装在另一台机器上,或者组合起来(即一些本地的、一些远程的、一些在世界另一端的 Linux 上运行等) )。

显然,如果存在共享数据结构,那么拥有两个独立的进程将无济于事,尽管我认为可以通过将它们放置在共享内存中来克服这个问题。但是所有插件 shim 进程都必须与主可执行文件位于同一台计算机上。

如果事情更加多线程,我认为上面概述的模式实际上不会有太大变化。您可能希望消息中的字段指示正在发生的情况。如果插件正在操作由主可执行文件创建的信号量、互斥体等,则可能会变得更加困难。