将 C++ 程序编译成 Python 模块

有些时候我们需要将已有的 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 模块一样调用我们自己的模块啦。

至于更多的细节,比如如何传递字典参数,就请大家自行参考官方文档吧。