Python与C/C++互操作,,Python调用C/


Python调用C/C++

Python调用C/C++的方法可以分为两类:

手写扩展模块:除了被调用的C/C++函数外,一般还需要编写包裹函数、导出表、导出函数、编译脚本等代码。

使用封装库的接口:比如官方的ctypes,还有第三方的如CFFI、Boost、SWIG、pybind11等。

最终在Python中都是通过载入动态链接库的方式实现调用,手写模块的方式更为复杂,但也更为高效。在实际工程中,一般对需要调用C/C++的函数(主要出于性能考虑)先通过ctypes实现,再有针对性地使用手写模块的方式改写。

ctypes

ctypes[文档]是Python官方的一个提供C兼容数据类型的外部函数库,通过动态链接或共享库的方式调用。

从原理上来说[文章]:ctypes直接调用二进制的动态链接库(平台兼容性差),在Windows下最终调用的是Windows API中的LoadLibrary函数和GetProcAddress函数,在UNIX平台下最终调用的是Posix标准中的dlopen和dlsym函数。ctypes实现了一系列的类型转换方法,Python数据类型被包装或直接推算为C类型,即手写模块中的PyObject * <-> C types的转换由ctypes内部完成。

手写扩展模块

Python/C API Reference Manul

包裹函数:主要实现Python和C的参数转换,以及对C函数的调用。

导出表:告诉Python模块名,其中的被调函数名,以及对应的包裹函数和参数说明。

导出函数:按照导出表完成模块初始化。函数名必须以PyInit_为前缀。

Python和C之间的类型转换代码如下表所示。
| Format Code | Python Type | C Type |
| :-: | :-: | :-: |
| s | str | char * |
| z | str/None | char /NULL |
| i | int | int |
| l | long | long |
| c | str | char |
| d | float | double |
| D | complex | Py_Complex
|
| O | (any) | PyObject * |
| S | str | PyStringObject |

相比于ctypes,手写扩展模块复杂得多,同时还可能带来一些问题,如包裹函数中可能需要free()操作(防止内存泄漏)、对象引用计数的宏(Py_INCREF(), Py_DECREF(), Py_XINCREEF(), Py_XDECREF())、多线程的宏(Py_BEGIN_ALLOW_THREADS, Py_END_ALLOW_THREADS)等,因此对程序员要求较高。

实现与性能

首先给出一个ctypes的例子。

编写一个简单的C程序fac.c实现阶乘功能。

int fac(int n){    if (n < 2)        return 1;    return n * fac(n - 1);}

编译生成动态链接库。

gcc fac.c -fPIC -shared -o fac.so

在Python中调用。

from ctypes import cdlllibc = cdll.LoadLibrary(‘./fac.so‘)print(libc.fac(3)) # output 3! = 6

然后给出同样功能的手写扩展模块实现。

首先使用样板(即接口)包裹上述阶乘程序,使得应用程序代码可以和Python解释器进行交互。注意:Python2和Python3的接口略有不同,本文针对Python3。

int fac(int n){    if (n < 2)        return 1;    return n * fac(n - 1);}#include <Python.h>// 包裹函数static PyObject *ExFac(PyObject *self, PyObject *args){    int n;    // 参数转换    // 根据指定格式解析并将结果放入指针变量,返回0表示解析失败    if (!PyArg_ParseTuple(args, "i", &n))        return NULL;    // 把C数据转换为Python对象并返回    return (PyObject *)Py_BuildValue("i", fac(n));}static PyMethodDef ExfunMethods[] = {    // 表示参数以tuple形式传入    {"fac", ExFac, METH_VARARGS},    {NULL, NULL},};// 导出表static struct PyModuleDef ExfunMethod = {    PyModuleDef_HEAD_INIT,    "exfun",    NULL,    -1,    ExfunMethods};// 导出函数void PyInit_exfun(){    PyModule_Create(&ExfunMethod);}

把该模块编译到Python中。

# setup.pyfrom distutils.core import setup, ExtensionMOD = ‘exfun‘setup(name=MOD, ext_modules=[Extension(MOD, sources=[‘exfun.c‘])])
python3 setup.py buildpython3 setup.py install

在Python中调用。

import exfunprint(exfun.fac(3)) # output 6

在Python中导入并调用exfun.fac()后,包裹函数ExFac()被调用,接受一个Python的整型参数,并转化为C的整型,然后调用C的fac()函数,得到一个整型返回值,再转为Python的整型作为整个函数调用的结果返回。

最后比较原生Python与上述两种C调用的性能表现。

Python提供timeit模块来测量小代码段的执行时间。以计算20的阶乘为例,timeit默认执行1,000,000次。

from ctypes import cdllimport exfunimport timeitdef fac(n):    if n < 2:        return 1    return n * fac(n - 1)libc = cdll.LoadLibrary(‘./fac.so‘)print(timeit.timeit("fac(20)", setup="from __main__ import fac")) # 2.78sprint(timeit.timeit("libc.fac(20)", setup="from __main__ import libc")) # 0.39sprint(timeit.timeit("exfun.fac(20)", setup="from __main__ import exfun")) # 0.16s

需要注意的是,由于Python3中int对长整型的支持,计算20!时原生Python得到的是正确的结果,而调用C的都溢出了。

其他方法

如CFFI,
Boost,
SWIG,
pybind11。

C/C++调用Python

C/C++调用Python一般是为了利用脚本开发的灵活性,类似于游戏开发中Lua和C++的结合。在C++应用中,我们可以用一组插件来实现一些具有统一接口的功能,一般插件都使用动态链接库实现。如果插件的变化比较频繁,我们就可以使用Python来代替动态链接库形式的插件,这样可以方便地根据需求的变化改写脚本代码,提高灵活性。

多语言程序分析工具

就像上面 实现与性能 一节中看到的,多语言的使用为脚本语言带来了巨大的性能提升,然而同时也提高了编程复杂度,潜藏内存泄漏、悬空引用等一系列问题,也增加了系统性能分析的难度。下面列举一些可能有帮助的工具。

Intel Pin: 使用动态二进制插桩的程序分析工具。

Pungi: 静态分析Python的C扩展接口代码的引用计数错误。(未开源,不适用于C++扩展)

CPyChecker: 在扩展模块中检查一系列错误的gcc插件。

Intel SEAPI (ITT API): 生成和控制应用程序执行过程中的跟踪数据集合。

Python与C/C++互操作

评论关闭