foo_wrapper中有3种不同类型的函数:
-
source_func_wrapper 是一个 python 函数,python 运行时处理这个函数的调用。
-
header_func 是一个在编译时使用的内联函数,所以后面不需要它的定义/机器码。
-
另一方面,
source_func 必须由静态(foo_wrapper 中的情况)或动态(我假设这是您对test_lib 的希望)链接器处理。
接下来,我将尝试解释为什么设置不能开箱即用,但首先我想介绍两个(至少在我看来)最佳替代方案:
答:完全避免这个问题。您的 foo_wrapper 包装了来自 foo.h 的 c 函数。这意味着所有其他模块应该使用这些包装函数。如果每个人都可以直接访问该功能 - 这会使整个包装器过时。在你的 `pyx-file: 中隐藏foo.h 接口:
#foo_wrapper.pdx
cdef source_func_wrapper()
cdef header_func_wrapper()
#foo_wrapper.pyx
cdef extern from "foo.h":
int source_func()
int header_func()
cdef source_func_wrapper():
return source_func()
cdef header_func_wrapper():
B:希望通过 c 函数直接使用 foo 功能可能是有效的。在这种情况下,我们应该使用与 stdc++-library 的 cython 相同的策略:foo.cpp 应该成为一个共享库,并且应该只有一个 foo.pdx-file(没有 pyx!)可以通过 cimport 导入任何需要的地方。此外,libfoo.so 应作为依赖项添加到 foo_wrapper 和 test_lib。
然而,接近 B 意味着更多的忙——你需要把 libfoo.so 放在动态加载器可以找到的地方......
其他选择:
正如我们将看到的,有很多方法可以让foo_wrapper+test_lib 工作。首先,让我们更详细地了解一下python中动态库的加载是如何工作的。
我们首先看看手头的test_lib.so:
>>> nm test_lib.so --undefined
....
U PyXXXXX
U source_func
有很多未定义的符号,其中大部分以Py 开头,将在运行时由python 可执行文件提供。但也有我们的恶人——source_func。
现在,我们通过
启动 python
LD_DEBUG=libs,files,symbols python
并通过import test_lib 加载我们的扩展程序。在触发的调试跟踪中,我们可以看到以下内容:
>>>>: file=./test_lib.so [0]; dynamically loaded by python [0]
python 通过dlopen 加载test_lib.so 并开始查找/解析来自test_lib.so 的未定义符号:
>>>>: symbol=PyExc_RuntimeError; lookup in file=python [0]
>>>>: symbol=PyExc_TypeError; lookup in file=python [0]
这些 python 符号很快就能找到——它们都在 python 可执行文件中定义——动态链接器首先查看的位置(如果这个可执行文件与-Wl,-export-dynamic 链接)。但是source_func就不一样了:
>>>>: symbol=source_func; lookup in file=python [0]
>>>>: symbol=source_func; lookup in file=/lib/x86_64-linux-gnu/libpthread.so.0 [0]
...
>>>>: symbol=source_func; lookup in file=/lib64/ld-linux-x86-64.so.2 [0]
>>>>: ./test_lib.so: error: symbol lookup error: undefined symbol: source_func (fatal)
所以在查找所有加载的共享库之后,找不到符号,我们必须中止。有趣的事实是,foo_wrapper 尚未加载,因此无法在此处查找 source_func(它将在下一步中作为 test_lib 的依赖项被 python 加载)。
如果我们用预加载的foo_wrapper.so 启动 python 会发生什么?
LD_DEBUG=libs,files,symbols LD_PRELOAD=$(pwd)/foo_wrapper.so python
这一次,调用import test_lib 成功,因为预加载的foo_wrapper 是动态加载器查找符号的第一个位置(在python 可执行文件之后):
>>>>: symbol=source_func; lookup in file=python [0]
>>>>: symbol=source_func; lookup in file=/home/ed/python_stuff/cython/two/foo_wrapper.so [0]
但是,当foo_wrapper.so 没有预加载时,它是如何工作的?首先让我们将foo_wrapper.so 作为库添加到我们的test_lib 设置中:
ext_modules = cythonize([
Extension("test_lib", ["test_lib.pyx"],
libraries=[':foo_wrapper.so'],
library_dirs=['.'],
)])
这将导致以下链接器命令:
gcc ... test_lib.o -L. -l:foo_wrapper.so -o test_lib.so
如果我们现在查看符号,那么我们看不出有什么区别:
>>> nm test_lib.so --undefined
....
U PyXXXXX
U source_func
source_func 仍未定义!那么链接共享库有什么好处呢?不同之处在于,现在 foo_wrapper.so 已根据需要在 test_lib.so 中列出:
>>>> readelf -d test_lib.so| grep NEEDED
0x0000000000000001 (NEEDED) Shared library: [foo_wrapper.so]
0x0000000000000001 (NEEDED) Shared library: [libpthread.so.0]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
ld 不链接,这是动态链接器的工作,但它会进行试运行并通过注意帮助动态链接器,需要foo_wrapper.so 才能解析符号,因此必须加载它在开始搜索符号之前。但是,它并没有明确说明符号 source_func 必须在 foo_wrapper.so 中查找 - 我们实际上可以在任何地方找到它并使用它。
让我们再次启动 python,这次没有预加载:
>>>> LD_DEBUG=libs,files,symbols python
>>>> import test_lib
....
>>>> file=./test_lib.so [0]; dynamically loaded by python [0]....
>>>> file=foo_wrapper.so [0]; needed by ./test_lib.so [0]
>>>> find library=foo_wrapper.so [0]; searching
>>>> search cache=/etc/ld.so.cache
.....
>>>> `foo_wrapper.so: cannot open shared object file: No such file or directory.
好的,现在动态链接器知道了,它必须找到foo_wrapper.so,但它不在路径中,所以我们收到一条错误消息。
我们必须告诉动态链接器在哪里寻找共享库。方法有很多,其中一种是设置LD_LIBRARY_PATH:
LD_DEBUG=libs,symbols,files LD_LIBRARY_PATH=. python
>>>> import test_lib
....
>>>> find library=foo_wrapper.so [0]; searching
>>>> search path=./tls/x86_64:./tls:./x86_64:. (LD_LIBRARY_PATH)
>>>> ...
>>>> trying file=./foo_wrapper.so
>>>> file=foo_wrapper.so [0]; generating link map
这次找到foo_wrapper.so(动态加载器查看LD_LIBRARY_PATH暗示的位置),加载然后用于解析test_lib.so中的未定义符号。
但是如果使用runtime_library_dirs-setup 参数有什么区别呢?
ext_modules = cythonize([
Extension("test_lib", ["test_lib.pyx"],
libraries=[':foo_wrapper.so'],
library_dirs=['.'],
runtime_library_dirs=['.']
)
])
现在调用
LD_DEBUG=libs,symbols,files python
>>>> import test_lib
....
>>>> file=foo_wrapper.so [0]; needed by ./test_lib.so [0]
>>>> find library=foo_wrapper.so [0]; searching
>>>> search path=./tls/x86_64:./tls:./x86_64:. (RPATH from file ./test_lib.so)
>>>> trying file=./foo_wrapper.so
>>>> file=foo_wrapper.so [0]; generating link map
foo_wrapper.so 可以在所谓的RPATH 上找到,即使不是通过LD_LIBRARY_PATH 设置的。我们可以看到这个RPATH 被静态链接器插入:
>>>> readelf -d test_lib.so | grep RPATH
0x000000000000000f (RPATH) Library rpath: [.]
然而,这是相对于当前工作目录的路径,这在大多数情况下并不是我们想要的。应该通过绝对路径或使用
ext_modules = cythonize([
Extension("test_lib", ["test_lib.pyx"],
libraries=[':foo_wrapper.so'],
library_dirs=['.'],
extra_link_args=["-Wl,-rpath=$ORIGIN/."] #rather than runtime_library_dirs
)
])
现在显示结果shared library. readelf 的相对于当前位置的路径(例如可以通过复制/移动来改变):
>>>> readelf -d test_lib.so | grep RPATH
0x000000000000000f (RPATH) Library rpath: [$ORIGIN/.]
这意味着将相对于加载的共享库的路径搜索所需的共享库,即test_lib.so。
如果您想重用 foo_wrapper.so 中的符号,这也是您的设置方式,我不提倡这样做。
不过,有一些可能性可以使用您已经构建的库。
让我们回到原来的设置。如果我们首先导入foo_wrapper(作为一种预加载)然后才导入test_lib,会发生什么?即:
>>>> import foo_wrapper
>>>>> import test_lib
这不是开箱即用的。但为什么?显然,来自foo_wrapper 的加载符号对其他库不可见。 Python 使用dlopen 动态加载共享库,正如this good article 中所解释的,有一些不同的策略可能。我们可以使用
>>>> import sys
>>>> sys.getdlopenflags()
>>>> 2
查看设置了哪些标志。 2 表示RTLD_NOW,表示符号在加载共享库时直接解析。我们需要与RTLD_GLOBAL=256 进行 OR 标记,以使符号在全局/动态加载的库之外可见。
>>> import sys; import ctypes;
>>> sys.setdlopenflags(sys.getdlopenflags()| ctypes.RTLD_GLOBAL)
>>> import foo_wrapper
>>> import test_lib
它工作正常,我们的调试跟踪显示:
>>> symbol=source_func; lookup in file=./foo_wrapper.so [0]
>>> file=./foo_wrapper.so [0]; needed by ./test_lib.so [0] (relocation dependency)
另一个有趣的细节:foo_wrapper.so 被加载一次,因为 python 不会通过 import foo_wrapper 加载模块两次。但是即使打开两次,在内存中也只会出现一次(第二次读取只会增加共享库的引用计数)。
但现在有了深刻的见解,我们甚至可以走得更远:
>>>> import sys;
>>>> sys.setdlopenflags(1|256)#RTLD_LAZY+RTLD_GLOBAL
>>>> import test_lib
>>>> test_lib.do_it()
>>>> ... it works! ....
为什么会这样? RTLD_LAZY 表示符号不是在加载时直接解析,而是在第一次使用时解析。但是在第一次使用(test_lib.do_it())之前,foo_wrapper 被加载(在test_lib 模块中导入)并且由于RTLD_GLOBAL,它的符号可以用于稍后解析。
如果我们不使用RTLD_GLOBAL,那么只有在我们调用test_lib.do_it() 时才会出现故障,因为在这种情况下全局看不到来自foo_wrapper 的所需符号。
对于这个问题,为什么将两个模块 foo_wrapper 和 test_lib 与 foo.cpp 链接起来并不是一个好主意:单例,请参阅 this。