V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
BruceAuyeung
V2EX  ›  程序员

c++动态调用链接库的问题

  •  
  •   BruceAuyeung · 2019-09-27 18:09:33 +08:00 via Android · 3721 次点击
    这是一个创建于 1887 天前的主题,其中的信息可能已经有所发展或是发生改变。
    c++完全门外汉向大家问个问题,假如 c++二进制里面没有包含某个符号(对应一个 so 里面的函数),这个二进制能调到外部 so 里面的这个函数吗?我设想的场景:
    我要开发一个 c++程序,会调用外部 so/dll 文件(出于描述方便考虑,后文就只提 so,so 可能是 c 开发的,也可能是 c++开发的),但我不知道会有哪些 so,so 里面有哪些函数以及函数入参出参,所以就设计一个 xml 文件,用于描述有哪些 so,各自路径,各自的方法列表和参数说明,我 c++二进制运行的时候加载这个 xml 完成解析并完成 so 方法的注册,这样 c++程序就能调用这些方法啦
    请问,这可行吗?
    第 1 条附言  ·  2019-09-27 20:06:41 +08:00
    https://github.com/node-ffi-napi/node-ffi-napi
    下面是一段 JS 代码,ffi-napi 是用 C++写的 nodejs addon。你看 ffi-napi 编译的时候是不知道要调用 ceil 方法的,ffi-napi 是通过 ffi.Library 方法才知道有个 libm 外部动态库,里面有 ceil 方法。这样做的好处是当外部 libm 新增导出方法时,ffi-napi 无需重新编译,只需要修改前段 JS 代码就可以直接使用 ceil 方法了。
    ~~~
    var ffi = require('ffi-napi');

    var libm = ffi.Library('libm', {
    'ceil': [ 'double', [ 'double' ] ]
    });
    libm.ceil(1.5); // 2

    // You can also access just functions in the current process by passing a null
    var current = ffi.Library(null, {
    'atoi': [ 'int', [ 'string' ] ]
    });
    current.atoi('1234'); // 1234
    ~~~
    由于确实是完全门外汉,所以暂时还不理解 node-ffi-napi 是怎么实现的。
    27 条回复    2019-09-28 13:59:40 +08:00
    wbing
        1
    wbing  
       2019-09-27 18:18:30 +08:00 via iPhone
    “但我不知道会有哪些 so,so 里面有哪些函数以及函数入参出参,所以就设计一个 xml 文件,用于描述有哪些 so,各自路径,各自的方法列表和参数说明”
    —————-
    感觉这有点前后矛盾啊,你都不知道会有哪些 so,那这个 xml 文件是怎么生成的。
    wevsty
        2
    wevsty  
       2019-09-27 18:26:10 +08:00
    没有导出的函数是不能从外部调用的。

    编译 dll/so 的时候必须导出外部需要的函数,但是你可以使用自己的方法来描述导出的函数。比如你说的通过加载 xml 来确定导出了什么函数,然后动态加载 dll/so 再来调用是可以的。
    ysc3839
        3
    ysc3839  
       2019-09-27 18:26:47 +08:00 via Android
    怀疑这是个 X-Y Problem https://coolshell.cn/articles/10804.html
    建议你直接说你要实现什么。
    yrand
        4
    yrand  
       2019-09-27 18:52:21 +08:00
    我猜是想实现插件之类的功能。必须要定好接口才行
    Foreverdxa
        5
    Foreverdxa  
       2019-09-27 19:20:00 +08:00
    lib
    dll
    头文件 这些不好使???
    BruceAuyeung
        6
    BruceAuyeung  
    OP
       2019-09-27 19:32:47 +08:00 via Android
    @wbing 是指编译我的 c++程序时,我是不知道有哪些 so 的
    BruceAuyeung
        7
    BruceAuyeung  
    OP
       2019-09-27 19:33:15 +08:00 via Android
    @wevsty 外部 so 我们假定是已经导出了的
    BruceAuyeung
        8
    BruceAuyeung  
    OP
       2019-09-27 19:35:59 +08:00 via Android
    @ysc3839 就是我希望我的 c++编译完之后,能调用任意外部 so 里面的任意方法
    BruceAuyeung
        9
    BruceAuyeung  
    OP
       2019-09-27 19:40:09 +08:00 via Android
    @Foreverdxa 这个在编译的时候已经知道会调哪些 so 的哪些方法了吧
    across
        10
    across  
       2019-09-27 19:41:44 +08:00
    不是脑筋急转弯吧,没想到什么方法,如果能解,大概就是加入个 Lua 脚本之类的进行粘合?
    pursuer
        11
    pursuer  
       2019-09-27 19:43:09 +08:00 via Android
    dlopen RTLD_GLOBAL 可以让加载的 so 的导出对后续加载的 so 可见,但这和 xml 啥的没什么关系,不过这个方式可能动态库卸载不了吧,我也不确定。你说的注册机制倒有点像 windows com 的设计。
    BruceAuyeung
        12
    BruceAuyeung  
    OP
       2019-09-27 19:52:42 +08:00
    https://github.com/node-ffi-napi/node-ffi-napi
    下面是一段 JS 代码,ffi-napi 是用 C++写的 nodejs addon。你看 ffi-napi 编译的时候是不知道要调用 ceil 方法的,ffi-napi 是通过 ffi.Library 方法才知道有个 libm 外部动态库,里面有 ceil 方法。这样做的好处是当外部 libm 新增导出方法时,ffi-napi 无需重新编译,只需要修改前段 JS 代码就可以直接使用 ceil 方法了。
    ~~~
    var ffi = require('ffi-napi');

    var libm = ffi.Library('libm', {
    'ceil': [ 'double', [ 'double' ] ]
    });
    libm.ceil(1.5); // 2

    // You can also access just functions in the current process by passing a null
    var current = ffi.Library(null, {
    'atoi': [ 'int', [ 'string' ] ]
    });
    current.atoi('1234'); // 1234
    ~~~
    HHehr0ow
        13
    HHehr0ow  
       2019-09-27 20:41:51 +08:00   ❤️ 1
    是可行的。
    以 Windows 下 DLL 为例,使用 LoadLibrary 加载 module 之后获得 handle,再使用 GetProcAddress 获得目标函数指针 pFooTarget。
    之后神奇的部分就发生了,假定 FooTarget 使用了 x86 cdecl calling convention,可以根据 xml 中描述的变量信息,在调用 pFooTarget 之前自行按照 cdecl 的规则进行参数压栈,最后一句汇编 CALL,即可完成函数的调用。
    同样,调用完毕后,需要自行到寄存器或者栈上取回返回值,比如 eax。
    missdeer
        14
    missdeer  
       2019-09-27 20:51:49 +08:00
    13 楼正解。
    Windows 上有个叫 rundll32.exe 的程序就是这个功能。
    iceheart
        15
    iceheart  
       2019-09-27 20:57:57 +08:00 via Android   ❤️ 1
    可以实现,但是复杂程度不是你能接受的。
    举个例子: 一个 so 库的某个导出函数,需要一个复杂的结构体指针作为参数。
    你要构造一个什么样的 xml 来描述这个参数的全部信息?
    再假设你做到了,你如何构建数据的存储结构呢?
    你不是为这一个特殊结构构建实现,而是为所有可能出现的描述做解析做处理。你觉得最初要的灵活性,还能实现么?
    chingyat
        16
    chingyat  
       2019-09-27 21:07:10 +08:00   ❤️ 1
    1. dlopen 打开动态库
    2. dlsym 找到符号
    3. cast 为相应的函数指针
    4. 调用

    这样不行吗?
    zealot0630
        17
    zealot0630  
       2019-09-27 21:18:52 +08:00 via Android
    可以,搜索 dlopen/dlsym
    402124773
        18
    402124773  
       2019-09-27 21:34:53 +08:00
    搜索 windows 下 com 机制调用,好像有类似的情况。
    BruceAuyeung
        19
    BruceAuyeung  
    OP
       2019-09-27 21:38:35 +08:00
    @chingyat 根据你们的提示,我搜索了下
    https://github.com/node-ffi-napi/node-ffi-napi/search?q=dlopen&unscoped_q=dlopen

    好像 node-ffi-napi 就是这么实现的
    BruceAuyeung
        20
    BruceAuyeung  
    OP
       2019-09-27 21:44:07 +08:00
    @iceheart 谢谢。参数类型可以做出约束,不需要过于复杂。
    x1314aq
        21
    x1314aq  
       2019-09-27 21:51:02 +08:00
    完全可以,dlopen()系列函数就是为这个而生的
    edimetia3d
        22
    edimetia3d  
       2019-09-28 01:42:25 +08:00   ❤️ 1
    首先, @ysc3839 说的很对, 楼主应该从根本上描述下自己的需求, 而不是自己想当然的来一个解决方案,一条路走到黑.

    然后, LZ 其实就是想做一个 C/C++的简单 addon 系统.
    如果做得太玩具, 那可能不如重新编译. 如果做得太全面, 考虑到 LZ 描述的水平, 可能吃不下这个饼,C/C++毕竟是"静态"的.
    具体而言:
    @chingyat 的路线正确, 但是有点像把大象装进冰箱里. 第三步第四步其实是很麻烦的, 毕竟你在编译期并不知道要 cast 成什么样的函数, 最起码需要考虑参数数量的问题. 这里, 要么按 @HHehr0ow 所说的, 按照 C 的 ABI 直接压栈进行调用. 要么楼主可能就要按照写一个很大的`switch(arg_number)`,每个 case 都先 cast 一下,再触发函数调用, 且不可避免的所有形参都必须是某种`Variant`类型

    @iceheart 提到了参数类型的问题, 其实 xml 是不需要描述函数签名的,只要有函数名就够了, dlsym 拿到的只有一个指针,类型信息对函数调用是没有意义的. 重点是序列化 /反序列化的问题,大概就是 protobuf 这样的库做的. 也就是你在 js 中传了一堆东西给某个进程, 这个进程需要把这堆东西变成一串对象`arg1,arg2....argN`作为函数实参,然后进行函数调用. 如果涉及了复杂类型, 那么"在 C++中实现反射"等着你.

    当然,这里面还有很多比较细节的问题, 比如调用的约定, IPC 传参的实现,等等等等,LZ 不要再考虑一下开头第一句吗?

    发呆比较闲 ,闲扯一下, 所以没有后续 XD
    ysc3839
        23
    ysc3839  
       2019-09-28 01:54:27 +08:00 via Android
    @edimetia3d 看了楼主的回复,说不准真的只是想了解一下底层原理。如果是这样的话楼上的人已经说得差不多了。
    iceheart
        24
    iceheart  
       2019-09-28 07:27:39 +08:00 via Android
    @BruceAuyeung #20
    也就是说 so 库的接口函数声明格式是可控的?
    那又何必搞这么复杂,定义一个动态类型参数就行啊,类似 json 的。
    实在不会弄就传入个 json,返回个 json,把 so 库当个外部调用的 server 就行了啊。
    lspvic
        25
    lspvic  
       2019-09-28 08:04:43 +08:00 via Android
    一般提供 dll/so 库的人也会提供头文件的,里面导出函数的签名定义的清清楚楚,直接 include 进去就可以了。ffi 是封装了 dll 的调用,但是还是需要知道函数的签名才能使用。一个 dll/so 库没有头文件或者函数签名文档是没法用的,或者说仅仅从 dll/so 文件是没法看出函数的参数及返回值的。
    顺便刚刚用过了 node-ffi,不支持 node12,还得自己写个 native addon 用 LoadLibrary 来调用 dll,只能在 windows 下编译,不能用 ci 了。
    Shazoo
        26
    Shazoo  
       2019-09-28 10:18:26 +08:00   ❤️ 1
    1. 解析 dll/so 文件本体,获取 dll/so 的 export 函数表不难,从 readelf 库里面能找到,之前为了调试,用过。
    2. 但是,仅只是获取的函数表,重要的函数参数是无法获取的。
    3. 你说的 xml 之类的管理方式自然可行。但是,一般来说,都是通过引入 so/dll 的头文件参与编译来实现调用。
    4. 如果是为了动态的调用新的 so/dll (不想重新编译),你这方案貌似没问题。不过很难想象应用场景。一般不是写个 bridge library 然后不停更新这个比较好吗……
    secondwtq
        27
    secondwtq  
       2019-09-28 13:59:40 +08:00   ❤️ 5
    首先楼主这个问题和 C++ 没关系,其实应该算是 Linker/Loader/ABI 的范畴,传统上算是 C (”学会 C 语言“在现在的语境下已经包含了太多的东西了)。

    可以把原问题大致分成两个部分:
    第一是定位并载入动态库以及库中的符号,这一步完成之后会给你一个函数代码的指针。而楼主是要运行时动态加载一个库,这个也是没有问题的,OS 提供了相关的 API,很多软件都有类似插件系统一样的东西,最常见的就是各大主流操作系统都支持的驱动和内核扩展——实在不行你自己定义一个格式自己载入也行 ... 做 JIT 都会涉及类似的操作,至于直接载入 native code 的也有,我猜 Chrome 之前搞得什么 NaCl 就是。
    只不过现在都去写 JavaScript,扩展需求一般直接写程序解决不编译成 binary (这实际上是更好的方式),才会有楼主的问题——但是讽刺的是,node 里载入 native 库本来就会走同样的一个流程,并且 node 生态是很依赖 native 库的。

    (另外用 binary 做扩展还是写代码做扩展,其实更多取决于需求,Solaris DTrace 和 Linux eBPF 这些做 profiling 的工具虽然是内核级的,但都是用户写程序运行时编译然后塞进内核里面跑,甚至可以直接 JIT 成 native code,像 bcc 一类的工具实际上把 eBPF 做成了 kernel 的 WebAssembly,这对于剖析程序行为过程中不断的调试是很有帮助的。做 graphics 的话,调试过程中 shader 都是一个文本塞进去 JIT 的,而且 GPU 并没有统一的 ISA (不仅仅是不同 vendor 之间的区别,同一 vendor 的不同代产品也不一样),不好直接跑 native code。而使用 native code 做扩展,好处是不限语言,性能上限一般更好(虽然会失去做 IPO/PGO 的机会),并且更方便扩展本身代码的保护。
    至于 JavaScript 为啥不兴搞 native 扩展,纯属是因为确实需要做 native 扩展的需求太少,就算有点需求,对于大多数人来说做 native 扩展的成本又太高)

    第二步是(在上一步已经得到函数指针的情况下)调用对应的函数。这个就涉及到 Data Layout 和 Calling Convention 之类的问题。

    需要注意这两步之间的区别,尤其在外部库和程序本身使用同一种语言的情况下,”动态加载动态库“和”动态获取并调用其中的*任意*函数“是两个过程,在讨论实现细节时把它们混在一起说是不合适的。一般使用 C/C++ 作为 host 来实现的插件系统,都是由 host 来寻找并载入一个动态库,之后会固定地调用其中的某个 init 函数(函数名称、签名都是固定的),init 函数再把各种扩展的东西注册到 host 中。这个过程中所有的接口都是 host 早就决定好的,因此不涉及”动态获取并调用其中的*任意*函数“的过程。

    为了更好的理解第一步和第二步之间的区别,首先应该理解“一个编程语言调用自己写的库”和“一个编程语言调用 C 写的库”之间的区别。
    很多编程语言都有自己钦定的调用(同样是该编程语言写的)外部库的方式——所有那些乱七八糟的模块,包,import 都属于这个范畴。但是它们的共性是都是调用自己写的东西,你在 Java 里面 import 一个包进来,那个包是 JVM Bytecode 格式,你在 Python 里面 import 一个模块,那模块也是 Python 写的。因为是同一个编程语言,同一个编译器实现,所以你能直接用语言原生的方式使用外部库,非常自然,一般这都是最方便的方式。
    但是这是静态调用,如果要动态调用外部库,一般需要其他方式。典型的在 Java 里,动态载入 class 需要用反射。(对于解释型语言来说,静态调用和动态调用是有可能统一的,比如 JavaScript 的 require 就是万能的,既可以静态调用也可以动态调用)。不变的是依然需要所调用的模块是使用该语言编写的——这个说法不严格,应该说是符合该语言自身体系标准的,比如在 node 里可以用 require 来调用 native addon,但是这要求 addon 文件自身实现了 addon 的接口,你不能直接编译一个 OpenSSL,然后就在 node 里把它的 .so require 进来,这个需要使用下一种方式。

    这里就可以看出来,无论是静态调用和动态调用,你都会使用一种机制来找到对应的库,在库载入进来之后,使用库中函数的过程,就和使用你自己用该语言写的函数是一样的。“找库”和“调函数”是两个正交的东西。

    下一种方式,就是在某个编程语言里面调用 C 外部库。
    一个编程语言可以不实现任何的模块等调用外部库的方式,却依然是图灵完全的。但是这样的编程语言字啊实际场景中最多只能做到 Hello World,难以回答“JavaScript 也可以写服务器么?”这种问题。所以编程语言要做模块,做包,也即上面所说的“调用同样是该编程语言写的外部库”的机制,并且提供一堆的库,现在你提供了库让 JavaScript 可以写服务器了,但是缺乏常识的萌新们又问出一堆问题:JavaScript 可以写桌面程序么?可以做 IoT 么?可以做韩国女团的人脸识别么?
    解决这些问题的终极方案,就是实现一个调用 C 外部库的机制。因为如果一个编程语言能调用 C 库,实际上就相当于 C 有的库它都能有。这样 JavaScript 就不仅能做韩国女团的人脸识别,甚至还能控制火箭发射(以及火箭爆炸),你就直接告诉萌新们“C 能做的我都能做”就可以了。所以任何一个像样的语言,都会有一个调用 C 库的机制。
    这个“调用 C 库的机制”也分不同的方式,常见的一种是:用 C 写 binding,和你要调用的库链接到一起,这一般需要依赖于语言实现的具体 API (在 Java 里倒是有标准化的 JNI,但是我觉得不是所有人都把 JNI 当作是自己认知中“Java 语言”这一概念神圣不可分割的一部分)。Python 和 Lua 的 C Binding、Node 的 C++ Binding 都是一个道理。
    还有一种是使用语言本身来描述 C 接口,开发者不需要写任何 C 代码。这个可以继续细分,不过区别更加微妙——一些编译型语言可以在编译时静态生成对 C 函数的调用,比如 Go 的 cgo 和 Haskell 的 FFI。编译型语言也可以在运行时进行完全动态的调用,比如 OCaml 的 ctypes。而解释型语言由于没有这个区分,所以这个做得都差不多,典型如 Python 的 cffi,LuaJIT 的 FFI。
    (从命名可见,狭义上的 FFI 仅仅指上面最后一段的东西 ...)

    在这种场景下,你依然可以选择是“调用其中固定的函数”还是“动态调用任意函数”,对于编译型语言,你还可以选择“编译时链接固定的库”还是“运行时动态载入库”。

    这里有两个常见的特例:
    一个是 C,对于 C 来说,不存在“调用自己写的库”和“调用 C 写的库”的区别(因为”自己 = C“ ...)。这样所有其他编程语言费了半天功夫实现的编译器接口也好 FFI 接口也好,在 C 里都是 free 的,因为所有 C 库都天然地符合 C 自身的体系,根本不”foreign“。
    但是标准 C 仅仅解决了”静态调用外部库“的问题,并没有解决”动态调用“( C 标准根本就和动态库都没关系)。这个就需要用操作系统的动态库 API,结合 libffi。由于 C 的表达能力实在太过捉鸡,所以这个写起来就像在写另外一种语言,尽管调的还是 C 函数。
    在这个过程中,操作系统 API 解决的是”动态加载动态库“的问题,libffi 解决的是“动态调用函数的问题”。实际上没人拦着在 c 文件里面写一个函数,然后在同一个 c 文件里面用 libffi 调用它。

    另一个是 C++,首先 C++ 与 C 保持了很大的兼容性,也能直接调用 C 库(嘛虽然有个 extern "C"),这样上面说 C 的话,对 C++ 也都成立。C++ 标准也没解决动态调用的问题,所以动态调用 C 库也需要 libffi,但是问题在于动态载入 C++ 库(这个没问题),*并且*动态调用其中的函数 ... 因为
    我至今没见过有人做过那么疯狂的事情!
    虽然说有无数的语言都不断地打磨自己的 C FFI,但是很少有做“C++ FFI”并且实质上成功的。尽管 C++ 的生态也很丰富,支持调用 C++ 库中的内容(哪怕是个 binding )的好处也很大。
    这不仅仅是 C++ 复杂性的问题,很大程度上也是因为 C 实在太过 primitive,几乎相当于所有语言的下限,而 C++ 接口中涉及的概念对于很多编程语言来说就是根本不兼容的(很多语言没有 class ),所以做“C++ FFI”这个事情,很多时候 by definition 就是有问题的(就不说 template 根本没法做)。
    所以很多 C++ 库都是库本身用 C++ 写,然后用 C wrap 一下接口,这样所有能调用 C 的语言,也都能调用这个库。
    这就是为什么说楼主的问题和 C++ 根本就没有关系。
    话又说回来,这里”动态加载 C++ 动态库“其实是没有问题的,问题出在“调用 C++ 动态库中的函数”这一步。

    当然上面这些都是上个时代的东西,现在 114 天就可以产生 514 个新的 RPC 框架,而在 polyglot 的世界里,很多 RPC 框架是跨语言的。跨语言的 RPC 框架一般试图提取各种语言数据模型的共性(即:各种语言的下限,或最大公约数),并且使用一个语言无关的 IDL 来描述。比如 COM/DCOM ( literally 也是上个时代的东西 ...)就是使用一个微软扩展过的 IDL,gRPC 使用 Protocol Buffer 等。
    IDL 不仅仅 RPC 会用,它可以作为一个抽象层存在于任何跨语言的场景中(只不过现在萌新们只知道 RPC ...)。比如 DOM API 就是语言无关的,DOM API 可以用一种叫 WebIDL 的东西来描述。而浏览器引擎一般都会带这样一份 IDL 定义,然后会实现一个代码生成器,在编译时根据 IDL 生成对应 JavaScript 引擎的 binding,将引擎中的 C++ 接口暴露给 Web 开发者。这个原理和写 node C++ addon 其实是一样的,不过流程要成熟许多,更方便维护。
    所以楼主说用 XML 也好 JSON 也罢来描述接口,多少也有点重新造轮子的意思。

    @Shazoo 你 assume 了所有代码都是自己控制的,忽略了第三方扩展的需求。并且写 bridge library 并不一定比 FFI 更方便。
    很多时候 bridge 需要依赖于编译器的 API,这是编译器的一个实现细节,原则上用户不需要关心。
    更别说某些语言的 FFI 只需要一个 C header 就可以帮你把很多事情全做掉,方便的 FFI 恰恰是语言和生态强大的体现,必须写 bridge library,反倒是个问题。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5815 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 41ms · UTC 03:14 · PVG 11:14 · LAX 19:14 · JFK 22:14
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.