foo_wrapper
中有三种不同类型的函数:
source_func_wrapper
是一个Python函数,Python运行时处理调用该函数。
header_func
是一个内联函数,在编译时使用,因此后续不需要其定义/机器代码。
source_func
则必须由静态链接器(这是foo_wrapper
中的情况)或动态链接器(我假设这是您对test_lib
的要求)处理。
接下来,我将尝试解释为什么设置不能直接使用,但首先我想介绍两个(至少在我看来)最好的替代方案:
A:彻底避免这个问题。你的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功能,这可能是有效的。 在这种情况下,我们应该像Cython与stdc++库一样使用相同的策略:将foo.cpp变成共享库,并且只有一个foo.pdx文件(没有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
。
现在,我们通过
LD_DEBUG=libs,files,symbols python
通过 import test_lib
导入我们的扩展。在触发的调试跟踪中,我们可以看到以下内容:
>>>>: file=./test_lib.so [0]; dynamically loaded by python [0]
Python通过dlopen
加载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
(它将作为下一步的依赖项由 Python 加载 test_lib
)。
如果我们使用预加载的 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
所需来帮助动态链接器解析符号,因此在开始搜索符号之前必须加载它。但是,它并没有明确说明必须在foo_wrapper.so
中查找符号source_func
- 实际上我们可以在任何地方找到它并使用它。
让我们重新启动 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
设置参数,有什么区别呢?
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
)
])
为了使生成的共享库
的路径相对于当前位置(例如通过复制/移动可以更改),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
动态加载共享库,并且正如在 这篇优秀文章 中所解释的那样,有一些不同的策略可供选择。我们可以使用
>>>> 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。
foo.c
代码,为什么我不能像调用cdefed函数一样调用它(这些函数也在pxd中声明->转换为.h并在pyx中定义->转换为.c文件)。基本上是完全相同的情况,或者我错过了什么? - SleepProgger