Python CFFI 中的动态链接?

问题描述 投票:0回答:1

我正在尝试使用 CFFI 在 Python 中动态链接依赖的 C 库;难道我理解错了?

在下面极其简化的示例中,库

foo_b
依赖于库
foo_a
。具体来说,它依赖于
bar
,并且它公开了自己的函数
baz

from cffi import FFI
from pathlib import Path

Path('foo_a.h').write_text("""\
int bar(int x);
""")
Path('foo_a.c').write_text("""\
#include "foo_a.h"
int bar(int x) {
  return x + 69;
}
""")

Path('foo_b.h').write_text("""\
int baz(int x);
""")
Path('foo_b.c').write_text("""\
#include "foo_a.h"
#include "foo_b.h"
int baz(int x) {
  return bar(x * 100);
}
""")

ffi_a = FFI()
ffi_b = FFI()

ffi_a.cdef('int bar(int x);')
ffi_a.set_source('ffi_foo_a', '#include "foo_a.h"', sources=['foo_a.c'])
ffi_a.compile()

ffi_b.cdef('int baz(int x);')
ffi_b.include(ffi_a)
ffi_b.set_source('ffi_foo_b', '#include "foo_b.h"', sources=['foo_b.c'])
ffi_b.compile()

import ffi_foo_a
if ffi_foo_a.lib.bar(1) == 70: print('foo_a OK')
else: raise AssertionError('foo_a ERR')

import ffi_foo_b  # Crashes on _this_ line due to undefined symbol "bar", DESPITE the fact that we included ffi_a, which should provide that symbol
if ffi_foo_b.lib.baz(420) == 42069: print('foo_b OK')
else: raise AssertionError('foo_b ERR')

但是,它不会编译,而是在指示的行上崩溃并显示指示的错误消息。

考虑到 CFFI 文档中的以下内容,我不明白为什么这个示例不起作用:

对于外联模块,

ffibuilder.include(other_ffibuilder)
行应该出现在构建脚本中,并且
other_ffibuilder
参数应该是来自另一个构建脚本的另一个FFI实例。当两个构建脚本变成生成文件时,例如
_ffi.so
_other_ffi.so
,然后导入
_ffi.so
将在内部导致
_other_ffi.so
被导入。此时,来自
_other_ffi.so
的真实声明与来自
_ffi.so
的真实声明相结合。

如果 ffibuilder.include() 不是动态链接多个基于 CFFI 的库的正确方法,那么什么是?

或者如果 ffibuilder.include() 动态链接多个基于 CFFI 的库的正确方法,我做错了什么?

python dynamic-linking python-cffi
1个回答
0
投票

ffi_foo_b.cpython-XXX.so
无法导入,因为C级问题,它找不到符号
bar
。看看CFFI中的测试,似乎不支持这种情况:
ffi_foo_b.include(ffi_foo_a)
语法不足以导致C级
ffi_foo_b.cpython-XXX.so
被特殊编译。如果
ffi_foo_a.cpython-XXX.so
需要 在 C 级别 才能加载
ffi_foo_b.cpython-XXX.so
,那么它将无法工作。 CFFI 文档有点误导。相反,这意味着,您可以采用
struct
中出现的
ffi_a
类型定义之类的内容,并在
lib_foo_b.ffi
中使用它们。

就 CFFI 实现而言,我不太确定如何支持这种情况:例如,在 Windows 上,您需要特殊技巧从 DLL 导出符号(对于 CPython 扩展,扩展名为

.dll
.pyd
模块)。换句话说,您在 Windows 上的示例生成的
ffi_foo_a
根本不会在 C 级别自动导出
bar
。您仍然可以调用
ffi_foo_a.bar()
,因为在
bar
内查找
ffi_foo_a
是在与原始 C 级别不同的级别(在任何平台上)完成。如果没有
ffi_foo_b.bar()
尝试在 C 级别直接使用
ffi.include()
的问题,您也可以将其称为
ffi_foo_b
,感谢
bar
。但你不能使用
bar
中的 C 符号
foo_b.set_source()

目前我推荐以下解决方案之一:

  1. 将所有内容整合到一个

    ffi
    中。

  2. 根据需要制定标准,独立于 CFFI 和 Python

    .so
    ,例如
    foo_a.so
    foo_b.so
    ,使用适当的C级Makefile或其他东西,用
    foo_b
    以这样的方式编译,表明它依赖于
    foo_a
    ——使用一些gcc参数,如
    -L . -lfoo_a.so
    。然后你可以用两个CFFI库包装这两个
    .so
    。然后你可以使用
    ffi.include()
    ;这确实是
    ffi.include()
    的工作原理。

  3. 作为 1 和 2 的混合,您可能可以将

    foo_a.so
    编译为独立模块,然后包装在
    ffi_a
    中,但保留
    ffi_b
    的现有解决方案,因为预计不会出现来自
    ffi_b
    的符号从其他地方可以在 C 级别找到。

编辑:另外两个可能的解决方案:

  1. 您可能可以让它像您编写的那样工作,但您需要添加特定于平台和编译器的选项。我不推荐这个选项。

  2. 您可以通过不从

    bar()
    调用
    foo_b.c
    来绕过该问题。相反,您可以编写如下代码:

     Path('foo_b.c').write_text("""\
     #include "foo_b.h"
     static int (*_glob_bar)(int);  // global variable
     int baz(int x) {
       return _glob_bar(x * 100);
     }
     """)
    
     ffi_b.cdef("""
         int (*_glob_bar)(int);
         int baz(int x);
     """)
    

    这消除了 C 级别的依赖。你需要 导入后初始化全局变量一次:

     import ffi_foo_b
     ffi_foo_b.lib._glob_bar =
         ffi_foo_a.ffi.addressof(ffi_foo_a.lib, "bar")
    
© www.soinside.com 2019 - 2024. All rights reserved.