Cython和C包装器出现未定义的符号

3

我正在尝试将C代码暴露给Cython,并在尝试从另一个Cython模块中使用在我的C文件中定义的函数时遇到“未定义符号”错误。
在我的头文件中定义的函数和使用手动包装器的函数没有问题。

基本上与这个问题相同,但解决方案(链接库)对我来说不太令人满意。
我认为在setup.py脚本中漏掉了一些内容?


我的例子:

foo.h

int source_func(void);

inline int header_func(void){
    return 1;
}

foo.c

#include "foo.h"

int source_func(void){
    return 2;
}


foo_wrapper.pxd

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

cdef source_func_wrapper()

foo_wrapper.pyx

cdef source_func_wrapper():
    return source_func()


我想使用的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


setup.py 构建 foo_wrappertest_lib 两个模块。

from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize

# setup wrapper
setup(
    ext_modules = cythonize([
        Extension("foo_wrapper", ["foo_wrapper.pyx", "foo.c"])
    ])
)

# setup test module 
setup(
    ext_modules = cythonize([
        Extension("test_lib", ["test_lib.pyx"])
    ])
)
1个回答

阿里云服务器只需要99元/年,新老用户同享,点击查看详情
12

foo_wrapper中有三种不同类型的函数:

  1. source_func_wrapper是一个Python函数,Python运行时处理调用该函数。
  2. header_func是一个内联函数,在编译时使用,因此后续不需要其定义/机器代码。
  3. 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.sotest_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_wrappertest_libfoo.cpp不是一个好主意的问题,请参见单例模式:this


回复:“如果每个人都可以直接访问功能-这将使整个包装器变得过时。” :尽管如此,这正是我计划要做的。为py和cython的常见情况cdefed类,但仍然能够在需要时从cython使用核心库函数。在我看来,这是一个有效的用例。 - SleepProgger
说实话,我有点烦恼,因为我必须以这种方式完成它。Cython嵌入了我的foo.c代码,为什么我不能像调用cdefed函数一样调用它(这些函数也在pxd中声明->转换为.h并在pyx中定义->转换为.c文件)。基本上是完全相同的情况,或者我错过了什么? - SleepProgger
@SleepProgger 可能“过时”和“混乱的设计”是太强烈的措辞了,通过包装器调用函数会涉及一些开销,因此可能希望直接使用 C 调用。 - ead
抱歉,我以为我已经接受了你的答案。谢谢你提供详细的信息。如果只想暴露一些特定的函数,另一种方法是在 pxd 文件中声明函数指针,并在 pyx 文件中定义它们。 - SleepProgger

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,