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_AsLongPyFloat_AsDoublePyUnicode_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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <Python.h>

// 函数的实现
static PyObject* my_function(PyObject* self, PyObject* args) {
PyObject* result = NULL;
PyObject* temp = NULL;

// 解析参数,这里假设我们接收一个对象
if (!PyArg_ParseTuple(args, "O", &temp)) {
return NULL;
}

// 增加参数的引用计数
Py_INCREF(temp);
result = temp; // 这里我们简单地返回传入的对象

return result;
}

// 定义模块的方法
static PyMethodDef MyModuleMethods[] = {
{"my_function", my_function, METH_VARARGS, "Return the first argument"},
{NULL, NULL, 0, NULL} // Sentinel
};

// 定义模块
static struct PyModuleDef mymodule = {
PyModuleDef_HEAD_INIT,
"my_module", // 模块名
NULL, // 模块文档,可以为NULL
-1, // 模块保持状态,在全局变量中。如果不是-1,则需要初始化函数
MyModuleMethods // 模块方法列表
};

// 初始化函数
PyMODINIT_FUNC PyInit_my_module(void) {
return PyModule_Create(&mymodule);
}

头文件

1
#include <Python.h>

要在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语言中的表示,比如PyObjectPyIntObjectPyDictObject等。这些类型允许C代码创建和操作Python对象。

