Cython未定义符号,带C包装器

Sle*_*ger 1 cython

我试图将C代码公开给cython,并尝试使用另一个cython模块中的c文件中定义的函数时遇到“未定义符号”错误。
我的h文件中定义的函数以及使用手动包装的函数都可以正常工作。

基本上与这个问题相同,但是解决方案(针对库的链接)对我来说并不令人满意。
我认为我在setup.py脚本中缺少什么?


我的案例的最小示例:

foo.h

int source_func(void);

inline int header_func(void){
    return 1;
}
Run Code Online (Sandbox Code Playgroud)

foo.c

#include "foo.h"

int source_func(void){
    return 2;
}
Run Code Online (Sandbox Code Playgroud)


foo_wrapper.pxd

cdef extern from "foo.h":
    int source_func()
    int header_func()

cdef source_func_wrapper()
Run Code Online (Sandbox Code Playgroud)

foo_wrapper.pyx

cdef source_func_wrapper():
    return source_func()
Run Code Online (Sandbox Code Playgroud)


我想在以下功能中使用cython模块:
test_lib.pyx

cimport foo_wrapper

def do_it():
    print "header func"
    print foo_wrapper.header_func() # ok
    print "source func wrapped"
    print foo_wrapper.source_func_wrapper() # ok    
    print "source func"
    print foo_wrapper.source_func() # undefined symbol: source_func
Run Code Online (Sandbox Code Playgroud)


setup.py同时构建foo_wrappertest_lib

cdef extern from "foo.h":
    int source_func()
    int header_func()

cdef source_func_wrapper()
Run Code Online (Sandbox Code Playgroud)

ead*_*ead 6

共有3种不同类型的功能foo_wrapper

  1. source_func_wrapper 是python函数,并且python运行时处理此函数的调用。
  2. header_func 是在编译时使用的内联函数,因此以后不需要其定义/机器代码。
  3. source_func另一方面,必须由静态(这种情况在foo_wrapper)或动态(我假设这是您的期望test_lib)链接器处理。

接下来,我将尝试解释为什么设置不能立即使用,但是我首先要介绍两种(至少在我看来)最佳选择:

