使用C/C++编写Python扩展库

kipway@kipway.com

2017.5.25

本文描述在Windows系统下(Linux下也一样,官方文档本来就没有区分系统)使用C/C++编写Python3扩展库的方法,Python Extending 的官方文档在这里(Extending and Embedding the Python Interpreter )。下面主要讲原理,有描述不清楚的请参考官方文档。

1.Python扩展库的本质

Python扩展库的文件后缀名为pyd,实际就是一个标准的C方式导出函数的DLL动态库,使用结构体指针和函数指针来实现对象的传递。制定了Python和扩展库之间函数参数传递规范,数据对象规范,为方便使用,像微软的COM API一样,Python提供了Python/C API来完成参数的解析,对象的构造等。

2.开发环境

开发环境:既然是windows系统,当然使用微软的集成开发环境,这里使用VS2015中的VC140。

运行测试环境:到Python官网下载最新的Python3.6.1,安装后到安装目录提取Python/C API,两个目录,include和libs复制出来,加入到VC工程的附加包含目录和附加库目录即可。下面先以一个具体例子讲解,先不到出对象,只导出函数。

3.实现一个简单的Python扩展库

假设这里的例子扩展模块名为libkdc,在VS里建立一个win32 DLL工程,工程名为linkdc,使用MT模式摆脱VC运行库。将Python/C API的include和Libs目录加入到libkdc工程的附加包含目录和附加库目录。在源代码中引入Python.h,为方便使用,再定义一个C方式导出函数宏PyExt_FUNC

#include <Python.h>
#ifdef _WIN32
#   if defined(__cplusplus)
#       define PyEXT_FUNC extern "C" __declspec(dllexport) PyObject*
#   else 
#       define PyEXT_FUNC __declspec(dllexport) PyObject*
#   endif
#else
#   define PyEXT_FUNC extern "C" PyObject*
#endif

然后定义一个导出函数供Python使用

PyEXT_FUNC py_kdc_open(PyObject* self, PyObject* args)
{
    const char* sip;
    int port;
    const char* suser;
    const char* spass;
    if (!PyArg_ParseTuple(args, "siss", &sip, &port, &suser, &spass))
    {
        Py_INCREF(Py_None);
        return Py_None;
    }
    int  nh = kdc_open(sip, port, suser, spass, 0);//这句就不必关注了,实现open的具体代码    
    return Py_BuildValue("i", nh);
}

先认为所有的导出函数都是上面的例子,返回一个PyObject*对象,有两个PyObject*参数(实际也可以有三个,具体请参见官方文档)。self相当于this指针(因为Python使用结构体和函数指针来实现的对象),目前只导出函数,因此不必理会这个参数。 args就是可变参数的参数数组对象(Python中叫做“Tuple”),Python进来的参数要使用API函数PyArg_ParseTuple来解析成C变量,返回值要使用API函数Py_BuildValue把C变量装换为Python对象。Python/C API不仅可以创建简单的值对象,还可以创建Python内置的Tuple,List,Dict等集合类对象。细节参考官方文档,也就是Python/C API提供了常用的Pyhton对象和C之间的转换函数。

如果是普通的动态库,上面定义的导出函数py_kdc_open已经可以被其他程序使用了,但是Python解释程序还不知道有这个函数,因此需要实现一个Python约定的函数PyInit_xxx(),其中的xxx就是模块名libkdc,当Python中import libkdc时,就会首先调用这个约定的导出函数,获取其他导出函数的定义,相当于微软COM组件IUnknown接口的QueryInterface方法,但是这里Python来的更简单更直接,Python来源于Linux,Linux下大部分的软件和工具是为程序员服务的,简单直接明了,windows下的软件和工具是直接为最终用户服务的,程序员用起来晦涩难懂。下面给出PyInit_libkdc的实现

