使用 ctypes/cffi 解决循环共享对象依赖关系
Posted
技术标签:
【中文标题】使用 ctypes/cffi 解决循环共享对象依赖关系【英文标题】:Resolving circular shared-object dependencies with ctypes/cffi 【发布时间】:2019-04-19 00:54:30 【问题描述】:我想使用cffi
(如果必须的话,甚至是ctypes
)在Linux 上从Python 3 访问C ABI。 API 由多个.so
文件实现(我们称它们为libA.so
、libB.so
和libC.so
),这样libA
包含主要的导出函数,其他库提供对libA
的支持.
现在,libA
依赖于 libB
和 libB
依赖于 libC
。但是,有一个问题。有一个由libA
定义的全局数组,libC
期望存在。所以libC
实际上依赖于libA
——一个循环依赖。尝试使用等效于dlopen
的cffi 或ctags 来加载libA
会导致libB
和libC
中的符号丢失,但尝试首先加载libC
会导致有关丢失数组的错误(位于@ 987654342@).
由于它是一个变量,而不是一个函数,因此 RTLD_LAZY 选项似乎不适用于此处。
奇怪的是,ldd libA.so
没有将libB
或libC
显示为依赖项,所以我不确定这是否是问题的一部分。我想这依赖于任何与这些库链接的程序来明确指定它们。
有没有办法解决这个问题?一个想法是创建一个依赖于libA
、libB
和libC
的新共享对象(例如“all.so”),以便dlopen("all.so")
可以一次性加载它需要的所有内容,但我可以也不能让它工作。
处理这种情况的最佳策略是什么?实际上,我尝试访问的 ABI 非常大,可能有 20-30 个共享对象文件。
【问题讨论】:
“静态数组”是如何声明的?希望没有static
关键字。
嗯,抱歉,我想我的意思是 global - 它是由 libC 声明的 extern
,但在 libA
中不是 static
。
当然它不是由 libA 声明的 static (因为 libC 不会“看到”它)。 *global" 是什么意思?
呃,不是静态的? :) 我的意思是它适用于任何与libA
链接的东西我猜。
【参考方案1】:
这(如果我理解正确的话)是 Nix 上的一个完全正常的用例,应该可以正常运行。
在处理与 ctypes ([Python 3]: ctypes - A foreign function library for Python) 相关的问题时,解决这些问题的最佳(通用)方法是:
编写一个(小)C 应用程序来完成所需的工作(当然,工作) 然后才移动到ctypes(基本上这是翻译上面的应用程序)我准备了一个小(和虚拟)示例:
defines.h:
#pragma once
#include <stdio.h>
#define PRINT_MSG_0() printf("From C: [%s] (%d) - [%s]\n", __FILE__, __LINE__, __FUNCTION__)
libC:
libC.h:
#pragma once
size_t funcC();
libC.c:
#include "defines.h"
#include "libC.h"
#include "libA.h"
size_t funcC()
PRINT_MSG_0();
for (size_t i = 0; i < ARRAY_DIM; i++)
printf("%zu - %c\n", i, charArray[i]);
printf("\n");
return ARRAY_DIM;
libB:
libB.h:
#pragma once
size_t funcB();
libB.c:
#include "defines.h"
#include "libB.h"
#include "libC.h"
size_t funcB()
PRINT_MSG_0();
return funcC();
libA:
libA.h:
#pragma once
#define ARRAY_DIM 3
extern char charArray[ARRAY_DIM];
size_t funcA();
libA.c:
#include "defines.h"
#include "libA.h"
#include "libB.h"
char charArray[ARRAY_DIM] = 'A', 'B', 'C';
size_t funcA()
PRINT_MSG_0();
return funcB();
code.py:
#!/usr/bin/env python3
import sys
from ctypes import CDLL, \
c_size_t
DLL = "./libA.so"
def main():
lib_a = CDLL(DLL)
func_a = lib_a.funcA
func_a.restype = c_size_t
ret = func_a()
print(":s returned :d".format(func_a.__name__, ret))
if __name__ == "__main__":
print("Python :s on :s\n".format(sys.version, sys.platform))
main()
输出:
[cfati@cfati-ubtu16x64-0:~/Work/Dev/***/q053327620]> ls code.py defines.h libA.c libA.h libB.c libB.h libC.c libC.h [cfati@cfati-ubtu16x64-0:~/Work/Dev/***/q053327620]> gcc -fPIC -shared -o libC.so libC.c [cfati@cfati-ubtu16x64-0:~/Work/Dev/***/q053327620]> gcc -fPIC -shared -o libB.so libB.c -L. -lC [cfati@cfati-ubtu16x64-0:~/Work/Dev/***/q053327620]> gcc -fPIC -shared -o libA.so libA.c -L. -lB [cfati@cfati-ubtu16x64-0:~/Work/Dev/***/q053327620]> ls code.py defines.h libA.c libA.h libA.so libB.c libB.h libB.so libC.c libC.h libC.so [cfati@cfati-ubtu16x64-0:~/Work/Dev/***/q053327620]> LD_LIBRARY_PATH=. ldd libC.so linux-vdso.so.1 => (0x00007ffdfb1f4000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f56dcf23000) /lib64/ld-linux-x86-64.so.2 (0x00007f56dd4ef000) [cfati@cfati-ubtu16x64-0:~/Work/Dev/***/q053327620]> LD_LIBRARY_PATH=. ldd libB.so linux-vdso.so.1 => (0x00007ffc2e7fd000) libC.so => ./libC.so (0x00007fdc90a9a000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fdc906d0000) /lib64/ld-linux-x86-64.so.2 (0x00007fdc90e9e000) [cfati@cfati-ubtu16x64-0:~/Work/Dev/***/q053327620]> LD_LIBRARY_PATH=. ldd libA.so linux-vdso.so.1 => (0x00007ffd20d53000) libB.so => ./libB.so (0x00007fdbee95a000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fdbee590000) libC.so => ./libC.so (0x00007fdbee38e000) /lib64/ld-linux-x86-64.so.2 (0x00007fdbeed5e000) [cfati@cfati-ubtu16x64-0:~/Work/Dev/***/q053327620]> nm -S libC.so | grep charArray U charArray [cfati@cfati-ubtu16x64-0:~/Work/Dev/***/q053327620]> nm -S libA.so | grep charArray 0000000000201030 0000000000000003 D charArray [cfati@cfati-ubtu16x64-0:~/Work/Dev/***/q053327620]> LD_LIBRARY_PATH=. python3 code.py Python 3.5.2 (default, Nov 12 2018, 13:43:14) [GCC 5.4.0 20160609] on linux From C: [libA.c] (9) - [funcA] From C: [libB.c] (7) - [funcB] From C: [libC.c] (7) - [funcC] 0 - A 1 - B 2 - C funcA returned 3
但是,如果您的数组被声明为 static ([CPPReference]: C keywords: static)(因此,它不能像示例中那样为 extern),那么你有点受宠若惊。
@EDIT0:扩展示例,使其更符合描述。
由于 ldd 没有显示 .so 之间的依赖关系,我将假设每个都是动态加载的。
utils.h:
#pragma once
#include <dlfcn.h>
void *loadLib(char id);
utils.c:
#include "defines.h"
#include "utils.h"
void *loadLib(char id)
PRINT_MSG_0();
char libNameFormat[] = "lib%c.so";
char libName[8];
sprintf(libName, libNameFormat, id);
int load_flags = RTLD_LAZY | RTLD_GLOBAL; // !!! @TODO - @CristiFati: Note RTLD_LAZY: if RTLD_NOW would be here instead, there would be nothing left to do. Same thing if RTLD_GLOBAL wouldn't be specified. !!!
void *ret = dlopen(libName, load_flags);
if (ret == NULL)
char *err = dlerror();
printf("Error loading lib (%s): %s\n", libName, (err != NULL) ? err : "(null)");
return ret;
以下是 libB.c 的修改版本。请注意,同样的模式也应该应用于原来的libA.c。
libB.c:
#include "defines.h"
#include "libB.h"
#include "libC.h"
#include "utils.h"
size_t funcB()
PRINT_MSG_0();
void *mod = loadLib('C');
size_t ret = funcC();
dlclose(mod);
return ret;
输出:
[cfati@cfati-ubtu16x64-0:~/Work/Dev/***/q053327620]> ls code.py defines.h libA.c libA.h libB.c libB.h libC.c libC.h utils.c utils.h [cfati@cfati-ubtu16x64-0:~/Work/Dev/***/q053327620]> gcc -fPIC -shared -o libC.so libC.c utils.c [cfati@cfati-ubtu16x64-0:~/Work/Dev/***/q053327620]> gcc -fPIC -shared -o libB.so libB.c utils.c [cfati@cfati-ubtu16x64-0:~/Work/Dev/***/q053327620]> gcc -fPIC -shared -o libA.so libA.c utils.c [cfati@cfati-ubtu16x64-0:~/Work/Dev/***/q053327620]> ls code.py defines.h libA.c libA.h libA.so libB.c libB.h libB.so libC.c libC.h libC.so utils.c utils.h [cfati@cfati-ubtu16x64-0:~/Work/Dev/***/q053327620]> ldd libA.so linux-vdso.so.1 => (0x00007ffe5748c000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4d9e3f6000) /lib64/ld-linux-x86-64.so.2 (0x00007f4d9e9c2000) [cfati@cfati-ubtu16x64-0:~/Work/Dev/***/q053327620]> ldd libB.so linux-vdso.so.1 => (0x00007ffe22fe3000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fe93ce8a000) /lib64/ld-linux-x86-64.so.2 (0x00007fe93d456000) [cfati@cfati-ubtu16x64-0:~/Work/Dev/***/q053327620]> ldd libC.so linux-vdso.so.1 => (0x00007fffe85c3000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f2d47453000) /lib64/ld-linux-x86-64.so.2 (0x00007f2d47a1f000) [cfati@cfati-ubtu16x64-0:~/Work/Dev/***/q053327620]> nm -S libC.so | grep charArray U charArray [cfati@cfati-ubtu16x64-0:~/Work/Dev/***/q053327620]> nm -S libA.so | grep charArray 0000000000201060 0000000000000003 D charArray [cfati@cfati-ubtu16x64-0:~/Work/Dev/***/q053327620]> LD_LIBRARY_PATH=. python3 code.py Python 3.5.2 (default, Nov 12 2018, 13:43:14) [GCC 5.4.0 20160609] on linux Traceback (most recent call last): File "code.py", line 22, in <module> main() File "code.py", line 12, in main lib_a = CDLL(DLL) File "/usr/lib/python3.5/ctypes/__init__.py", line 347, in __init__ self._handle = _dlopen(self._name, mode) OSError: ./libA.so: undefined symbol: funcB
我相信这会重现问题。现在,如果您修改(的 1st 部分)code.py 为:
#!/usr/bin/env python3
import sys
from ctypes import CDLL, \
RTLD_GLOBAL, \
c_size_t
RTLD_LAZY = 0x0001
DLL = "./libA.so"
def main():
lib_a = CDLL(DLL, RTLD_LAZY | RTLD_GLOBAL)
func_a = lib_a.funcA
func_a.restype = c_size_t
ret = func_a()
print(":s returned :d".format(func_a.__name__, ret))
if __name__ == "__main__":
print("Python :s on :s\n".format(sys.version, sys.platform))
main()
你会得到以下输出:
[cfati@cfati-ubtu16x64-0:~/Work/Dev/***/q053327620]> LD_LIBRARY_PATH=. python3 code.py Python 3.5.2 (default, Nov 12 2018, 13:43:14) [GCC 5.4.0 20160609] on linux From C: [libA.c] (11) - [funcA] From C: [utils.c] (6) - [loadLib] From C: [libB.c] (8) - [funcB] From C: [utils.c] (6) - [loadLib] From C: [libC.c] (7) - [funcC] 0 - A 1 - B 2 - C funcA returned 3
注意事项:
在 C 中有RTLD_LAZY | RTLD_GLOBAL
非常重要。如果 RTLD_LAZY 被 RTLD_NOW 取代,它不会工作
另外,如果 RTLD_GLOBAL 没有被指定,它也不会起作用。我没有检查是否可以指定其他 RTLD_ 标志来代替 RTLD_GLOBAL 以使事情仍然有效
创建处理所有库加载和初始化的包装库将是一件好事(解决方法),特别是如果您计划从多个地方使用它们(这样,整个过程将只在一个地方发生)。但是,之前的项目符号仍然适用
出于某种原因,ctypes 没有公开 RTLD_LAZY(事实上,还有许多其他相关标志)。在 code.py 中定义它是一种解决方法,在不同的 (Nix) 平台(风格)上,它的值可能会有所不同
【讨论】:
是的,对不起,我的意思不是static
在这个意义上,我的意思是全球性的。我的错误。我将编辑我的问题。
在这个例子中(感谢你把它放在一起),ldd
正确地显示了依赖关系,所以 dlopening libA
将导致整个库组“一次”被引入。由于我不明白的原因,我实际处理的库(它们不是开源的并且在 NDA 下,所以我无法提供详细信息 - 但提供者很大,不会提供这种支持像我这样的人)似乎没有在ldd
中列出他们的依赖项。坦率地说,我不确定这怎么可能,但事实就是如此。所以我必须单独打开每一个,因此是 catch-22。
我一直在追求的一个想法是创建一个与所有其他库链接的虚拟库(在此示例中为libA
、libB
、libC
),然后尝试dlopen ,希望这样做会将所有的依赖项作为一个组引入。不过我还没有完成这项工作,因为必须处理 20 到 30 个库。
我已经阅读了整个问题。请记住,在 Lnx 上,可以剥离 .so 的符号信息,(ldd 会显示蹲下,但 lib 将是加载并从中调用函数)。
最后一部分回答你的问题了吗?以上是关于使用 ctypes/cffi 解决循环共享对象依赖关系的主要内容,如果未能解决你的问题,请参考以下文章