使用vscode+msvc为python编写c/c++扩展
博主近期因为项目中暴露的性能问题😥,需要为python编写一个c++的扩展,以达到提高运行效率的目的,故借此机会了解了利用python自带的C类库来编写C/C++扩展的具体实现方法,在此记录一下。
这里使用的python版本为3.10.2, 使用的是vs build tool 2022提供的msvc进行编写
一、配置vscode+msvc环境
本文使用的vscode和vs build tool 2022来实现环境的搭建,这里默认已经安装好了mscv以及vscode的c++相关插件。
首先新建一个文件夹,作为工作空间,并在其中新建一个source文件夹用于存放源代码,一个build文件夹用来存储编译之后的结果。
打开开始菜单,找到vs2022的文件夹,并选择对应版本的控制台打开。(选择X86还是X64版本的控制台取决于python的版本,这里务必保持和python一致。博主使用的是x64版本的python,故打开对应的x64版本控制台)在打开控制台之后,将路径切换到刚刚创建好的工作空间目录下,输入"code ."打开vscode。 在source文件夹下新建一个main.cpp文件,并写入一个简单的hello world程序,用于之后的测试 点击F5运行代码,会看到vscode出现弹窗。我们使用的是msvc,因此应该选择C++(windows) 之后的一个弹窗选择cl.exe 可以看到运行的结果,说明我们msvc配置成功了👍
由于本文涉及的内容并没有调试的过程,因此此处不再演示如何进行debug,若读者有这方面的需求,可以参考vscode官网的c++文档,里面有更为详细的过程👆
二、编写c++部分代码
此时可以看的工作空间的目录下,vscode自动生成了一个task.json配置文件(如下图所示) 其中args数组中是cl的配置项,目前是最基础的配置,如果需要编写python的扩展,那么还需要对其进行调整,这里我直接展示调整之后的task.json文件 这里介绍一下对应的修改项以及其意义
- /LD:编译动态链接库
- /Fe:编译链接之后的生成的文件名,其后面跟随编译所需要的源文件,vscode的默认配置只能编译一个文件,这里将其调整为可以编译多个文件,并修改了生成路径。
- /I:添加头文件依赖。我们需要借助python提供的python.h在进行编写扩展,所以这里需要把这个头文件所在的路径告知给cl(图中的为博主的路径,在使用时只需要替换为自己python头文件所在路径即可,下面的库文件路径同理)
- /link:链接器选项, 这个选项之后的配置都是针对link.exe的配置
- /MACHINE :将目标文件的类型调整为x64(x86同理,但一定保证和python的类型一样)
- /LIBPATH: python库文件所在的地址
将参数调整好之后,便可以开始进行编码了。这里附上python官方提供的原版教程以供参考。本文用一个简单的案例来演示,先上代码。
#include<Python.h>
#include<iostream>
int Add(int x, int y){
return x + y;
}
void Print(char* str){
std::cout<<str<<std::endl;
}
static PyObject* py_add(PyObject* self, PyObject* args){
int x, y;
if(!PyArg_ParseTuple(args, "ii", &x, &y))
return NULL;
return Py_BuildValue("i", Add(x, y));
}
static PyObject* py_print(PyObject* self, PyObject* args){
char* str;
if(!PyArg_ParseTuple(args, "s", &str))
return NULL;
Print(str);
Py_INCREF(Py_None);
return Py_None;
}
static PyMethodDef pymethods[] = {
{"my_add", py_add, METH_VARARGS, "加法"},
{"my_print", py_print, METH_VARARGS, "打印"},
{NULL, NULL, 0, NULL}
};
static PyModuleDef mymodule = {
PyModuleDef_HEAD_INIT,
"mymodule",
NULL,
-1,
pymethods
};
extern "C"
PyMODINIT_FUNC
PyInit_mymodule(void){
return PyModule_Create(&mymodule);
}
先简单写两个函数,一个实现加法,一个实现打印。为了让python可以识别,需要对它们使用Python.h提供的PyObject来封装。
每个返回PyObject的函数都有两个固定参数,self和args。函数中的PyArg_ParseTuple用来将python形式的参数映射为C类型的数据。函数的返回值可以使用Py_BuildValue进行封装,以转换成python可以识别的类型。这里需要注意的是,如果函数没有任何需要返回的值,那么需要让函数返回None,具体方式见py_print()函数。
在函数封装完毕后,将方法打包进PyMethodDef结构体中,这个结构体数组中每一项的四个值分别对应:
- python调用的接口名称
- 封装好的函数的函数名
- METH_VARARGS(参数对应模式,另外还有关键字对应方式,详情请查看python官网文档)
- 注释
将全部函数打包好之后,便可以开始打包模型了,也就是代码中的PyModuleDef,这里比较重要的是第二个值,这个值是模块的名称。
最后编写PyInit函数用来生成之前打包好的模组,此处的格式较为固定,需要注意的是PyInit_name()中,name是打包模组的名称,python在第一次执行我们打包的模块时会调用这个函数。在编写好全部代码后,使用快捷键"shift+ctrl+B"编译代码。
上述代码中有一点会出现问题,及python.h可能不能被识别到,此时需要添加c/c++语言配置文件,并将python.h所在的路径添加到其中的includePath中
三、Python调用动态链接库
在编译链接之后,build文件夹下应该会有若干文件生成,如下图
其中dll动态链接库就是打包好的C++扩展,不过python不能直接识别dll文件,需要先将其重命名为.pyd格式,再使用import进行引入。 下面调用模型的接口进行测试
import mymodule as md
md.my_print("hello world")
print(md.my_add(1, 1))
可以得到输出的结果,说明打包成功😊
本文为本人的学习笔记,如有问题欢迎指出和讨论@茱莉亚之歌
|