Python的C扩展
Python的C扩展
总之,我踏入监狱的最初印象,是极其恶劣的;但尽管如此,——说来也怪!——我觉得,监狱生活比我在路上所想象的要轻松得多。囚犯尽管戴着镣铐,却可以在整个监狱自由地走动、吵架、唱歌、干私活、抽烟斗甚至喝酒(尽管喝酒的人很少),每到晚上还有些人开始赌博。就说劳动吧,我觉得并不十分繁重,算不上什么,很久以后我才终于明白了,说这种劳动是繁重的,其主要原因不在于它艰苦而持续不断,而是因为它是强制性的,是在棍棒的驱使之下非干不可。自由自在的庄稼汉的劳动也许多得不可比拟,有时还要夜以继日地干,夏季尤其如此;然而他是在为自己劳动,怀有一个合理的目的,因而比起被强制地从事于己无益的劳动的苦役犯来,会觉得无比的轻松。有一天我忽然想到,如果要彻底制服、压垮一个人,要对他处以一种最可怕的刑罚,以致最可怕的杀人凶手也闻之胆寒,不敢以身试法,——那么只要使劳动具有毫无益处、毫无意义的特点即可。
如果说现在的苦役对苦役犯来说是枯燥乏味的劳动,那么就劳动本身而言,它还是一种合理的行为:囚犯在制砖、松土、抹墙、盖房子;这种劳动是有意义、有目的的。从事劳动的犯人有时甚至会着迷,只想干得更巧妙、更麻利、更出色。可要是强迫他,举例来说,把一只桶里的水倒进另一只桶,再倒回原来的桶里,或捣沙土,或把一个地方的土堆拉到另一个地方,再拉回来,那么我想,犯人过不了几天就会上吊自杀,或者犯下千百种罪行,但求一死,以便摆脱这种屈辱、羞惭和痛苦。不言而喻,这样的惩罚变成了一种酷刑,一种复仇,而且是没有意义的,因为它不能达到任何合理的目的。而由于任何强制性劳动都必然会具有这种酷刑、徒劳无益、屈辱和羞惭的成分,因而苦役犯的劳动比任何自由的劳动都痛苦得无可比拟,其原因恰恰在于它的强制性。
死屋手记, 陀思妥耶夫斯基, 1862.
Python语言规范有很多种实现,比如最常见的Python就是CPython,用C语言编写的Python实现。其他的比如PyPy,是用Python语言实现的;Jython,用Java编写的;IronPython,用.NET框架的实现,等等。在这篇博客里,只讨论最常见的CPython和C语言的交互。有很多包可以用来实现与C/C++的交互,比如Cython或者Pybind11,但是直接使用Python的扩展API来编写一些简单的C扩展模块,可以更好的理解Python底层是怎么工作的。
基本图像
我们要编写一个提供给Python的C扩展模块,关键的工作是实现C和Python之间的类型转换。C是一种底层的静态语言,比如一个int类型占据4个字节;而Python使用高级的动态类型的对象模型,一个int类型占据了28个字节。所以实际上,Python中的int并不是真正的底层的整型,而只是一个看起来像整型的对象。所以我们经常说在Python中一切皆对象,在这里本体论终于有一个明确的答案了。
这个对象的底层实现就是一个C的结构体PyObject
。比如Python的浮点数在C实现上就是一个PyFloatObject
结构体,字符串是一个PyUnicodeObject
结构体,这两个结构体都由PyObject
结构体派生而来。PyObject
对象是Python对象模型的基石,是Python中所有数据类型的基本表示,无论是整数、字符串、列表还是任何其他Python对象,它们在底层都是通过PyObject
或其派生类型来表示的。Python是C写的,但我们会学习Python本身的很多语法来写出Pythonic的代码,这也有跨尺度的想法在里面,more is different。
PyObject
主要包含三个部分:
- 引用计数 (
ob_refcnt
):引用计数用于Python的自动内存管理,它记录了对象被引用的次数。当引用计数降至0时,Python解释器知道这个对象不再被需要,可以安全地回收其占用的内存。 - 类型指针 (
ob_type
):这个指针指向一个描述对象类型的结构体。这个结构体包含了一系列重要的信息,比如对象的名称(比如"int"
、"list"
等),以及与该类型相关的方法(例如,对于列表对象,如何添加元素,如何获取长度等)。 - 具体数值:结构体中还存在成员变量用于储存C语言层面上的数值,比如对于Python浮点数对象,在结构体中存在一个叫作
ob_fval
的C中的double
类型的成员变量;而对于Python的整型对象,由于它需要能够储存任意大小的整数,而不仅仅是固定大小的值,因此整数值不是通过单一的成员变量直接储存的,而是通过ob_digit
数字数组来动态表示的,这种表示方式支持任意精度的整数,这是也Python能够处理任意长度整数而不受类型大小限制的原因所在。
通过将所有Python对象表示为PyObject
或其派生类型,Python实现了一种强大的类型抽象。这种接口层面的一致,使得可以提供统一的C API来处理所有对象,良好地保持C结构体与Python对象模型的兼容性。这意味着我们不需要手动编写Python和C之间的类型转换,而只需要调用Python.h
中提供的接口就可以了。
比如要实现C到Python的类型转换,我们可以
- 使用
PyLong_FromLong
将C的long
转换为Python的整数。 - 使用
PyFloat_FromDouble
将C的double
转换为Python的浮点数。 - 使用
PyUnicode_FromString
将C的字符串(char*
)转换为Python的Unicode字符串。
相反,当我们的C扩展函数接收来自Python的数据时,需要将这些Python对象转换为C类型的数据以便处理,一般通过解析参数和提取值的方式完成,比如使用PyArg_ParseTuple
和相关的API函数:
PyArg_ParseTuple
可以解析Python的参数元组,并将其中的项转换为C类型。- 其他特定类型的提取函数,如
PyLong_AsLong
、PyFloat_AsDouble
、PyUnicode_AsUTF8
等,用于将Python对象转换为相应的C类型。
写好函数了,我们就可以接着用Python提供的接口定义出一个模块,然后在Python中调用它了。在Python中一切皆对象,也就是一切底层实现皆结构体。Python的模块也是一个对象,在C实现中对应一个PyModuleObject
结构体。在C中创建一个Python扩展模块的过程大致是这样的,首先,我们要定义一个PyModuleDef
结构体,就像指定文档元信息一样,这个结构体用来定义一个模块的静态信息,包括模块的名字、模块的文档字符串、模块的方法表以及模块的初始化函数等。模块里的方法表,也需要定义一个PyMethodDef
结构体。我们通过PyModuleDef
结构体定义了模块的基本信息之后,就可以调用PyModule_Create
函数根据这个签名结构体来创建模块对象了。这个函数将在Python首次导入模块时被调用,返回一个PyModuleObject
结构体。
在C代码层面,我们要做的就差不多是这些了,现在我们需要把C和Python连接起来。C是底层静态语言,Python是高级动态语言,C扩展需要被编译成共享库(如.so
、.pyd
文件),Python才能够加载他们。我们不需要手动去编译他们,同样只需要用Python的distutils
或者setuptools
模块来帮我们做这件事,我们要做的就是编写一个setup.py
文件。构建完成后,我们就可以在Python中调用这个模块。
以上就是我们构建一个Python的C扩展模块的基本流程。下面通过一个具体的例子再做一些简单的实践。
一个自定义C模块
基于Python的C API编写一个简单的my_module.c
文件,
1 |
|
头文件
1 |
要在C代码中使用Python C API,我们首先需要在源文件的顶部导入<Python.h>
头文件。
<Python.h>
是Python C API的主要头文件,它在编写Python扩展模块时起着核心作用。这个头文件包含了用于在C或C++代码中与Python解释器交互的大量宏、类型定义和函数声明。通过包含<Python.h>
,我们可以创建新的Python类型,调用Python对象的方法,以及执行其他操作,这些操作使得C/C++代码能够与Python代码无缝集成。具体来说,<Python.h>
提供了以下功能:
类型定义
它定义了Python对象在C语言中的表示,比如PyObject
、PyIntObject
、PyDictObject
等。这些类型允许C代码创建和操作Python对象。
宏
<Python.h>
提供了大量的宏来简化常见的任务,例如引用计数的增加和减少(Py_INCREF
、Py_DECREF
),以及Python对象之间的比较。
函数声明
这个头文件声明了许多函数,这些函数用于创建Python数据结构(列表、字典等),执行Python代码,以及其他与Python解释器交互的操作。
错误处理
它提供了机制来处理在C扩展中发生的错误和异常,比如设置异常和检查是否有异常发生。
内存管理
<Python.h>
提供了一套内存管理工具,允许开发者在扩展模块中分配和释放内存,同时确保与Python解释器的内存管理相兼容。
解释器控制
它允许C代码初始化Python解释器、执行Python脚本和表达式,以及关闭解释器。
函数定义
我们定义了一个my_function
函数,这个函数的功能是原封不动地返回传入的对象,
1 | static PyObject* my_function(PyObject* self, PyObject* args) |
static
关键字指定了函数的链接性(linkage),static
函数仅在定义它的文件内部可见,不可被文件外部的代码引用;函数的返回类型是一个PyObject*
指针,这代表这个函数可以返回Python中的任何类型对象;PyObject* args
这个参数包含了从Python代码传递给C函数的所有参数,不论有几个参数都会全部放到args
里。这些参数以Python元组的形式提供,C代码需要解析这个元组来获取具体的参数。
关于PyObject* self
,在这里是一个固定的签名,因为Python的C API就是这样设计的,可以认为是某种协议,函数签名必须具有某种形式才能识别。因为Python的C API设计为允许单个函数签名同时服务于模块级函数和类方法。所以所有C扩展函数都需要接受一个self
参数,即使它在某些情况下不会被使用。这确保了API的一致性,并简化了内部机制,让Python在调用C扩展函数时不需要区分它是模块级函数还是类方法。
对于模块级的函数(即直接在模块中定义的函数),self
参数实际上是不被使用的。在这种情况下,self
仅仅是一个占位符,实际上不会在函数体内使用它。另一方面,如果这个C函数是定义为一个类的方法,那么self
参数会指向该类的实例(或者在类方法中指向类本身),这与Python中self
的用法一致。在这种情况下,self
用于访问或修改实例的属性或调用其方法。因此,在这里假如我们在Python中调用my_function
并传入参数42时,这个42不会被赋值给C函数中的self
参数,而是会被打包成一个PyObject*
类型的元组,然后作为args
参数传递给C函数。
1 | // 解析参数,这里假设我们接收一个对象 |
接下来,PyArg_ParseTuple
是`Python.h
提供的函数,用于从args
元组中提取参数。它需要几个参数:第一个是包含实际参数的Python元组args
,第二个是一个格式字符串,指示你期望的参数类型,随后是一系列变量的地址,这些变量是用来接收从元组中提取的值。
在这个例子中,格式字符串是"O"
,这里的O
表示一个Python对象。格式字符串告诉PyArg_ParseTuple
,我们期望从Python传递的参数是什么类型。O
意味着期待可以是任何类型的Python对象。我们也可以用其他格式字符串,比如使用"d"格式字符来指定我们期待一个双精度浮点数,用l
来指定一个整型。当PyArg_ParseTuple
成功执行时,它会将args
元组中的第一个元素(因为这里只有一个O
,所以只解析一个参数)的引用赋值给temp
,此时temp
将指向传递给函数的Python对象。如果元组中有更多元素,它们将被忽略,因为格式字符串只指定了一个参数。
如果解析失败(例如,如果没有提供足够的参数,或者提供的参数类型不匹配),PyArg_ParseTuple
会返回false
(在C中用0表示),并设置一个适当的Python异常,告知调用者发生了错误。这时,函数通过返回NULL
来指示Python调用者有一个异常发生,这是Python C扩展中处理错误的典型方式。
我们也可以指定提取多个参数,比如对于一个加法函数
1 |
|
这时候我们就把提取出来的两个元素的引用分别赋值给a
和b
。
1 | // 增加参数的引用计数 |
在最上面的例子里,我们还通过Py_INCREF(temp)
增加了参数的引用计数。通过调用Py_INCREF(temp);
,我们明确地告诉Python:"这里返回了这个对象的一个新引用",这样就正确地更新了引用计数。这是管理Python内存时非常重要的一个步骤,确保对象在所有引用它的地方都"知道"它的存在,避免提前释放。因为引用计数为0的时候,Python就会回收掉这个变量对应的内存。
假如我们在函数里没有明确增加引用计数,这时我们在Python每调用一次my_function(a)
,a
的引用计数就会减少一次,最后发生Segmentation fault,段错误。这是因为当C函数返回一个Python对象给调用者时,Python运行时会自动处理引用计数。具体来说,当一个对象被作为返回值传递时,Python会假设这个返回值的引用计数已经正确设置以反映这次新的引用。如果你的函数返回了一个对象而没有通过Py_INCREF
适当增加其引用计数,Python会在某个时点减少这个对象的引用计数,因为它认为返回操作增加了一个引用。
模块定义
1 | // 定义模块的方法 |
首先,我们把这个方法注册到模块当中,这里的PyMethodDef MyModuleMethods[]
数组是用来定义模块中可从Python访问的函数的。每个条目都是PyMethodDef
结构,它包含四个字段:
- ml_name: 方法的名称,这是Python代码中用来引用函数的字符串。
- ml_meth: 指向C函数的指针。
- ml_flags: 方法的标志,指示调用约定,如
METH_VARARGS
表示该函数接受一个Python的元组作为参数,包含所有Python层传递的参数。 - ml_doc: 方法的文档字符串,用于生成帮助文本。
其中数组的最后一个元素是一个哨兵(Sentinel)值,表示方法定义的结束。
我们知道在C语言中,数组本身不存储其长度信息,这意味着没有直接的方法来查询一个数组的长度,尤其是当数组通过指针传递给函数时。这是C语言设计的一个特点,导致了我们必须采用其他方式来跟踪数组的大小。对于比如静态分配(在编译时大小已知)的数组,可以通过一个在编译时计算的表达式来获取数组的长度:
1 | int arr[] = {1, 2, 3, 4, 5}; |
然而,这种方法不适用于动态分配的数组或者通过指针传递给函数的数组,因为在这些情况下,sizeof
操作符只能返回指针的大小,而不是它指向的内存区域的大小。对于这些情况,常见的做法包括:
- 显式传递大小:函数调用时除了传递数组指针外,还需要传递一个表示数组长度的额外参数。
- 使用特殊的终止标记:对于某些特定类型的数组(例如字符串),使用一个特殊值(如
'\0'
字符)作为数组末尾的标记。但这要求数组不能包含这个特殊值作为其正常元素之一。 - 封装成结构体:将数组和其长度封装在一起作为一个结构体,这样就可以在任何需要的地方访问数组长度。
在这里,我们就是按照Python提供的约定,把NULL
的哨兵值用来作标识数组的结束。在这个数组中,Python解释器会一直读取PyMethodDef
结构,直到遇到这个哨兵值为止。这样,解释器就知道不再有更多的方法需要处理。如果省略它,解释器在遍历方法定义数组时将不知道何时停止,这可能导致访问数组边界之外的内存。结果可能是不确定的,通常会导致程序崩溃或未定义行为。
1 | // 定义模块 |
接下来,我们就可以定义扩展模块了,在这里我们填充PyModuleDef
的各个字段,其中各字段的含义如下所示,主要需要的就是模块名和模块方法列表:
1 | struct PyModuleDef { |
- PyModuleDef_Base m_base:
- 这是一个宏,用于初始化结构的第一个字段。对于
PyModuleDef
结构体,这个字段应始终设置为PyModuleDef_HEAD_INIT
,以正确初始化结构体。
- 这是一个宏,用于初始化结构的第一个字段。对于
- const char* m_name:
- 模块的名字。这是导入模块时使用的名字,例如
import mymodule
中的mymodule
。
- 模块的名字。这是导入模块时使用的名字,例如
- const char* m_doc:
- 模块的文档字符串。这个字符串可以通过Python的
help()
函数查看。它是可选的,如果你不提供模块文档,可以将其设置为NULL
。
- 模块的文档字符串。这个字符串可以通过Python的
- Py_ssize_t m_size:
- 模块的大小。对于大多数模块来说,这个值会被设置为
-1
,表示模块不支持重新初始化,也就是说模块一旦被初始化后就不能再改变。如果你的模块需要支持状态重置或者有模块级别的状态信息,这里需要使用不同的值。
- 模块的大小。对于大多数模块来说,这个值会被设置为
- PyMethodDef* m_methods:
- 指向一个数组的指针,该数组定义了模块中的方法。每个方法用
PyMethodDef
结构体表示。这个数组以一个{NULL, NULL, 0, NULL}
记录作为结束标志。
- 指向一个数组的指针,该数组定义了模块中的方法。每个方法用
- inquiry m_reload:
- 一个可选的钩子函数,用于支持模块的重新加载。大多数模块不需要这个功能,可以将其设置为
NULL
。
- 一个可选的钩子函数,用于支持模块的重新加载。大多数模块不需要这个功能,可以将其设置为
- traverseproc m_traverse:
- 用于垃圾回收的可选钩子。如果你的模块定义了自己的对象并需要参与垃圾回收,则需要提供这个函数。否则,可以设置为
NULL
。
- 用于垃圾回收的可选钩子。如果你的模块定义了自己的对象并需要参与垃圾回收,则需要提供这个函数。否则,可以设置为
- inquiry m_clear:
- 一个用于清除模块状态的可选函数。如果你的模块在全局变量中保持状态,并且需要在模块重新加载或Python解释器清理时清除这些状态,你需要提供这个函数。否则,可以设置为
NULL
。
- 一个用于清除模块状态的可选函数。如果你的模块在全局变量中保持状态,并且需要在模块重新加载或Python解释器清理时清除这些状态,你需要提供这个函数。否则,可以设置为
- freefunc m_free:
- 用于在模块生命周期结束时释放模块资源的可选函数。如果你的模块分配了需要手动释放的资源,你需要提供这个函数。否则,可以设置为
NULL
。
- 用于在模块生命周期结束时释放模块资源的可选函数。如果你的模块分配了需要手动释放的资源,你需要提供这个函数。否则,可以设置为
1 | // 初始化函数 |
我们已经通过签名定义了PyModuleDef
,最后就是利用这个签名通过Python的API,真正地创建模块对象,这需要我们提供一个模块的初始化函数。这个函数的任务是创建并返回模块对象。它不接受参数,并通过调用PyModule_Create
函数来创建模块对象。这个函数的名字必须遵循PyInit_<module_name>
的格式。初始化函数创建模块对象并返回它。如果有任何错误发生,它应该返回NULL
。这个函数在Python首次导入模块时被调用。在这里,PyMODINIT_FUNC
是一个宏,定义了Python模块初始化函数的返回类型为PyObject*
,并确保了跨平台的兼容性和正确的导出(如果在Windows上使用DLL等)。这确保了无论在哪个平台上编译,这个函数都能被Python解释器正确地识别和调用;
编译共享库
在C的部分,我们要做的工作就基本做完了,接下里,我们需要把我们的C文件编译成共享库,来供Python加载。我们不需要手动完成这个过程,只需要setup.py
脚本就可以了。
1 | from distutils.core import setup, Extension |
在这里,我们定义了一个名为MyPackage
的包,它包含了一个扩展模块my_module
,源代码在my_module.c
文件中。我们可以运行命令python setup.py build_ext --inplace
来编译模块。如果一切顺利,将在当前目录下生成一个名为my_module.*.so
的共享库文件(在Windows上是.pyd
文件),其中*
代表与Python版本和平台相关的一些信息。
在Python中调用
最后,我们就可以导入我们的模块了。
1 | import my_module |
在导入模块的过程中,首先,Python解释器会在预定义的路径列表中搜索名为xxx
的模块。这个列表包括内置模块、当前目录、环境变量PYTHONPATH
指定的目录,以及安装目录下的库目录;一旦找到模块,Python会根据文件扩展名(如.py
、.pyc
、.so
、.pyd
等)来确定模块的类型。对于C扩展模块,这通常是.so
(在Unix-like系统上)或.pyd
(在Windows上)文件;对于C扩展模块,Python会使用动态链接加载器(如dlopen
在Unix-like系统上,或LoadLibrary
在Windows上)来加载模块的共享库文件;
加载模块文件后,解释器会查找一个名为PyInit_xxx
的函数,在这个函数中,xxx
是模块名。这个函数是模块的初始化函数,它负责创建和初始化模块对象。;PyInit_xxx
函数内部会创建一个PyModuleDef
类型的对象,这个对象包含了模块的元信息(如模块名、模块文档字符串、模块方法表等)。这是通过调用PyModule_Create
或PyModule_Create2
函数完成的,这些函数接收一个指向PyModuleDef
结构的指针;一旦PyModuleDef
对象被正确初始化,PyInit_xxx
函数会通过PyModule_Create
函数创建一个模块对象,并返回这个对象的指针。
如果初始化成功,这个模块对象将被加入到sys.modules字典中,以后的导入语句可以直接从这个字典中获取模块对象,而不需要重新加载和初始化;如果在加载或初始化过程中发生错误(例如,找不到初始化函数,或者初始化函数中发生异常),Python会抛出一个导入错误(ImportError
)。