使用 CFFI ffi/lib 对象#
CFFI 操作指针、结构体和数组#
在 Python 中,C 代码的整数和浮点值映射到常规的 int
、long
和 float
。此外,C 类型 char
对应于 Python 中的单字符字符串。(如果你想将其映射到小整数,可以使用 signed char
或 unsigned char
。)
同样,C 类型 wchar_t
对应于单字符 unicode
字符串。请注意,在某些情况下(具有底层 4 字节 wchar_t
类型的窄 Python 构建),单个 wchar_t
字符可能对应于一对代理项,表示长度为 2 的 unicode
字符串。如果你需要将这样的 2 个字符的 unicode
字符串转换为整数,那么 ord(x)
不起作用;相反,使用 int(ffi.cast('wchar_t', x))
。
版本 1.11 中的新功能:除了 wchar_t
之外,C 类型 char16_t
和 char32_t
的工作方式相同,但具有已知的固定大小。在以前的版本中,这可以通过使用 uint16_t
和 int32_t
来实现,但没有自动转换为 Python unicodes 的功能。
指针、结构和数组更复杂:它们没有明显的 Python 等价物。因此,它们对应于类型为 cdata
的对象,例如 <cdata 'struct foo_s *' 0xa3290d8>
。
ffi.new(ctype, [initializer])
:此函数构建并返回给定 ctype 的新 cdata 对象。ctype 通常是描述 C 类型的常量字符串。它必须是一个指针或数组类型。如果它是一个指针,例如 "int *"
或 struct foo *
,那么它会为一个 int
或 struct foo
分配内存。如果它是一个数组,例如 int[10]
,那么它会为十个 int
分配内存。在这两种情况下,返回的 cdata 的类型都是 ctype。
内存最初填充为零。也可以给出一个初始化器,如后面所述。
from cffi import FFI
ffi = FFI()
ffi.new("int *")
<cdata 'int *' owning 4 bytes>
ffi.new("int[10]")
<cdata 'int[10]' owning 40 bytes>
ffi.new("char *") # allocates only one char---not a C string!
<cdata 'char *' owning 1 bytes>
ffi.new("char[]", b"foobar") # this allocates a C string, ending in \0
<cdata 'char[]' owning 7 bytes>
与 C 不同,返回的指针对象对分配的内存具有所有权:当这个确切的对象被垃圾回收时,内存就会被释放。如果在 C 级别上,你将指针存储在别处,那么请确保你也保持该对象的生命周期尽可能长。(这也适用于如果你立即将返回的指针转换为不同类型的指针:只有原始对象拥有所有权,所以你必须保持它的生命周期。一旦你忘记了它,转换后的指针就会指向垃圾!换句话说,所有权规则附加到包装的 cdata 对象上:它们不能也不应附加到底层的原始内存上。)
示例:
import weakref
global_weakkeydict = weakref.WeakKeyDictionary()
def make_foo():
s1 = ffi.new("struct foo *")
fld1 = ffi.new("struct bar *")
fld2 = ffi.new("struct bar *")
s1.thefield1 = fld1
s1.thefield2 = fld2
# here the 'fld1' and 'fld2' object must not go away,
# otherwise 's1.thefield1/2' will point to garbage!
global_weakkeydict[s1] = (fld1, fld2)
# now 's1' keeps alive 'fld1' and 'fld2'. When 's1' goes
# away, then the weak dictionary entry will be removed.
return s1
通常你不需要使用弱引用字典(weak dict):例如,要调用一个带有 char **
参数的函数,该参数包含一个指向 char *
指针的指针,你可以这样做:
p = ffi.new("char[]", b"hello, world") # p is a 'char *'
q = ffi.new("char **", p) # q is a 'char **'
# lib.myfunction(q)
# p is alive at least until here, so that's fine
然而,这总是错误的做法(使用已释放的内存):
p = ffi.new("char **", ffi.new("char[]", b"hello, world"))
# WRONG! as soon as p is built, the inner ffi.new() gets freed!
p = ffi.new("struct my_stuff")
p.foo = ffi.new("char[]", b"hello, world")
# WRONG! as soon as p.foo is set, the ffi.new() gets freed!
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[9], line 1
----> 1 p = ffi.new("struct my_stuff")
2 p.foo = ffi.new("char[]", b"hello, world")
3 # WRONG! as soon as p.foo is set, the ffi.new() gets freed!
File /opt/hostedtoolcache/Python/3.12.7/x64/lib/python3.12/site-packages/cffi/api.py:267, in FFI.new(self, cdecl, init)
265 if isinstance(cdecl, basestring):
266 cdecl = self._typeof(cdecl)
--> 267 return self._backend.newp(cdecl, init)
TypeError: expected a pointer or array ctype, got 'struct my_stuff'
cdata对象支持大部分与C语言相同的操作:您可以从指针、数组和结构中读取或写入。在 C 语言中,通常使用 *p
语法来解引用指针,这在 Python 中是不合法的,因此您必须使用替代语法 p[0]
(这也是有效的 C 语言)。此外,C 语言中的 p.x
和 p->x
语法在 Python 中都变成了 p.x
。
我们使用 ffi.NULL
作为与 C NULL 相同的地方。与后者一样,它实际上被定义为 ffi.cast("void *", 0)
。例如,读取一个空指针将返回一个 <cdata 'type *' NULL>
,您可以将其与 ffi.NULL
进行比较以进行检查。
在 Python 中没有 C 语言中的 &
运算符的通用等价物(因为它不适合这个模型,而且在这里似乎不需要)。有一个 ffi.addressof()
函数,但只适用于某些情况。例如,您不能获取一个数字的“地址”;同样,您也不能获取 CFFI 指针的地址。如果您有这种类型的 C 代码:
int x, y;
fetch_size(&x, &y);
opaque_t *handle; // some opaque pointer
init_stuff(&handle); // initializes the variable 'handle'
more_stuff(handle); // pass the handle around to more functions
当那么你需要这样重写它,用逻辑上指向这些变量的指针替换 C 中的变量:
px = ffi.new("int *")
py = ffi.new("int *") arr = ffi.new("int[2]")
lib.fetch_size(px, py) -OR- lib.fetch_size(arr, arr + 1)
x = px[0] x = arr[0]
y = py[0] y = arr[1]
p_handle = ffi.new("opaque_t **")
lib.init_stuff(p_handle) # pass the pointer to the 'handle' pointer
handle = p_handle[0] # now we can read 'handle' out of 'p_handle'
lib.more_stuff(handle)
在C语言中,任何返回指针、数组或结构体类型的操作都会生成一个新的cdata对象。与“原始”对象不同,这些新的cdata对象没有所有权:它们只是对现有内存的引用。
作为上述规则的一个例外,解引用一个拥有结构体或联合体对象的指针将返回一个“共同拥有”相同内存的cdata结构体或联合体对象。因此在这种情况下,有两个对象可以保持相同的内存活跃。这是为了应对你真的想要一个结构体对象但没有任何方便的地方来保持原始指针对象(通过ffi.new()返回)活跃的情况。
示例:
# void somefunction(int *);
x = ffi.new("int *") # allocate one int, and return a pointer to it
x[0] = 42 # fill it
lib.somefunction(x) # call the C function
print(x[0]) # read the possibly-changed value
C语言中的类型转换在 Python 中可以通过 ffi.cast("type", value)
实现。它们应该能在与C语言相同的场景下工作。此外,这是获取整数或浮点类型的cdata对象的唯一方法:
x = ffi.cast("int", 42)
x
<cdata 'int' 42>
int(x)
42
要将指针转换为 int
,将其转换为 intptr_t
或 uintptr_t
,这些类型在C中被定义为足够大的整数类型(例如在32位系统上):
>>> int(ffi.cast("intptr_t", pointer_cdata)) # signed
-1340782304
>>> int(ffi.cast("uintptr_t", pointer_cdata)) # unsigned
2954184992L
作为 ffi.new()
的可选第二个参数给出的初始化器可以是你在C代码中用作初始化器的任何东西,使用列表或元组代替C语法 { .., .., .. }
。示例:
typedef struct { int x, y; } foo_t;
foo_t v = { 1, 2 }; // C syntax
v = ffi.new("foo_t *", [1, 2]) # CFFI equivalent
foo_t v = { .y=1, .x=2 }; // C99 syntax
v = ffi.new("foo_t *", {'y': 1, 'x': 2}) # CFFI equivalent
与C语言一样,字符数组也可以从字符串进行初始化,在这种情况下,会隐式地附加一个终止空字符(null character):
x = ffi.new("char[]", b"hello")
x
<cdata 'char[]' owning 6 bytes>
len(x) # the actual size of the array
6
x[5] # the last item in the array
b'\x00'
x[0] = b'H' # change the first item
ffi.string(x) # interpret 'x' as a regular null-terminated string
b'Hello'
类似地,wchar_t、char16_t或char32_t数组可以从unicode字符串进行初始化,并且在cdata对象上调用ffi.string()将返回存储在源数组中的当前unicode字符串(如果需要,添加代理项)。有关更多详细信息,请参阅Unicode字符类型部分。
请注意,与Python列表或元组不同,但与C语言相同,您不能使用负数从C数组的末尾进行索引。
更一般地说,只要可以从初始化器中推导出长度,C数组类型就可以在其C类型中不指定长度,就像在C语言中一样:
# int array[] = { 1, 2, 3, 4 }; // C syntax
array = ffi.new("int[]", [1, 2, 3, 4]) # CFFI equivalent
作为一种扩展,初始化器还可以只是一个数字,用于给出长度(如果您只需要零初始化):
# int array[1000]; // C syntax
array = ffi.new("int[1000]") # CFFI 1st equivalent
array = ffi.new("int[]", 1000) # CFFI 2nd equivalent
如果长度实际上不是一个常数,这很有用,可以避免使用诸如 ffi.new("int[%d]" % x)
之类的操作。事实上,这是不推荐的:ffi
通常会缓存字符串 "int[]"
以避免每次都重新解析它。
C99 的可变大小结构体也得到了支持,只要初始化器说明了数组应该有多长:
# typedef struct { int x; int y[]; } foo_t;
p = ffi.new("foo_t *", [5, [6, 7, 8]]) # length 3
p = ffi.new("foo_t *", [5, 3]) # length 3 with 0 in the array
p = ffi.new("foo_t *", {'y': 3}) # length 3 with 0 everywhere
最后,请注意,任何用作初始化器的 Python 对象也可以在不使用 ffi.new()
的情况下直接用于数组项或结构字段的赋值操作。实际上,p = ffi.new("T*", initializer)
等同于 p = ffi.new("T*"); p[0] = initializer
。示例:
# if 'p' is a <cdata 'int[5][5]'>
p[2] = [10, 20] # writes to p[2][0] and p[2][1]
# if 'p' is a <cdata 'foo_t *'>, and foo_t has fields x, y and z
p[0] = {'x': 10, 'z': 20} # writes to p.x and p.z; p.y unmodified
# if, on the other hand, foo_t has a field 'char a[5]':
p.a = "abc" # writes 'a', 'b', 'c' and '\0'; p.a[4] unmodified
CFFI Python 3支持#
Python 3是受支持的,但需要注意的是,C语言中的char类型对应于Python中的bytes类型,而不是str。在将Python字符串传递给CFFI或从CFFI接收它们时,您有责任对所有Python字符串进行编码/解码。
这只涉及char类型及其派生类型;API的其他部分在Python 2中接受字符串,在Python 3中继续接受字符串。
CFFI 调用类似main的函数示例#
想象我们有以下代码:
from cffi import FFI
ffi = FFI()
ffi.cdef("""
int main_like(int argv, char *argv[]);
""")
lib = ffi.dlopen("some_library.so")
现在,除了如何创建这里的 char**
参数之外,其他都很简单。第一个想法:
lib.main_like(2, ["arg0", "arg1"])
不起作用,因为初始化器接收到两个Python字符串对象,而它期望的是 <cdata 'char *'>
对象。您需要显式使用ffi.new()
来创建这些对象:
lib.main_like(2, [ffi.new("char[]", "arg0"),
ffi.new("char[]", "arg1")])
请注意,这两个 <cdata 'char[]'>
对象在调用期间保持活动状态:它们只在列表本身被释放时才被释放,而列表只有在调用返回时才被释放。
如果您想构建一个要重用的“argv”变量,那么需要更多的注意事项:
# 不工作!
argv = ffi.new("char *[]", [ffi.new("char[]", "arg0"),
ffi.new("char[]", "arg1")])
在上面的示例中,内部“arg0”字符串在构建“argv”时立即被释放。您必须确保保留对内部“char[]”对象的引用,无论是直接保留还是像这样保持列表的活动状态:
argv_keepalive = [ffi.new("char[]", "arg0"),
ffi.new("char[]", "arg1")]
argv = ffi.new("char *[]", argv_keepalive)
CFFI 函数调用#
在调用C函数时,传递参数的规则与分配给结构字段的规则基本相同,返回值的规则与读取结构字段的规则相同。例如:
int foo(short a, int b);
n = lib.foo(2, 3) # 返回一个正常的整数
lib.foo(40000, 3) # 引发 OverflowError
你可以将普通Python字符串传递给 char *
参数(但不要将普通Python字符串传递给可能修改它的 char *
参数的函数!):
size_t strlen(const char *);
assert lib.strlen("hello") == 5
(请注意,传递给函数的 char *
指针在调用完成后可能不再有效。同样,如果你写 lib.f(x); lib.f(x)
,其中 x
是一个包含字节字符串的变量,那么两次对 f()
的调用有时会收到不同的 char *
指针,每个指针只在相应的调用期间有效。这对于使用许多优化来调整字节字符串对象底层数据的PyPy来说尤其重要。CFFI 不会在每次调用时制作和释放整个字符串的副本——它通常不会这样做——但是你不能编写依赖于它的代码:有些情况下这会破坏程序。如果你需要一个指针保持有效,你需要显式地创建一个,例如使用 ptr = ffi.new("char[]", x)
。)
你也可以将 unicode 字符串作为 wchar_t *
或 char16_t *
或 char32_t *
参数传递。请注意,C语言对于使用类型 *
或类型 []
的参数声明没有任何区别。例如,int *
完全等同于 int[]
(甚至 int[5]
;5
被忽略)。对于 CFFI,这意味着你总是可以传递可以转换为 int *
或 int[]
的参数。例如:
void do_something_with_array(int *array);
lib.do_something_with_array([1, 2, 3, 4, 5]) # 适用于int[]
参见转换以了解类似的方式传递 struct foo_s *
参数的方法——但总的来说,在这种情况下,更清楚地传递 ffi.new('struct foo_s *', initializer)
。
CFFI支持将结构和联合体传递给函数和回调。例如:
struct foo_s { int a, b; };
struct foo_s function_returning_a_struct(void);
myfoo = lib.function_returning_a_struct()
# `myfoo`: <cdata 'struct foo_s' owning 8 bytes>
为了性能,通过编写 lib.some_function
获得的非可变 API 级别函数不是 <cdata>
对象,而是另一种类型的对象(在CPython上,<built-in function>
)。这意味着您不能直接将它们传递给期望函数指针参数的其他C函数。只有 ffi.typeof()
才能在它们上工作。要获取包含常规函数指针的cdata,请使用 ffi.addressof(lib, "name")
。
有一些(晦涩难懂)的限制支持的参数和返回类型。这些限制来自 libffi
,仅适用于调用 <cdata>
函数指针;换句话说,它们不适用于使用API模式的非可变 cdef()
声明的函数。这些限制是您不能直接传递作为参数或返回类型:
联合(但指向联合的指针是可以的);
使用位域的结构(但指向此类结构的指针是可以的);
在
cdef()
中声明为“...
”的结构。
在API模式下,您可以绕过这些限制:例如,如果您需要从Python调用这样的函数指针,您可以编写一个自定义C函数,该函数接受函数指针和实际参数,并从C进行调用。然后在 cdef()
中声明该自定义C函数,并从Python使用它。
CFFI 可变参数函数调用#
C语言中的可变参数函数(以“...
”作为最后一个参数)可以正常声明和调用,但传递给变量部分的所有参数必须是cdata对象。这是因为如果你这样写:
lib.printf("hello, %d\n", 42) # 不起工作!
你无法确定你真的想将 42
作为C int
传递,而不是 long
或 long long
。同样的问题也出现在 float
与 double
之间。因此,如果需要的话,你必须使用 ffi.cast()
强制转换为所需的C类型:
lib.printf("hello, %d\n", ffi.cast("int", 42))
lib.printf("hello, %ld\n", ffi.cast("long", 42))
lib.printf("hello, %f\n", ffi.cast("double", 42))
但是当然:
lib.printf("hello, %s\n", ffi.new("char[]", "world"))
请注意,如果你使用的是dlopen(),则cdef()中的函数声明必须与C中的原始声明完全匹配,这通常是正确的——特别是,如果这个函数在C中是可变参数的,那么它的cdef()声明也必须是可变参数的。你不能在cdef()中使用固定参数来声明它,即使你只打算用这些参数类型来调用它。原因是某些架构根据函数签名是否固定有不同的调用约定。(在x86-64上,有时在PyPy的JIT生成代码中可以看到一些参数为double时的差异。)
请注意,CFFI将函数签名int foo();解释为等同于int foo(void);。这与C标准不同,在C标准中,int foo();实际上类似于int foo(…);并且可以用任何参数调用。(C的这个特性是C89之前的遗留物:在foo()的主体中不能访问参数,除非依赖于编译器特定的扩展。如今,几乎所有带有int foo();的代码实际上都意味着int foo(void);。)
CFFI 外部“Python”(新式回调)#
当C代码需要一个指向一个函数的指针,该函数调用您选择的Python函数时,您可以在离线API模式下这样做。
这是1.4版本中的新功能。如果向后兼容性是一个问题,请使用旧式回调。(原始回调调用速度较慢,并且与libffi的回调具有相同的问题;特别是,参见警告。本节描述的新样式完全不使用libffi的回调。)
在构建脚本中,在 cdef
中声明一个带有 extern "Python"
前缀的函数:
import cffi
ffibuilder = cffi.FFI()
ffibuilder.cdef("""
extern "Python" int my_callback(int, int);
void library_function(int(*callback)(int, int));
""")
ffibuilder.set_source("_my_example", r"""
#include <some_library.h>
""")
函数 my_callback()
然后在应用程序的代码中用 Python 实现:
from _my_example import ffi, lib
@ffi.def_extern()
def my_callback(x, y):
return 42
你可以通过获取 lib.my_callback
来获得一个 <cdata>
指针-函数对象。这个 <cdata>
可以传递给C代码,然后像回调一样工作:当C代码调用这个函数指针时,Python函数my_callback
被调用。(你需要将lib.my_callback
传递给C代码,而不是my_callback
:后者只是上面的Python函数,不能传递给C。)
CFFI通过将my_callback
定义为静态C函数来实现这一点,该函数在set_source()
代码之后编写。然后,<cdata>
指向这个函数。这个函数的作用是调用运行时附加了@ffi.def_extern()
的Python函数对象。
对于每个extern "Python"
函数,应该为相同名称的全局函数应用@ffi.def_extern()
装饰器。
为了支持一些特殊情况,可以通过再次调用@ffi.def_extern()
来重新定义附加的Python函数——但这不是推荐的做法!最好为这个名称附加一个单一的全局Python函数,并在一开始就更灵活地编写它。这是因为每个extern "Python"
函数都变成了只有一个C函数。再次调用@ffi.def_extern()
会改变这个函数的C逻辑,使其调用新的Python函数;旧的Python函数不再可调用。从lib.my_function
获得的C函数指针始终是这个C函数的地址,即保持不变。
CFFI Extern “Python”和 void *
参数#
如前所述,您不能使用extern "Python"来创建一个可变数量的C函数指针。然而,在纯C代码中也无法实现这一点。因此,通常C会使用带有 void *data
参数的回调函数来定义回调。您可以使用 ffi.new_handle()
和 ffi.from_handle()
通过这个 void *
参数传递一个Python对象。例如,如果回调的C类型是:
typedef void (*event_cb_t)(event_t *evt, void *userdata);
并且您通过调用以下函数注册事件:
void event_cb_register(event_cb_t cb, void *userdata);
那么您将在构建脚本中编写以下内容:
ffibuilder.cdef("""
typedef ... event_t;
typedef void (*event_cb_t)(event_t *evt, void *userdata);
void event_cb_register(event_cb_t cb, void *userdata);
extern "Python" void my_event_callback(event_t *, void *);
""")
ffibuilder.set_source("_demo_cffi", r"""
#include <the_event_library.h>
""")
然后在您的主应用程序中像这样注册事件:
from _demo_cffi import ffi, lib
class Widget(object):
def __init__(self):
userdata = ffi.new_handle(self)
self._userdata = userdata # must keep this alive!
lib.event_cb_register(lib.my_event_callback, userdata)
def process_event(self, evt):
print "got event!"
@ffi.def_extern()
def my_event_callback(evt, userdata):
widget = ffi.from_handle(userdata)
widget.process_event(evt)
其他一些库没有明确的 void *
参数,但允许您将 void *
附加到现有的结构上。例如,库可能会说 widget->userdata
是一个为应用程序保留的通用字段。如果事件的签名现在是这样:
typedef void (*event_cb_t)(widget_t *w, event_t *evt);
那么您可以像这样使用底层的 widget_t *
中的 void *
字段:
from _demo_cffi import ffi, lib
class Widget(object):
def __init__(self):
ll_widget = lib.new_widget(500, 500)
self.ll_widget = ll_widget # <cdata 'struct widget *'>
userdata = ffi.new_handle(self)
self._userdata = userdata # must still keep this alive!
ll_widget.userdata = userdata # this makes a copy of the "void *"
lib.event_cb_register(ll_widget, lib.my_event_callback)
def process_event(self, evt):
print "got event!"
@ffi.def_extern()
def my_event_callback(ll_widget, evt):
widget = ffi.from_handle(ll_widget.userdata)
widget.process_event(evt)
从C代码中直接访问extern "Python"函数#
如果你想在set_source()中编写的C代码中直接访问某些 extern "Python"
函数,你需要编写一个前向声明。(默认情况下需要是静态的,但参见下一段。)这个函数的实际实现是由CFFI在C代码之后添加的——这是必要的,因为声明可能使用由set_source()定义的类型(例如上面的event_t,来自#include
),所以它不能在之前生成。
ffibuilder.set_source("_demo_cffi", r"""
#include <the_event_library.h>
static void my_event_callback(widget_t *, event_t *);
/* 在这里你可以编写使用'&my_event_callback'的C代码 */
""")
这也可以用于编写直接调用Python的自定义C代码。以下是一个例子(在这种情况下效率较低,但如果 my_algo()
中的逻辑更复杂,可能会很有用):
ffibuilder.cdef("""
extern "Python" int f(int);
int my_algo(int);
""")
ffibuilder.set_source("_example_cffi", r"""
static int f(int); /* 前向声明 */
static int my_algo(int n) {
int i, sum = 0;
for (i = 0; i < n; i++)
sum += f(i); /* 在这里调用f() */
return sum;
}
""")