如何用C++ 写Python模块扩展(一)
最近做一个小软件需要用到虚拟摄像头,在网上找了找虚拟摄像头软件 发现 Vcam 软件有个API 可以用,有API当然是最好的啦,但是这个API只有C++和C#的。都说 “人生苦短,得用python”能用Python解决的事情尽量别用C++,于是萌生了自己写个模块的想法。
值得庆幸的是之前研究过一段时间C++。
先贴两个python官方文档链接
C API
第三方模块开发指南
开发环境准备
- 由于虚拟摄像头软件只有windows驱动 所以开发平台为Windows
- VS2013 (刚好系统里面有安装,据说水平好的可以直接用记事本写,我这种层次的还是用IDE 比较好不然没法玩了)
- Anaconda3 或官方Python 3.6 注意区分32位和64位版本 考虑到一些其他工具的兼容性 我使用的是32位版本Anaconda
- 注意编译32位dll时必须用32位版本python的库,64位必须用64位的库,不同版本Python库编译出来的dll可能不通用
工程配置
-
建立win32 DLL工程
![](file://C:\Users\Rex\Desktop\博客\dll.png) -
调整工程属性
- 由于本次使用的是32位Python所以直接将平台设置为win32
- 配置属性>>常规目标文件扩展名 设为.pyd 方便python直接调用
- 配置属性>>C++>>常规附加包含目录 将python安装目录下的include文件夹包含进去
- 配置属性>>连接器>>常规附加库目录 添加python安装目录下的libs目录
- 配置属性>>连接器>>输入附加依赖项 添加python.lib
-
头文件
-
写Python的C++扩展必须包含Python.h 和 structmember.h两个头文件
#include <windows.h> #include <iostream> #include <sstream> #include <Python.h> #include <structmember.h>
-
-
API文件导入略过
Python模块包含的类创建(上)
-
首先创建一个struct 用来存放类的各项属性.
struct IVCamRenderer; # 这个IVCamRenderer在VCam API文件里面有定义 这里重新声明下 typedef struct _VCam { PyObject_HEAD // 结构体的第一个元素必须是 PyObject_HEAD 宏 IBaseFilter * __vcam_renderer; //VCam类的第一个成员 IVCamRenderer * __my_vcam; //第二个成员 由于要处理图片用到了GDI+ 此属性用来存放 }VCam; static PyMemberDef VCam_DataMembers[] = { //类/结构的数据成员类说明 表. 根据官方文档说明此类表必须要要以一个元素全为NULL的数据结构结尾,后面还有一个Method 表也是如此 { "__vcam_renderer", T_OBJECT, offsetof(VCam, __vcam_renderer), 0, "The vcam_renderer of instance" }, { "__my_vcam", T_OBJECT, offsetof(VCam, __my_vcam), 0, "The vcam of instance." }, { NULL, NULL, NULL, 0, NULL } };
我们来看一下PyMemberDef 的定义
/* An array of PyMemberDef structures defines the name, type and offset of selected members of a C structure. These can be read by PyMember_GetOne() and set by PyMember_SetOne() (except if their READONLY flag is set). The array must be terminated with an entry whose name pointer is NULL. */ typedef struct PyMemberDef { char *name; // 在Python中显示的名称 int type; // 变量类型 Py_ssize_t offset; // offset 变量在前面为模块类定义的模块中的offset int flags; //读写权限标记 char *doc; //帮助文档内容 } PyMemberDef; /* Types */ #define T_SHORT 0 #define T_INT 1 #define T_LONG 2 #define T_FLOAT 3 #define T_DOUBLE 4 #define T_STRING 5 #define T_OBJECT 6 /* XXX the ordering here is weird for binary compatibility */ #define T_CHAR 7 /* 1-character string */ #define T_BYTE 8 /* 8-bit signed int */ /* unsigned variants: */ #define T_UBYTE 9 #define T_USHORT 10 #define T_UINT 11 #define T_ULONG 12 /* Added by Jack: strings contained in the structure */ #define T_STRING_INPLACE 13 /* Added by Lillo: bools contained in the structure (assumed char) */ #define T_BOOL 14 #define T_OBJECT_EX 16 /* Like T_OBJECT, but raises AttributeError when the value is NULL, instead of converting to None. */ #define T_LONGLONG 17 #define T_ULONGLONG 18 #define T_PYSSIZET 19 /* Py_ssize_t */ #define T_NONE 20 /* Value is always None */ /* Flags */ #define READONLY 1 #define READ_RESTRICTED 2 #define PY_WRITE_RESTRICTED 4 #define RESTRICTED (READ_RESTRICTED | PY_WRITE_RESTRICTED)
-
写两个函数用来处理python类初始化资源申请和和类析构时资源释放
初始化函数
static void VCam_init(VCam* Self, PyObject* pArgs) //构造方法. { Self->__vcam_renderer = nullptr; Self->__my_vcam = nullptr; HRESULT hr=::CoInitialize(nullptr); if (FAILED(hr = CoCreateInstance(CLSID_VCamRenderer, NULL, CLSCTX_INPROC, IID_IBaseFilter, reinterpret_cast<void**>(&(Self->__vcam_renderer))))) { PyErr_SetString(PyExc_OSError, "driver not installed!"); return; } // get [IVCamRender] interface from VCam Renderer filter if (FAILED(hr = Self->__vcam_renderer->QueryInterface(&(Self->__my_vcam)))) { PyErr_SetString(PyExc_OSError, "driver not installed!"); return; } }
请不要在意构造函数中一堆乱七八糟的代码 那些代码是VcamAPI初始化取对象的代码 正常简单点写就是假如类体内声明 一个 成员
XXType * instance;
构造时将其实例化一下申请一块内存
self->instance = new xxx;
析构函数
static void VCam_Destruct(VCam* Self) //析构方法. { if (Self->__my_vcam) Self->__my_vcam->SetConnectionNotificationEvent(reinterpret_cast<__int64>(nullptr)); if (Self->__vcam_renderer) Self->__vcam_renderer->Release(), Self->__vcam_renderer = nullptr; if (Self->__my_vcam) Self->__my_vcam->Release(), Self->__my_vcam = nullptr; Py_TYPE(Self)->tp_free((PyObject*)Self); //释放对象/实例. }
析构时候 shift键构造时候申请的内存防止内存泄漏即可
delete self->instance; self->instance = nullptr;
最后需要掉将Python对象释放
Py_TYPE(Self)->tp_free((PyObject*)Self); //释放对象/
-
写供Python调用的类中的各种方法
举例:写一个将虚拟摄像头显示 调整为镜像显示的方法
static PyObject* VCam_Mirror(VCam* Self, PyObject* Argvs) { Py_INCREF(Py_None); int mode=1; if (!PyArg_ParseTuple(Argvs, "|i", &mode)) { cout << "Parse the argument FAILED! You should pass correct values!" << endl; return Py_None; } Self->__my_vcam->SetMirror(mode); //Mirror the output video (0: no mirror, others: mirror), non-persistent. return Py_None; }
- 所有python方法返回值类型都必须为 PyObject*
- 对于传入的python类型参数需要用 PyArg_ParseTuple 或者 PyArg_VaParseTupleAndKeywords 等来解析成对应的C类型我这边只传入位置参数 所以用PyArg_ParseTuple 即可
- 对于解析函数中"|i"的解释:
- i表示转换格式为int型,其他各种格式具体见api参数说明
- 由于我定义此函数传入一个带默认值的位置参数"|"后面表示接的参数带有默认值,带有默认值的参数在解析前必须初始化一个值 具体见上面API
- 这个例子仅仅传入了一个参数,传入多个参数只需要在解析格式字符串中放入多个格式字符,后面用多个变量的引用去接收返回值,用来接收返回值的变量类型必须和格式声明一致,如"ssi" 表示传入三个参数参数类分别str,str,int 用来接收的C变量为char,char,int 且三个参数必须全部传入
- 函数返回值由于本次没有什么东西需要返回所以直接返回一个Py_None
- Py_INCREF(Py_None); 是干嘛用的?
由于CPython的内存管理机制特性 所有Python对象的引用都会有一个计数器,当计数器为0时CPython的垃圾回收机制就会将该对象的内存空间释放,在Python中引用对象Python会自动处理计数,但是在自己写的C代码里面直接对Python对象引用必须自行操作计数 引用前必须通过Py_INCREF 增加引用计数 再去使用对象 使用完后必须通过Py_DECREF 释放引用计数 否则可能造成程序崩溃或内存泄漏。
因为这里要调用一个Py_None 返回给Python 所以必须在调用前增加一个引用 由于这个Py_None对象直接返回给了Python python在用完以后会自行减掉计数,所以释放计数不需要自己来做,也不能自己做否则可能引起程序崩溃
其实上面写法是有问题的,开头直接申请了一个Py_None计数 若是后面这个Py_None没有被返回给Python且没有被释放那么这个Py_None在程序关闭前将永远占用一个内存,所以返回None能不能写的更加简单? 答案是肯定的 python 的头文件里面定义了一个宏 Py_RETURN_NONE 直接帮你处理了返回 Py_None 和引用计数问题。 - 返回其他类型数据
- 由于Python函数方法必须返回PyObject类型,所以函数返回值需要构造一个PyObject,构造完后还得做引用计数操作
- 好麻烦有没有 不过Python Api提供个一个Py_BuildValue函数 直接帮你处理好引用计数问题和Python对象创建问题
- Py_BuildValue 具体见api参数说明
看完引用计数问题感觉有点明白了 CPython 垃圾回收机制原理了有没有
- 参数解析的其他方法
PyArg_VaParseTupleAndKeywords 、PyArg_UnpackTuple 等用法详见手册
先写到这 后面再开一篇
如何用C++ 写Python模块扩展(二)