使用C/C++扩展Python之一

在 Thu 12 November 2015 发布于 Python 分类

假设我们需要使用C/C++实现一个翻转字符串的扩展功能, 下面是C语言的实现

#include <stdio.h>
#include <stdlib.h>

char* reverse(char *s)
{
    if (NULL == s) {
        return NULL;
    }
    int low = 0;
    int higth = strlen(s) - 1;
    while (low < hight) {
        char tmp = s[low];
        s[low] = s[hight]
        s[hight] = tmp;
        ++low;
        --hight;
    }
    return s;
}

首先,我们需要解决的问题是,怎样在Python环境下传递参数,即在Python环境下调用扩展模块,其传入的参数怎样被C扩展模块识别,再有就是C扩展模块中的函数被调用且返回的数据怎样体现到Python环境下在调用该模块的返回值中去。说到底就是CPython API库的数据类型转换。Python API的内建数据类型如list , string, dict, tuple等,在Python API中都有不同的数据结构,这里,只需要知道统一通过PyObject*来表示就好了(后续的文章会涉及到这方面)。Python提供了一组函数族把Python环境传入的参数转化成C扩展模块能识别的类型,并提供了另一单独的函数Py_BuildValue解决C扩展模块中函数返回值转换为Python环境能识别的数据类型。 这就是我们第一步需要做的:通过Python库来封装C扩展模块。

先来看看参数解析函数,这一系列函数常用的有3个,以下是这三个函数的定义:

int PyArg_ParseTuple(PyObject *args, const char *format, ...) // <1>
int PyArg_ParseTupleAndKeywords(PyObject *args, PyObject *kw, const char *format, char *keywords[], ...)  // <2>
int PyArg_Parse(PyObject *args, const char *format, ...)  // <3>

看着是不是和sscanf函数有些形似,只不过sscanf是格式化字符串。函数 \<1> 的参数args如其函数名描述的那样必须是一个元组对象,元组的元素为从Python环境调用扩展模块传入的各个参数。 参数format必须是一个格式化字符串,剩下的参数必须传入的是地址, 其类型由格式化字符串决定。函数 <2> 对应这Python关键字传递参数,其要求args也是一个元组。 函数 <3> 属于旧式函数,不推荐使用。 格式化字符串的语法如下(捡几个常用的,完整的请看Python的官方手册):

字符 Python数据类型 C数据类型 含义
s string/Unicode const char* 适配一个参数:指向字符串的指针。需要注意的C程序不需要提供字符串的存储空间,且Python字符串不能包含'\0'字符, 否则会引发TypeError异常。Unicode字符若在转换成C字符失败,将抛出UnicodeError异常
s# string/Unicode/读缓冲区兼容的对象 const char*, int/Py_ssize_t 类似于格式化参数s。其适配两个参数:一个是指向字符串的指针,一个是int(或者Py_ssize_t)类型的整数,其允许Python字符串中有'\0'字符
s* string/Unicode/读缓冲区兼容的对象 Py_buffer 类似于格式化参数s#。
z string/Unicode/None const char* 类似于格式化参数s, 但是Python字符串可以为空,即适配的字符串指针指向为空
z# string/Unicode/None/读缓冲区兼容的对象 const char*, int/Py_ssize_t 类似于格式化参数s。其适配两个参数:一个是指向字符串的指针,一个是int(或者Py_ssize_t)类型的整数。 其允许Python字符串中有'\0'字符。
z* string/Unicode/None/读缓冲区兼容的对象 Py_buffer 类似于格式化参数s#。
i integer int 把参数转换为int类型
I integer unsigned int 把参数转换为unsigned int 不带溢出检查
l integer long int 把参数转换为long int
k integer unsigned long int 把参数转换为long int,不带溢出检查
\ - -
() - - 传入的参数类型是元组,在格式化字符串里面要依次写入元组里每个元素的格式化字符,后面也要依次给出元组里元素格式化后存储的变量地址