PyMODINIT_FUNC PyInit_libkdc(void)
{
    static PyMethodDef pyMethods[] =
    {
        { "open", py_kdc_open, METH_VARARGS, "open the channel" },//先只看这一行就行了
        { "status", py_kdc_status, METH_VARARGS, "get the channel status" },
        { "close", py_kdc_close, METH_VARARGS, "open the channel" },        
        { "select", py_kdc_select, METH_VARARGS, "query records" },
        { NULL, NULL,0,NULL }
    };
    static struct PyModuleDef cModPyDem =
    {
        PyModuleDef_HEAD_INIT,
        "libkdc",  // name of module 
        NULL,      // module documentation, may be NULL 
        -1,        // size of per-interpreter state of the module, or -1 if the module keeps state in global variables. 
        pyMethods  // Methods
    };
    return PyModule_Create(&cModPyDem);
}

上面约定函数目的就是告诉Python这个libkdc扩展库里面有哪些方法可以调用(目前没有导出对象,只有方法,具体用法参见官方文档),好了,编译成动态库libkdc.dll,改名字为libkdc.pyd就可以当作Python扩展库使用了。

4.实现自定义Python对象

基本数据类型对象和Python内置的集合类对象一般够用了,如果要实现自定义对象返回给Python使用,可参见官方文档Defining New Types一章,其基本原理就是你需要扩展定义一个基于PyObject的结构体,增加你自己的成员变量,填写构造函数和析构函数和其他基础性的函数指针,填写自定义方法的函数指针。定义一个static的自定义结构体全局变量(相当于COM中的类厂),然后在PyInit_XXX()中使用Py_INCREF增加引用计数(和COM组件一样,Python也是通过引用计数来决定销毁对象的时机),使用PyModule_AddObject将自定义的对象加入到模块中。 这时候上面例子导出函数的self参数就有用了,相当于C++中的this指针,确定操作的是哪个对象,因为在Python扩展库中,对象的数据和方法是分开的。官方文档给出的例子如下:

#include <Python.h>
typedef struct {
    PyObject_HEAD
    /* Type-specific fields go here. */
} noddy_NoddyObject;
static PyTypeObject noddy_NoddyType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    "noddy.Noddy",             /* tp_name */
    sizeof(noddy_NoddyObject), /* tp_basicsize */
    0,                         /* tp_itemsize */
    0,                         /* tp_dealloc */
    0,                         /* tp_print */
    0,                         /* tp_getattr */
    0,                         /* tp_setattr */
    0,                         /* tp_reserved */
    0,                         /* tp_repr */
    0,                         /* tp_as_number */
    0,                         /* tp_as_sequence */
    0,                         /* tp_as_mapping */
    0,                         /* tp_hash  */
    0,                         /* tp_call */
    0,                         /* tp_str */
    0,                         /* tp_getattro */
    0,                         /* tp_setattro */
    0,                         /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT,        /* tp_flags */
    "Noddy objects",           /* tp_doc */
};
static PyModuleDef noddymodule = {
    PyModuleDef_HEAD_INIT,
    "noddy",
    "Example module that creates an extension type.",
    -1,
    NULL, NULL, NULL, NULL, NULL
};
PyMODINIT_FUNC
PyInit_noddy(void)
{
    PyObject* m;
    noddy_NoddyType.tp_new = PyType_GenericNew;
    if (PyType_Ready(&noddy_NoddyType) < 0)
        return NULL;
    m = PyModule_Create(&noddymodule);
    if (m == NULL)
        return NULL;
    Py_INCREF(&noddy_NoddyType);
    PyModule_AddObject(m, "Noddy", (PyObject *)&noddy_NoddyType);
    return m;
}

5.总结

Python使用了两个技术就实现了扩展:

  • 动态库,作为扩展库的包装方式,利用其动态加载特性实现import,通过约定的C导出函数查询接口和导出对象
  • C语言的结构体指针和函数指针实现动态语言的对象和C程序交换数据
  • 对于悟道的程序员来讲,计算机开发语言只有C语言和C语言产生的工具语言(java,.Net,js,Pyhton等),如果会使用词法分析flex和语法分析bison这两个工具,你可以自己设计一种语言。

    by the way 这篇文章使用gedit编辑,发现gedit书写html文档并不输notepad++和sublime text。