【问题标题】:Cython undefined symbol with c wrapper带有 c 包装器的 Cython 未定义符号
【发布时间】:2017-08-12 17:59:22
【问题描述】:

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

this question 的情况基本相同,但解决方案(链接库)对我来说并不令人满意。
我假设我在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"])
    ])
)

【问题讨论】:

    标签: cython


    【解决方案1】:

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

    1. source_func_wrapper 是一个 python 函数,python 运行时处理这个函数的调用。
    2. header_func 是一个在编译时使用的内联函数,所以后面不需要它的定义/机器码。
    3. 另一方面,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_wrappertest_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_wrappertest_libfoo.cpp 链接起来并不是一个好主意:单例,请参阅 this

    【讨论】:

    • RE:“如果每个人都可以直接访问该功能 - 这会使整个包装器过时”:这正是我打算做的。 cdefed 类用于 py 和 cython 的常见情况,但如果需要,仍然可以使用 cython 的核心库函数。恕我直言,这是一个有效的用例。
    • 我有点恼火,我不得不这样做。 Cython 嵌入了我的foo.c 代码,为什么我不能像调用 cdefed 函数一样调用它(这些函数也在 pxd 中声明 -> 转换为 .h 并在 pyx 中定义 -> 转换为 .c 文件)。基本上完全相同的场景或我在这里缺少什么?
    • @SleepProgger 可能“过时”和“泥泞设计”是一种过于强大的语言,通过包装器调用函数会涉及一些开销,因此可能需要直接 C 调用。
    • 对不起,虽然我已经接受了你的回答。感谢您提供详细信息。如果只想公开某些特定函数,另一种方法是简单地在 pxd 文件中声明函数指针并在 pyx 文件中定义它们。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-06-16
    • 1970-01-01
    • 2020-02-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多