Cython未定义的符号与c包装

时间:2017-08-12 17:59:22

标签: cython

我正在尝试将c代码暴露给cython并且正在运行"未定义的符号"尝试使用另一个cython模块的c文件中定义的函数时出错 使用手动包装器在我的h文件和函数中定义的函数没有问题。

基本上与this question的情况相同,但解决方案(链接到图书馆)对我来说并不令人满意。
我假设我遗漏了 extension UIImage { func resizeImage(_ dimension: CGFloat, opaque: Bool, contentMode: UIViewContentMode = .scaleAspectFit) -> UIImage { var width: CGFloat var height: CGFloat var newImage: UIImage let size = self.size let aspectRatio = size.width/size.height switch contentMode { case .scaleAspectFit: if aspectRatio > 1 { // Landscape image width = dimension height = dimension / aspectRatio } else { // Portrait image height = dimension width = dimension * aspectRatio } default: fatalError("UIIMage.resizeToFit(): FATAL: Unimplemented ContentMode") } if #available(iOS 10.0, *) { let renderFormat = UIGraphicsImageRendererFormat.default() renderFormat.opaque = opaque let renderer = UIGraphicsImageRenderer(size: CGSize(width: width, height: height), format: renderFormat) newImage = renderer.image { (context) in self.draw(in: CGRect(x: 0, y: 0, width: width, height: height)) } } else { UIGraphicsBeginImageContextWithOptions(CGSize(width: width, height: height), opaque, 0) self.draw(in: CGRect(x: 0, y: 0, width: width, height: height)) newImage = UIGraphicsGetImageFromCurrentImageContext()! UIGraphicsEndImageContext() } return newImage } } 脚本中的某些内容?

<小时/> 最小化我的案例:

foo.h中

setup.py

foo.c的

int source_func(void);

inline int header_func(void){
    return 1;
}

<小时/> 的 foo_wrapper.pxd

#include "foo.h"

int source_func(void){
    return 2;
}

foo_wrapper.pyx

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

cdef source_func_wrapper()

<小时/> cython模块我想使用以下功能:
test_lib.pyx

cdef source_func_wrapper():
    return source_func()

<小时/> setup.py 同时构建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 foo_wrapper

test_lib

1 个答案:

答案 0 :(得分:6)

foo_wrapper中有3种不同类型的功能:

  1. source_func_wrapper是一个python函数,python运行时处理这个函数的调用。
  2. header_func是一个在编译时使用的内联函数,因此以后不需要它的定义/机器代码。
  3. 另一方面,
  4. source_func必须由静态(foo_wrapper中的情况)或动态(我假设这是您对test_lib)链接器的期望来处理。
  5. 再向下我会尝试解释,为什么设置不能开箱即用,但我想先介绍两个(至少在我看来)最好的选择:

    答:完全避免这个问题。您的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++的cython相同的策略 - 库:foo.cpp应该成为一个共享库,并且应该只有一个foo.pdx - 文件(没有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加载我们的扩展程序。在触发的debug -trace中,我们可以看到以下内容:

    >>>>: file=./test_lib.so [0];  dynamically loaded by python [0]
    

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

    >>>>:  symbol=PyExc_RuntimeError;  lookup in file=python [0]
    >>>>:  symbol=PyExc_TypeError;  lookup in file=python [0]
    

    这些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)
    

    因此,在查找所有已加载的共享库后,找不到符号,我们必须中止。有趣的是,foo_wrapper尚未加载,因此无法在那里查找source_func(它将在下一步中作为test_lib依赖于python加载。)< / p>

    如果我们使用预加载的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上找到{p> 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,其符号可用于稍后解析。< / p>

    如果我们不使用RTLD_GLOBAL,则只有在我们致电test_lib.do_it()时才会出现失败,因为在这种情况下,foo_wrapper所需的符号在全局范围内无法看到。

    对于这个问题,为什么仅仅将foo_wrappertest_lib模块与foo.cpp:单身人士联系起来并不是一个好主意,请参阅this