在本章我们会用 C 实现一个简单的的 Python 内置模块, 以了解用 C/C++ 拓展 Python 时的核心的组件和基本流程。
假设这个模块叫 mymath
, 其中有一个sum
函数:
1 | import mymath |
一、编写源码
首先我们要编写这份 mymath.c
源码, 这份源码要做的事情大致如下:
- 从固定的接口返回一个 Python 的 Module 对象
- 这个 Module 对象通过一个全局的静态
struct
定义 - 这个 Module 对象里包含(一些) Python 函数对象
- 这些函数对象要接受 Python 对象组成的参数列表,并以 Python 对象为返回值
下面开始行动。
1. 从固定的接口返回一个 Python 的 Module 对象
就像一般 C 程序的入口是 main
函数一样, 用 C 编写 Python 的 Module 也有一个固定的接口:
1 | /* 使用 Python 的 C API 必须 include Python 的头文件,且放在第一行*/ |
2. 这个 Module 对象通过一个全局的静态 struct
定义
刚才我们在创建 Module 对象的时候使用到了一个静态 struct
的指针。Module 的名字,文档,它所包含的函数等都由这个 struct
指定。那我们怎么定义这个 struct
呢?
1 | static struct PyModuleDef mymath_module = { |
Python 里面万物皆 Object,且大多位于 heap 之中,但这个 Module 除外。
我们不想要 Module 被莫名其妙地释放掉,所以这个 struct
是 global static 的。
3. 这个 Module 对象里包含(一些) Python 函数对象
刚才在定义 Python Module struct 的时候提到 Module 所有的函数都由一个 array 提供。那这个 array 又怎么定义呢?
1 | static PyMethodDef MymathMethods[] = { |
嗯, 这也是一个 global static 的数据。
其实 PyMethodDef
是一个 struct
的别名, 为了保持一致性而特意命名成 Py<xxx>Def
的形式的。以下是这个 struct 的定义,看起来也是相当的 self-explained:
1 | struct PyMethodDef { |
现在再来看 MymathMethods
这个 array
的第一项。
字符串"sum"
是提供给 Python Interpreter 的函数名称,而 sum
是当从 Python 调用这个函数时, 实际调用的 C 函数,"add up two numbers and return their sum"
自然是文档。
METH_VARARGS
则影响 Python C API 如何解析从 Python Interpreter 传入的参数, 这是一个比较复杂的问题, 我们会在下一章函数的专题中仔细探讨。现在只需要记住,它把 Python 中传入的参数当做一个 tuple
。
最后一项又是一个约定俗称的规范, 这一项的函数名称为 NULL
, 用以说明这是这个 array
的最后一项。
接下来我们只剩下一个待解决的问题了, C 函数 sum
该怎么定义?
4. 这些函数对象要接受 Python 对象组成的参数列表,并以 Python 对象为返回值
Python 中万物皆 Object,函数的参数从 Python Interpreter 传过来,计算结果又返回到 Interpreter,所以它们自然都是 PyObject。或者说,都是 PyObject 的指针。
1 | PyObject *sum(PyObject *self, PyObject *args) { // 第一个是 self, 第二个是 args tuple |
在 C 里面定义的 Python 函数都有类似的 prototype。返回类型总是 PyObject*
,参数总是 (PyObject* self, PyObject* args, PyObject* kwargs)
,第二个和第三个分别会被转义成 Python 的 tuple
和 dict
, 并且都是可选的。
在函数体内,需要注意的就是解析参数以及在最后把返回值转义成 PyObject*
(当然也有异常流程和引用计数的问题,不过我们在下一章讨论)。
至此, 我们已经实现了一个完整的 Python Module 了。我们只需要把它编译到 Python 里面去, 就可以使用它啦。
二、编译安装
因为我们打算以内置(built-in)的形式使用我们的 Python Module,所以我们得把刚才编写好的源码放到 Python 源码中去,再重新编译和安装整个 Python(我们也能以 package 的形式安装我们的 C 拓展,当然那是以后的内容)。我建议在 Docker 中执行这些操作,以免破坏本机环境…
1. 获取 Python 源码
你可以从搜狐镜像下载一份 Python 3.5.3 的源码, 并解压它。
2. 放入我们的模块
在源码文件夹内, 有一个 Modules 文件夹专门用来放置内置的 Module, 把我们的mymath.c
复制到里面来。
接着 vim Modules/Setup.local
, 文件末尾插入 mymath mymath.o
。
1 | /* mymath.c */ |
1 | Modules/Setup.local |
3. 编译和安装
1 | make -j 4 |
4. Let’s Play
最后让我们进入 Python3 的 Interpreter 开始玩耍吧。
1 | root@bab0ce9dfc54:/tmp/Python-3.5.3# python3 |
将会看到如下输出
1 | Help on built-in module mymath: |
按 q
返回 Interpreter, 接着输入:
1 | >> number = mymath.sum(1, 2) |
Congratulations! 我们最初的目标实现了。