有些时候我们需要将已有的 C/C++ 程序编译成 Python 模块方便调用,其实 Python 提供了简便的支持。主要分为以下几步:
在 C/C++ 中编写 Python 方法
为了举例方便,假设我们要写一个简单的加法模块。要开始在 C/C++ 程序中编写 Python 方法,首先要包含 Python.h
这个头文件。然后根据需要声明函数,函数的形式非常统一,返回值和参数都是 PyObject*
类型,具体实现方法可以参考下面的代码:
#include <Python.h>
static PyObject* add(PyObject* self, PyObject* args) {
int a, b;
if (!PyArg_ParseTuple(args, "ii", &a, &b)) {
return NULL;
}
int sum = a + b;
PyObject* ret = Py_BuildValue("i", sum);
return ret;
}
对于每一个函数,要得到其中的参数,可以使用 PyArg_ParseTuple
这个函数来获取 Python 传进来的参数(保存在 args
这个变量中),args
可以理解为 Python 传进来的 Tuple,但在 C/C++ 中需要我们一个个提取其中的参数,所以,其用法非常接近于 sscanf
,区别在于格式字符串的写法而已,对于格式字符串中不同字母代表的意义,这里不再赘述,可以参考官方文档: https://docs.python.org/3/c-api/arg.html
这个函数解析成功则返回 true
,失败则是 false
。当我们获取了所需的参数之后,就可以调用 C/C++ 方法来进行处理。
处理完成的结果需要我们手动保存成一个 PyObject*
返回给 Python,保存的过程就是解析的反过程,用到的函数是 Py_BuildValue
,同样提供一个格式字符串,然后提供对应的变量即可。当然,构造返回变量也可以用另外一种办法:
PyObject* ret = PyLong_FromLong(success);
其中 Py*_From*
顾名思义,就是用对应类型的 C 变量构造一个对应类型的 Python 对象。
声明方法列表
当我们写好所需要的方法之后,要定义一个数组告诉 Python 哪些方法应该以什么方式调用,代码像下面一样:
static PyMethodDef MySumMethods[] = {
{"add", add, METH_VARARGS, "Add two integers"},
{NULL, NULL, 0, NULL}
};
数组元素结构很简单,每一个元素都用四个信息定义一个 Python 方法:Python 里的函数名、实际调用的 C 方法、Python 传参类型(大部分时候是 METH_VARARGS
或者 METH_VARARGS | METH_KEYWORDS
)、对于该方法的描述,最后以 {NULL, NULL, 0, NULL}
结尾。
声明完方法之后,就可以声明一个模块并导出了:
static struct PyModuleDef mysummodule = {PyModuleDef_HEAD_INIT, "mysum",
NULL, -1, MySumMethods};
PyMODINIT_FUNC PyInit_mysum() { return PyModule_Create(&mysummodule); }
代码也很简单,只要用类似的方法,根据定义传递信息即可。需要注意的是 PyInit_mysum()
这个函数是 Python 用来初始化模块的,后面的名字要和模块名字对应。
编写 setup.py 并编译
至此,我们已经完成了一个 Python 模块所需要的必须代码,接下来的工作就是要将其编译成 Python 可以调用的模块,这项工作是通过编写 setup.py
并调用 distutils
来完成的。setup.py
的代码如下:
from distutils.core import setup, Extension
mysum = Extension('mysum',
include_dirs=['./include'],
language="c++",
sources=['mysum.cpp'])
setup(name='mysum',
version='0.1',
description='Python interfaces of a C++ program',
ext_modules=[mysum])
代码也十分简单,只需要按照格式填写需要的信息即可。编写完成之后,我们就可以用 pip install .
来安装我们写好的模块了。之后就像调用普通 Python 模块一样调用我们自己的模块啦。
至于更多的细节,比如如何传递字典参数,就请大家自行参考官方文档吧。