Python与C/C++互操作
Posted humz
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python与C/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的参数转换,以及对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 cdll
libc = 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.py
from distutils.core import setup, Extension
MOD = ‘exfun‘
setup(name=MOD, ext_modules=[Extension(MOD, sources=[‘exfun.c‘])])
python3 setup.py build
python3 setup.py install
在Python中调用。
import exfun
print(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 cdll
import exfun
import timeit
def 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.78s
print(timeit.timeit("libc.fac(20)", setup="from __main__ import libc")) # 0.39s
print(timeit.timeit("exfun.fac(20)", setup="from __main__ import exfun")) # 0.16s
需要注意的是,由于Python3中int对长整型的支持,计算20!时原生Python得到的是正确的结果,而调用C的都溢出了。
其他方法
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++互操作的主要内容,如果未能解决你的问题,请参考以下文章
牛逼!用 AI 实现 C++JavaPython 代码互译!
使用自定义十进制原型合约(C#/C++ 互操作)时,Protobuf-net(反)序列化小数抛出