简介:Python 中调用 C/C++ 程序的方法有多种,这里简单介绍使用 C/C++ 编写 Python扩展供 Python 中使用的方法。相较于使用 ctypes 加载 C/C++ 程序编译的动态库进而调用函数,扩展模块的方式,从Python 中传入参数以及从 C/C++ 程序获取返回值的过程更为规范,或者说,更能够减少程序出现错误。使用 ctypes 调用程序,在多线程 Python 程序中容易产生段错误(segfault)。
C 扩展可以用来做两件不能直接在 Python 中做的事情: 构建新的内置对象类型、调用 C 库函数和系统调用。
为了支持扩展, Python 的 API 定义了许多函数、宏和变量,可以访问 Python 运行时系统的大部分内容。Python 的相关 API 可以通过在 C 文件中引入 “Python.h” 文件使用。
C 扩展接口依赖于 CPython ,扩展模块无法在其他 Python 实现上工作,因此可移植性较差。
如前所述,在 C++ 程序中使用 Python 的 API 需要在 C++ 程序中引入头文件 “Python.h”。而且由于 Python 可能会定义一些能在某些系统上影响标准头文件的预处理器定义,因此在包含任何标准头文件之前,必须先包含 Python.h。推荐总是在 Python.h 前定义 PY_SSIZE_T_CLEAN,实践证明,构建扩展后续是依赖这个宏定义的。因此 C++ 程序的文件头部应当如下所示:
#define PY_SSIZE_T_CLEAN
#include
自己定义的模块名称为 demo ,文件名称按照习惯可以命名为 demomodule.cpp,也可以不这么做,模块名的决定性因素为下边 PyModuleDef 类型的结构体。在模块 demo 下可以调用的函数,其函数名格式为 demo_funcname,如下 demo_myFunc 为 demo 模块下 myFunc 函数的定义。一个模块可以定义多个函数。
函数的参数列表形式固定,均为(PyObject *self, PyObject *args):
这里self与常用python类中定义函数的 self 类似,无需多看,这里的 args 则是函数接收的参数列表。
具体接收参数的内容,需要在函数定义内部 PyArg_ParseTuple 函数中确定。此函数负责解析在 Python 中调用模块中函数时接收的参数,将其转换为对应的 C 类型数据。
函数返回值:
定义的函数可以返回对象,也可以直接返回数据。
如果返回的是数据,则需要使用 Py_BuildValue 函数,将 C 类型的数据转换为 Python 中的数据类型。此函数的作用与上述函数 PyArg_ParseTuple 的作用相反。
如果返回的是对象,则意味这需要在此函数中构建一个对象,之后将对象返回给 Python,这时候需要注意给对象增加引用计数,否则会导致内存泄漏的问题。
参见这里的7-10
添加 C++ 函数到 扩展模块的源码如下:
static PyObject *demo_myFunc(PyObject *self, PyObject *args)
{
/*
* 这里是一些数据准备工作,用于接收从 Python 传递来的参数,给需要调用的 C++ 函数使用,这里的参数
* 接收需要一些参数解析
*/
unsigned char *cipher_text;
int cipher_text_length;
unsigned char *iv_salt_file_path;
unsigned char result[DEFAULT_RSA_KEY_LEN] = {0};
int len;
/*
* 对接收的参数进行解析,格式转换参见: https://docs.python.org/zh-cn/3/c-api/arg.html
* 这里的 s* 为接收 Python 中字符串或者 byte 类型的参数,将其解析为缓冲区中的内容, 这里的 i
* 为将 Python 中的整型解析为 C++ 中的整型
*/
if (!PyArg_ParseTuple(args, "s*is*", &cipher_text, &cipher_text_length, &iv_salt_file_path)){
return NULL;
}
/*
* 这里是对 C++ 函数的调用,不难发现,这里所使用的数据都是根据 Python 中传来的参数进行构建的,
* 而不是直接使用 Python 的那一份数据。由于 Python 只能管理自己的空间而不能管理其调用的外部函
* 数的空间,如果在外部函数直接使用原始的数据,可能导致在回收空间时出现问题,而且这种问题
* 很难发现。传入参数、返回值,都是根据一方函数中的构建,能够将 Python 与扩展中所使用空间良
* 好分离,避免互相影响。
*/
len = myFunc_c(cipher_text, cipher_text_length, iv_salt_file_path, result);
/* 如果这里的 myFunc_c 是 C++ 中的函数, C++ 函数还需要其他相关依赖,则也需要引入,可以通过引入头文件的方式 */
if (len < 0) {
PyErr_SetString(DemoError, "C function execution failed!");
}
/*
* 将 C++ 的 buf 中的内容解析为 Python 中的字符串类型,即根据 C++ 中的数据为 Python 调用构建返回值
* 注意这里返回的值是根据 C++ 程序中的数据进行构建,而不是从 Python 中直接传入一个参数,之后修改这个
* 参数后返回,这样内存回收时可能会有问题。
* 注意,如果这里返回的是一个对象,还需要注意引用计数相关的问题
*/
return Py_BuildValue("s", result);
}
模块中自定义方法的相关信息都需要在此结构中注册。
static PyMethodDef DemoMethods[]=
{
/*
* 模块中的函数、对应的函数名称。注意第三个参数 ( METH_VARARGS ) ,这个标志指定会使用 C 的调用惯例。
* 可选值有 METH_VARARGS 、 METH_VARARGS | METH_KEYWORDS 。值 0 代表使用 PyArg_ParseTuple() 的陈旧变量。
*/
{"myFunc", demo_myFunc, METH_VARARGS,"This function is used for test extension in python."},
{NULL,NULL, 0,NULL} /* 结构体固定格式结尾*/
};
上述模块方法表必须被模块定义结构所引用,对应此模块定义的最后一行,而且这个结构体必须传递给解释器的模块初始化函数。
static struct PyModuleDef demomodule = {
PyModuleDef_HEAD_INIT,
"demo", /* 扩展模块名 */
NULL, /* 扩展模块文档,可以为空 */
-1, /* size of per-interpreter state of the module, or -1 if the module keeps state in global variables. */
DemoMethods /* 2.3.1 中对应的模块方法表*/
};
此函数是初始化模块所用,扩展是 C++ 程序,为使 Python 能够调用此模块,编译之前需要使用 extern “C” 将其包裹起来,此函数名称格式为 PyInit_modulename,如这里的模块名称为 demo,对应的初始化函数名称为 PyInit_demo。
extern "C" PyMODINIT_FUNC PyInit_demo()
{
PyObject *m;
/*这里的引用是上述 2.3.2 中的模块定义结构体的引用 */
m = PyModule_Create(&demomodule);
if(m == NULL)
return NULL;
/* 可以自定义异常 */
DemoError = PyErr_NewException("demo.error", NULL, NULL);
Py_INCREF(DemoError);
PyModule_AddObject(m, "error", DemoError);
return m;
}
上述函数、结构体的相互依赖关系,决定了在实现扩展模块的 C++ 程序中各个功能结构实现的先后顺序。至此,扩展模块的实现就已经完成了。
扩展模块可以用 distutils 来构建,这是Python自带的。distutils 包含一个驱动脚本 setup.py,如下所示:
from distutils.core import setup, Extension
module1 = Extension('demo', # 扩展模块名称
sources = ['demomodule.cpp']) # 扩展模块对应的 C++ 文件
setup (name = 'PackageName', # 这里会影响到安装扩展时,在本地 Python 中的 egg-info 文件名称,可以自己定义
version = '1.0', # 扩展模块的版本号信息,可以自定义
description = 'This is a demo package', # 这是关于扩展的相关介绍
ext_modules = [module1]) # 这里的 module1 对应与上边的 module1
上述代码是一个简单版本的构建脚本,实际上,它可以还可以接受更多的参数。如通常我们在编译 C++ 程序时,需要通过 -I
和-L
参数指定编译所需要的一些头文件和库文件的路径,以及使用-l
指定需要链接的库(包括动态库和静态库),在 setup.py 脚本中,也需要指定这些参数(取决于使用的 C++ 程序是否需要)。同时,在 setup 函数中还可以指定更多信息,开发者可以根据需要添加,如下是一个包含更多信息的示例:
from distutils.core import setup, Extension
module1 = Extension('demo',
define_macros = [('MAJOR_VERSION', '1'), # 编译时设置一些宏定义
('MINOR_VERSION', '0')],
include_dirs = ['/usr/local/include'], # 指定需要引入的头文件位置
libraries = ['tcl83'], # 需要链接的库,比如加密程序需要的库为ssl、crypto(-l参数后的内容)
library_dirs = ['/usr/local/lib'], # 指定需要使用的库文件位置
sources = ['demomodule.cpp'])
setup (name = 'PackageName',
version = '1.0',
description = 'This is a demo package',
author = 'Martin v. Loewis', # 这里可以添加开发者相关的一些信息、网址以及详细的描述
author_email = '[email protected]',
url = 'https://docs.python.org/extending/building',
long_description = '''
This is really just a demo package.
''',
ext_modules = [module1])
到这里,我们已经准备好了扩展的实现代码—— C++ 程序,以及执行构建需要使用的脚本文件—— setup.py文件,接下来,执行构建命令即可:
python3 setup.py build
在首次构建时,能够发现,其编译过程实质上就是使用 g++ 对程序进行编译的过程,构建脚本在执行过程中,会添加很多相关的编译参数。
执行 build 之后,会发现当前目录下多了一个 build 文件夹,此文件夹之下还有两个子文件夹,分别存储编译过程中的中间文件(temp目录)和结果文件(lib目录)。temp 目录下的文件,就是 编译过程中 C++ 程序对应的 .o
文件,而 lib 目录下,则对应编译得到的库文件,其名称前缀为 demo.
。至此,我们编写的扩展就已经可以使用了。在 Python 程序中可以使用 import demo
,只要运行时 Python 程序可以找到此库文件的位置,程序即可正常运行。
但是此时 Python 程序时依赖此库文件位置的,如果程序找不到它,程序就会出错,显示找不到此模块。由于我们进行构建的 CI 环境时比较稳定的,很少有较大改动,因此,直接将此扩展安装至构建环境中的 Python 中,如同构建 CI 环境时,安装其他相关依赖一样,这样,就不用每次将构建的库文件引入到 has 服务的文件结构中。安装 Python 扩展至本地 Python 的命令如下:
python3 setup.py install
根据执行命令后屏幕的输出,很容易会发现,install
命令执行过程的前一部分和build
命令是相同的,包含了构建过程,构建完毕会将此库文件安装至本地的 Python 中。
至此,扩展就能够非常方便的在 Python 中使用了。
这里并不是想黑 ctypes,只是在最初使用的方案中,利用 ctypes 加载 C/C++ 程序编译的动态库文件,之后调用函数,并返回值,整个过程似乎是没有问题,但应该只是看起来没有问题而已。单个测试程序运行正常,Python 中能够调用函数,得到想要的效果。但是将其应用到 Python 多线程程序中,就会出现错误。查看 core 文件,最终出错地点是 Python 类中调用 C/C++ 函数相关实例回收的函数,出现了段错误,导致程序宕掉。
当时段时间内没有得到问题所在原因,就改为使用写 Python 扩展的形式。在学习和使用扩展的过程中,也逐渐意识到问题可能是什么原因导致的。
Python 程序和 C/C++ 程序在运行时,对于所需要的空间,各自开辟、各自回收。因此,二者在空间利用上最好不要有交集。使用 Python 扩展的形式这一点就很明确:扩展会通过 PyArg_ParseTuple 函数将 Python 中传递过来的参数解析,用 C/C++ 中的变量或者缓冲区接收,相当于对原数据进行一次拷贝,而不是直接在原数据上操作。在向 Python 返回数据时,不是直接返回 C/C++ 程序的结果,而是根据 C/C++ 程序中的结果构建 Python 数据(或者说是对象)。这样既完成了两种程序之间的数据通信,两种程序又不会互相干扰。