答: 完全避免此问题。您foo_wrapper从包装W函数foo.h。这意味着每个其他模块都应使用这些包装函数。如果每个人都可以直接访问功能-这将使整个包装器过时。将foo.h界面隐藏在您的`pyx文件中:

#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():
Run Code Online (Sandbox Code Playgroud)

B:直接通过c函数使用foo功能可能是有效的。在这种情况下,我们应该使用与cython相同的策略以及stdc++-library: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
Run Code Online (Sandbox Code Playgroud)

有很多未定义的符号,大多数Py以python可执行文件开头,并且会在运行时提供。但也有我们的邪恶者- source_func

现在,我们通过以下方式启动python

LD_DEBUG=libs,files,symbols python
Run Code Online (Sandbox Code Playgroud)

并通过加载我们的扩展程序import test_lib。在触发的debug -trace中,我们可以看到以下内容:

>>>>: file=./test_lib.so [0];  dynamically loaded by python [0]
Run Code Online (Sandbox Code Playgroud)

python test_lib.so通过加载dlopen并开始查找/解析来自的未定义符号test_lib.so

>>>>:  symbol=PyExc_RuntimeError;  lookup in file=python [0]
>>>>:  symbol=PyExc_TypeError;  lookup in file=python [0]
Run Code Online (Sandbox Code Playgroud)

这些python符号很快就能找到-它们都在python-executable中定义-动态链接程序首先查看(如果此可执行文件与链接-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)
Run Code Online (Sandbox Code Playgroud)

因此,在查找所有已加载的共享库后,找不到该符号,因此我们必须中止。有趣的是,foo_wrapper尚未加载,因此source_func无法在此处查找(下一步将作为test_libpython的依赖项加载)。

如果我们以预加载的方式启动python会发生什么foo_wrapper.so

  LD_DEBUG=libs,files,symbols LD_PRELOAD=$(pwd)/foo_wrapper.so python
Run Code Online (Sandbox Code Playgroud)

这次,调用import test_lib成功,因为预加载foo_wrapper是动态加载器首先查找符号的地方(在python-executable之后):

  >>>>: symbol=source_func;  lookup in file=python [0]
  >>>>: symbol=source_func;  lookup in file=/home/ed/python_stuff/cython/two/foo_wrapper.so [0]
Run Code Online (Sandbox Code Playgroud)

但是,如果foo_wrapper.so未预加载,它如何工作?首先,让foo_wrapper.so我们作为库添加到我们的设置中test_lib

ext_modules = cythonize([
    Extension("test_lib", ["test_lib.pyx"], 
              libraries=[':foo_wrapper.so'], 
              library_dirs=['.'],
    )])   
Run Code Online (Sandbox Code Playgroud)

这将导致以下链接器命令:

 gcc ... test_lib.o -L. -l:foo_wrapper.so -o test_lib.so
Run Code Online (Sandbox Code Playgroud)

如果我们现在查找符号,那么我们看不到任何区别:

>>> nm test_lib.so --undefined
....
   U PyXXXXX
   U source_func
Run Code Online (Sandbox Code Playgroud)

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]
Run Code Online (Sandbox Code Playgroud)

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.
Run Code Online (Sandbox Code Playgroud)

好的,现在动态链接器知道了,它必须找到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
Run Code Online (Sandbox Code Playgroud)

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=['.']
             )
])
Run Code Online (Sandbox Code Playgroud)

现在打电话

 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
Run Code Online (Sandbox Code Playgroud)

foo_wrapper.soRPATH即使未通过设置,也可以在所谓的上找到LD_LIBRARY_PATH。我们可以看到它RPATH是由静态链接器插入的:

  >>>> readelf -d test_lib.so | grep RPATH
        0x000000000000000f (RPATH)              Library rpath: [.]
Run Code Online (Sandbox Code Playgroud)

但是,这是相对于当前工作目录的路径,在大多数情况下,这不是所需的路径。一个人应该走绝对的道路或使用

   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
             )
])
Run Code Online (Sandbox Code Playgroud)

使相对于当前位置的路径(可以通过复制/移动来更改)shared library. readelf现在显示的结果:

>>>> readelf -d test_lib.so | grep RPATH
     0x000000000000000f (RPATH)              Library rpath: [$ORIGIN/.]
Run Code Online (Sandbox Code Playgroud)

这意味着将相对于已加载的共享库的路径(即)搜索所需的共享库test_lib.so

如果您想重用foo_wrapper.so我不主张使用的符号,那也应该是您的设置。


但是,可以使用一些已经建立的库。

让我们回到原始设置。如果我们先导入foo_wrapper(作为一种预加载)然后才导入,该test_lib怎么办?即:

 >>>> import foo_wrapper
 >>>>> import test_lib
Run Code Online (Sandbox Code Playgroud)

开箱即用,这是行不通的。但为什么?显然,从中加载的符号foo_wrapper对其他库不可见。Python dlopen用于动态加载共享库,并且如本文所述,有几种可能的策略。我们可以用

 >>>> import sys
 >>>> sys.getdlopenflags() 
 >>>> 2
Run Code Online (Sandbox Code Playgroud)

查看设置了哪些标志。2means RTLD_NOW,这意味着在加载共享库时直接解析符号。我们需要使用OR标志,RTLD_GLOBAL=256以使符号在动态加载的库的全局/外部可见。

>>> import sys; import ctypes;
>>> sys.setdlopenflags(sys.getdlopenflags()| ctypes.RTLD_GLOBAL)
>>> import foo_wrapper
>>> import test_lib
Run Code Online (Sandbox Code Playgroud)

并且它可以正常工作,我们的调试跟踪显示:

>>> symbol=source_func;  lookup in file=./foo_wrapper.so [0]
>>> file=./foo_wrapper.so [0];  needed by ./test_lib.so [0] (relocation dependency)
Run Code Online (Sandbox Code Playgroud)

另一个有趣的细节: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! ....
Run Code Online (Sandbox Code Playgroud)

为什么这个?RTLD_LAZY表示符号不是直接在加载时解析,而是在首次使用时解析。但是在第一次使用(test_lib.do_it())之前,先foo_wrapper加载(导入test_lib模块内部),并且由于RTLD_GLOBAL其符号可用于以后的解析。

如果不使用RTLD_GLOBAL,则仅在调用时才会失败test_lib.do_it(),因为foo_wrapper在这种情况下不会全局看到所需的符号。


为了这个问题,为什么它不是一个好主意只是为了两个模块链接foo_wrappertest_lib反对foo.cpp:单身,看到这个