下面的代码就是通过Python API来封装C库中的函数,该函数接受来自Python环境传递过来的参数列表,然后解析,并调用C库中的函数完成逻辑,如果有返回值的化,需要把函数的返回值在包装成PyObject*传递出去,这里面省略的引用计数以及错误处理等逻辑(后续文章会讲到)。

static PyObject* py_reverse(PyObject* self, PyObject* args)
{
    char* result;
    char* target = NULL;
    PyObject* retval = NULL;

    int ret = PyArg_ParseTuple(args, "s", &target);
    if (!ret) {
        return NULL;
    }   

    result = reverse(target);
    retval = (PyObject*)Py_BuildValue("s", result);
    return retval;
}

第二步, 建立映射关系 通过结构体PyMethodDef来实现Python扩展模块中方法和上面所示函数的映射关系,让我们看看结构体PyMethodDef的定义:

// methodobject.h
struct PyMethodDef {
    const char  *ml_name; // Python扩展模块中的方法名称
    PyCFunction  ml_meth; // 第二步中我们封装的函数, 此处是函数地址 , 需要对其做PyCFunction类型的强制转换
    int      ml_flags; // 代表这ml_meth不同的函数签名形式
    const char  *ml_doc; // 该Python扩展方法的文档
};

在这里需要注意的是PyCFunction这个函数指针类型,其定义如下:

// methodobject.h
typedef PyObject *(*PyCFunction)(PyObject *, PyObject *)

这个PyCFunction类型是大多数封装C模块方法的签名形式,它会传入一个元组(也就参数列表中第二个参数, 第一个参数给自身用的),其包括所有的从Python环境传过来的参数, 这些参数需要通过PyArg_ParseTuple()或者PyArg_UnpackTuple()这两个函数来转换成C语言能识别的数据类型。 标示ml_flags的值为METH_VARARGS就代表这这种形式的函数签名。

其实PyCFunction下面还定义了两个封装C模块方法的函数签名形式:

// methodobject.h
typedef PyObject *(*PyCFunctionWithKeywords)(PyObject *, PyObject *, PyObject *);
typedef PyObject *(*PyNoArgsFunction)(PyObject *);

这两种会在后续讲到。

下面来完成映射:

static struct PyMethodDef reverse_methods[] = {
    {"reverse", (PyCFunction)py_reverse, METH_VARARGS, NULL},
    {NULL, NULL, 0, NULL},
};

第三步, 初始化模块 这里需要注意的是,函数名称格式必须为"init"+模块的名字, 且其返回值的类型是void, 函数的参数列表必须为void,具体原因可以看这里, 我们看看PyMONDINIT_FUNC的声明:

#ifndef PyMODINIT_FUNC
#       if defined(__cplusplus)
#               define PyMODINIT_FUNC extern "C" void
#       else /* __cplusplus */
#               define PyMODINIT_FUNC void
#       endif /* __cplusplus */
#endif

在Python环境下,首次导入扩展模块的时候,就会调用初始化函数, 其调用Py_InitModule3(下面代码)创建一个模块. 下面,来完成初始化工作

PyMODINIT_FUNC initreverse(void) {
    Py_InitModule3("reverse", reverse_methods, "My first extension module.");
}

第四步, 使用distutils来生成和安装模块 从python2.6起,可以使用distutils来生成和安装模块了,唯一需要做的是你的python环境里面安装了distutils模块,且只需要写一个简单的 setup.py脚本build和install扩展模块。

from distutils.core import setup, Extensiion

moduleReverse = Extension('reverse',
            sources = ['reverse.c'])
setup(name = 'reverse',
    version = '1.0',
    description = 'This is a test!',
    ext_modules = [moduleReverse])

然后执行 python setup.py buildpython setup.py install 然后就可以使用了:

>>> import reverse
>>> dir(reverse)
['__doc__', '__file__', '__name__', '__package__', 'reverse']
>>> reverse.reverse('Zewen')   
neweZ

到此,一个简单的Python扩展模块就完成了。