使用 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.solibB.solibC.so),这样libA 包含主要的导出函数,其他库提供对libA 的支持.

现在,libA 依赖于 libBlibB 依赖于 libC。但是,有一个问题。有一个由libA 定义的全局数组,libC 期望存在。所以libC 实际上依赖于libA——一个循环依赖。尝试使用等效于dlopen 的cffi 或ctags 来加载libA 会导致libBlibC 中的符号丢失,但尝试首先加载libC 会导致有关丢失数组的错误(位于@ 987654342@).

由于它是一个变量,而不是一个函数,因此 RTLD_LAZY 选项似乎不适用于此处。

奇怪的是,ldd libA.so 没有将libBlibC 显示为依赖项,所以我不确定这是否是问题的一部分。我想这依赖于任何与这些库链接的程序来明确指定它们。

有没有办法解决这个问题?一个想法是创建一个依赖于libAlibBlibC 的新共享对象(例如“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_LAZYRTLD_NOW 取代,它不会工作 另外,如果 RTLD_GLOBAL 没有被指定,它也不会起作用。我没有检查是否可以指定其他 RTLD_ 标志来代替 RTLD_GLOBAL 以使事情仍然有效 创建处理所有库加载和初始化的包装库将是一件好事(解决方法),特别是如果您计划从多个地方使用它们(这样,整个过程将只在一个地方发生)。但是,之前的项目符号仍然适用 出于某种原因,ctypes 没有公开 RTLD_LAZY(事实上,还有许多其他相关标志)。在 code.py 中定义它是一种解决方法,在不同的 (Nix) 平台(风格)上,它的值可能会有所不同

【讨论】:

是的,对不起,我的意思不是 static 在这个意义上,我的意思是全球性的。我的错误。我将编辑我的问题。 在这个例子中(感谢你把它放在一起),ldd 正确地显示了依赖关系,所以 dlopening libA 将导致整个库组“一次”被引入。由于我不明白的原因,我实际处理的库(它们不是开源的并且在 NDA 下,所以我无法提供详细信息 - 但提供者很大,不会提供这种支持像我这样的人)似乎没有在ldd 中列出他们的依赖项。坦率地说,我不确定这怎么可能,但事实就是如此。所以我必须单独打开每一个,因此是 catch-22。 我一直在追求的一个想法是创建一个与所有其他库链接的虚拟库(在此示例中为libAlibBlibC),然后尝试dlopen ,希望这样做会将所有的依赖项作为一个组引入。不过我还没有完成这项工作,因为必须处理 20 到 30 个库。 我已经阅读了整个问题。请记住,在 Lnx 上,可以剥离 .so 的符号信息,(ldd 会显示蹲下,但 lib 将是加载并从中调用函数)。 最后一部分回答你的问题了吗?

以上是关于使用 ctypes/cffi 解决循环共享对象依赖关系的主要内容,如果未能解决你的问题,请参考以下文章

彻底理解Spring如何解决循环依赖

Spring解决循环依赖

Spring如何解决循环依赖?

分别用一二三级缓存解决循环依赖的方案

分别用一二三级缓存解决循环依赖的方案

Spring使用三级缓存解决循环依赖的过程