Python与C/C++互操作

Posted humz

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python与C/C++互操作相关的知识,希望对你有一定的参考价值。

Python调用C/C++

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

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

  2. 使用封装库的接口:比如官方的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 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的都溢出了。

其他方法

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++互操作的主要内容,如果未能解决你的问题,请参考以下文章

牛逼!用 AI 实现 C++JavaPython 代码互译!

C#/C++ 回调类(非函数)互操作 - 如何?

C++/C# 互操作中的内存映射和 P/Invoke 性能

使用自定义十进制原型合约(C#/C++ 互操作)时,Protobuf-net(反)序列化小数抛出

是否有用于 Octave 和 Scilab 的 C 类预处理器指令用于互兼容代码?

语言互操作性中的套接字编程