<Python.h>提供了大量的宏来简化常见的任务,例如引用计数的增加和减少(Py_INCREFPy_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
2
3
4
// 解析参数,这里假设我们接收一个对象
if (!PyArg_ParseTuple(args, "O", &temp)) {
return NULL;
}

接下来,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
2
3
4
5
6
7
8
9
10
#include <Python.h>

static PyObject* add_function(PyObject* self, PyObject* args) {
long a, b;
if (!PyArg_ParseTuple(args, "ll", &a, &b)) {
return NULL; // 如果解析失败,返回NULL
}
long sum = a + b;
return PyLong_FromLong(sum); // 返回新创建的Python整数对象
}

这时候我们就把提取出来的两个元素的引用分别赋值给ab

1
2
// 增加参数的引用计数
Py_INCREF(temp);

在最上面的例子里,我们还通过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
2
3
4
5
// 定义模块的方法
static PyMethodDef MyModuleMethods[] = {
{"my_function", my_function, METH_VARARGS, "Return the first argument"},
{NULL, NULL, 0, NULL} // Sentinel
};

首先,我们把这个方法注册到模块当中,这里的PyMethodDef MyModuleMethods[]数组是用来定义模块中可从Python访问的函数的。每个条目都是PyMethodDef结构,它包含四个字段:

  1. ml_name: 方法的名称,这是Python代码中用来引用函数的字符串。
  2. ml_meth: 指向C函数的指针。
  3. ml_flags: 方法的标志,指示调用约定,如METH_VARARGS表示该函数接受一个Python的元组作为参数,包含所有Python层传递的参数。
  4. ml_doc: 方法的文档字符串,用于生成帮助文本。

其中数组的最后一个元素是一个哨兵(Sentinel)值,表示方法定义的结束。

我们知道在C语言中,数组本身不存储其长度信息,这意味着没有直接的方法来查询一个数组的长度,尤其是当数组通过指针传递给函数时。这是C语言设计的一个特点,导致了我们必须采用其他方式来跟踪数组的大小。对于比如静态分配(在编译时大小已知)的数组,可以通过一个在编译时计算的表达式来获取数组的长度:

1
2
int arr[] = {1, 2, 3, 4, 5};
size_t arr_length = sizeof(arr) / sizeof(arr[0]);

然而,这种方法不适用于动态分配的数组或者通过指针传递给函数的数组,因为在这些情况下,sizeof操作符只能返回指针的大小,而不是它指向的内存区域的大小。对于这些情况,常见的做法包括:

  1. 显式传递大小:函数调用时除了传递数组指针外,还需要传递一个表示数组长度的额外参数。
  2. 使用特殊的终止标记:对于某些特定类型的数组(例如字符串),使用一个特殊值(如'\0'字符)作为数组末尾的标记。但这要求数组不能包含这个特殊值作为其正常元素之一。
  3. 封装成结构体:将数组和其长度封装在一起作为一个结构体,这样就可以在任何需要的地方访问数组长度。

在这里,我们就是按照Python提供的约定,把NULL的哨兵值用来作标识数组的结束。在这个数组中,Python解释器会一直读取PyMethodDef结构,直到遇到这个哨兵值为止。这样,解释器就知道不再有更多的方法需要处理。如果省略它,解释器在遍历方法定义数组时将不知道何时停止,这可能导致访问数组边界之外的内存。结果可能是不确定的,通常会导致程序崩溃或未定义行为。

1
2
3
4
5
6
7
8
// 定义模块
static struct PyModuleDef mymodule = {
PyModuleDef_HEAD_INIT,
"my_module", // 模块名
NULL, // 模块文档,可以为NULL
-1, // -1代表该模块不会为每个模块实例保持独立的状态信息
MyModuleMethods // 模块方法列表
};

接下来,我们就可以定义扩展模块了,在这里我们填充PyModuleDef的各个字段,其中各字段的含义如下所示,主要需要的就是模块名和模块方法列表:

1
2
3
4
5
6
7
8
9
10
11
struct PyModuleDef {
PyModuleDef_Base m_base;
const char* m_name;
const char* m_doc;
Py_ssize_t m_size;
PyMethodDef* m_methods;
inquiry m_reload;
traverseproc m_traverse;
inquiry m_clear;
freefunc m_free;
};
  • PyModuleDef_Base m_base:
    • 这是一个宏,用于初始化结构的第一个字段。对于PyModuleDef结构体,这个字段应始终设置为PyModuleDef_HEAD_INIT,以正确初始化结构体。
  • const char* m_name:
    • 模块的名字。这是导入模块时使用的名字,例如import mymodule中的mymodule
  • const char* m_doc:
    • 模块的文档字符串。这个字符串可以通过Python的help()函数查看。它是可选的,如果你不提供模块文档,可以将其设置为NULL
  • 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
  • freefunc m_free:
    • 用于在模块生命周期结束时释放模块资源的可选函数。如果你的模块分配了需要手动释放的资源,你需要提供这个函数。否则,可以设置为NULL
1
2
3
4
// 初始化函数
PyMODINIT_FUNC PyInit_my_module(void) {
return PyModule_Create(&mymodule);
}

我们已经通过签名定义了PyModuleDef,最后就是利用这个签名通过Python的API,真正地创建模块对象,这需要我们提供一个模块的初始化函数。这个函数的任务是创建并返回模块对象。它不接受参数,并通过调用PyModule_Create函数来创建模块对象。这个函数的名字必须遵循PyInit_<module_name>的格式。初始化函数创建模块对象并返回它。如果有任何错误发生,它应该返回NULL。这个函数在Python首次导入模块时被调用。在这里,PyMODINIT_FUNC是一个宏,定义了Python模块初始化函数的返回类型为PyObject*,并确保了跨平台的兼容性和正确的导出(如果在Windows上使用DLL等)。这确保了无论在哪个平台上编译,这个函数都能被Python解释器正确地识别和调用;

编译共享库

在C的部分,我们要做的工作就基本做完了,接下里,我们需要把我们的C文件编译成共享库,来供Python加载。我们不需要手动完成这个过程,只需要setup.py脚本就可以了。

1
2
3
4
5
6
7
8
9
from distutils.core import setup, Extension

module1 = Extension('my_module',
sources = ['my_module.c'])

setup(name = 'MyPackage',
version = '1.0',
description = 'This is a demo package',
ext_modules = [module1])

在这里,我们定义了一个名为MyPackage的包,它包含了一个扩展模块my_module,源代码在my_module.c文件中。我们可以运行命令python setup.py build_ext --inplace来编译模块。如果一切顺利,将在当前目录下生成一个名为my_module.*.so的共享库文件(在Windows上是.pyd文件),其中*代表与Python版本和平台相关的一些信息。

在Python中调用

最后,我们就可以导入我们的模块了。

1
2
import my_module
print(my_module.my_function(42))

在导入模块的过程中,首先,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_CreatePyModule_Create2函数完成的,这些函数接收一个指向PyModuleDef结构的指针;一旦PyModuleDef对象被正确初始化,PyInit_xxx函数会通过PyModule_Create函数创建一个模块对象,并返回这个对象的指针。

如果初始化成功,这个模块对象将被加入到sys.modules字典中,以后的导入语句可以直接从这个字典中获取模块对象,而不需要重新加载和初始化;如果在加载或初始化过程中发生错误(例如,找不到初始化函数,或者初始化函数中发生异常),Python会抛出一个导入错误(ImportError)。