为Python扩展C/C++接口

前言:身为脚本语言的Python受到万千AI开发者的青睐,而C和C++以优秀的执行效率收到嵌入式工程师和游戏开发者的热爱,将两者结合就将更加无敌了呢🚀

本文主要分享将C++函数导出为Python扩展模块,供python调用,以同时具有py的便利性和C艹的高效性

DEMO

首先,可以想到最基础的文件至少有3个:

  • C++目标函数源文件
  • Python主程序脚本文件
  • 编译链接文件

首先来看看高效的C++源文件吧

childModule.cpp

先直接贴上代码😜别跑!说明就跟在后面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
/********必须先导入Py的头文件**********/
#define PY_SSIZE_T_CLEAN
#include <Python.h>

#include <iostream> //IO流头文件
#include <string> //字符串头文件

/** @brief 打印数据函数
* @note 这是原函数,可以是任意想被调用的接口
* @author 江榕煜(2020.9.4)
* @param age:年龄
* @param gender:性别
* @param glass:眼镜
* @return 执行结果状态代码
**/
int printStringFromPython(float age, bool gender,bool glass)
{
std::cout<<"input age:"<<age<<std::endl;

if(gender)
std::cout<<"boy"<<std::endl;
else
std::cout<<"girl"<<std::endl;

if(glass)
std::cout<<"with glass"<<std::endl;
else
std::cout<<"without glass."<<std::endl;

return 10010; //随便返回个啥吧hhh
}

/*******接下来开始导出到Python模块************/

/** @brief 封装原函数接口至python数据接口
* @note 函数形式需要类似
* @author 江榕煜(2020.9.4)
* @param self:模块对象指针
* @param args:python调用函数时传入的参数元组对象指针
* @return retval:将封装的原函数返回值转python对象后返回
**/
static PyObject * Export_printStringFromPython(PyObject * self,PyObject * args)
{
float year; //年龄
int gender; //性别
int glass; //眼镜

if(!PyArg_ParseTuple(args,"fii",&year,&gender,&glass)) //python中的参数元组转C变量
return NULL; //转换失败

//正式执行原函数,并且将返回值进行转换
PyObject * retval = (PyObject *)Py_BuildValue("i",printStringFromPython(year,gender,glass));

return retval; //返回python对象结果
}

/** @brief 该数组中给定最终导出的功能list
* @note 注意内容格式要满足PyMethodDef要求
* @author 江榕煜(2020.9.4)
**/
static PyMethodDef ExportMethods[]={
//{"导出的python接口名字","封装后的函数接口","参数格式","说明"}
{"printString2C",Export_printStringFromPython,METH_VARARGS,"Print something from python with C++"},
{NULL,NULL,0,NULL}
};

/** @brief 导出的自定义模块信息说明
* @note 无
* @author 江榕煜(2020.9.4)
**/
static struct PyModuleDef MyVoiceModule = {
PyModuleDef_HEAD_INIT, //开头必须是这个东西
"MyVoice", //模块名字
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. */
ExportMethods //导出功能List变量
};

/** @brief 模块初始化接口
* @note 该接口有命名要求的!
* @author 江榕煜(2020.9.4)
* @param none
* @return python模块初始化对象
**/
PyMODINIT_FUNC PyInit_MyVoice(void) //初始化函数必须命名为 PyInit_name()
{
return PyModule_Create(&MyVoiceModule); //初始化给定的模块对象
}

首先,在文件入口就是python头文件的声明,要注意,这两句必须要加在最前面!(官方理由是,放后面可能内置的类型会bug)

接着就是直接定义正常情况下的C++函数内容了

然后就到重点了,移植接口至自定义扩展python模块,笔者总结为了4步:

  • 函数IO接口对象转换封装
  • 函数接口属性基础配置
  • 自定义模块属性设置
  • 模块初始化

由于Python的数据结构和C++不一样(太显然了),所以要对参数接口进行转换,当python调用函数时,入参和回参都是PyObject类型,回头看看接口转换的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static PyObject * Export_printStringFromPython(PyObject * self,PyObject * args)
{
float year; //年龄
int gender; //性别
int glass; //眼镜

if(!PyArg_ParseTuple(args,"fii",&year,&gender,&glass)) //python中的参数元组转C变量
return NULL; //转换失败

//正式执行原函数,并且将返回值进行转换
PyObject * retval = (PyObject *)Py_BuildValue("i",printStringFromPython(year,gender,glass));

return retval; //返回python对象结果
}

比如现在要调用函数为:

1
MyVoice.printString2C(5.32,1,0)

那么传入的就是一个元组,内容为:(5.32,1,0)。则调用PyArg_ParseTuple将数据转换为C/C++数据,用法类似于scanf。第一个参数传入的就是PyObject*类型(参数元组args),第二个参数为转换的数据内容转换符,接着就是对应转换符的C数据变量指针。

内容转换符此处给出几个常用的类型,具体的转换符文末给出了官方参考链接

转换符 Python类型 C/C++类型
i 整型 int
l 整型 long int
f 浮点数 float
d 浮点数 double
p 任意python值 bool

当调用的接口不是纯执行类函数时,需要返回参数或者别的附加参数,则使用Py_BuildValue将需要的参数转换回PyObject类型,用法类似于print,使用的转换符同PyArg_ParseTuple。

接着只需要在初始化函数中把接口属性数组和模块属性变量初始化好,调用PyModule_Create生成模块数据文件。注意,初始化函数命名必须为PyMODINIT_FUNC PyInit_<moduleName>(void),在后续调用模块时,python解释器将自动调用该初始化函数得到模块对象。

接着是编译源文件为Python库

setup.py

该脚本文件使用python自带的distutils工具构建C/C++扩展包

1
2
3
4
5
6
7
8
9
10
11
from distutils.core import setup, Extension #导入distutils模块

##-------编译设置变量------------
module1 = Extension('MyVoice', #模块名字
sources = ['childModule.cpp']) #源文件

##----------编译-----------
setup (name = 'MyVoice', #模块名字
version = '1.0', #发行时的版本
description = 'This is a demo package', #发行时的说明
ext_modules = [module1]) #给实现定义好的编译设置变量

这一部分就直接参考笔者给定的注释修改后,和C源文件放置在一个目录下,运行

1
py setup.py build

会在目录下生成的build文件夹中生成相应的库

调用库

fatherApp.py

1
2
3
4
5
6
7
8
import sys

#导入指定路径模块
sys.path.append(r".\build\lib.win-amd64-3.7")
import MyVoice

#执行自定义模块的函数
MyVoice.printString2C(5.32,1,0)

测试结果

testRes

开发环境

  • 硬件平台:Intel-X64
  • 操作系统:Windows10-Pro
  • IDE:Visual Studio Code
  • Python解释器:V3.7.5
  • C++编译器:gcc version 8.1.0 (x86_64-posix-seh-rev0, Built by MinGW-W64 project)

参考文献

Donate
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.
  • Copyrights © 2022-2023 RY.J
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信