如何用C++ 寫Python模塊擴展(一)


最近做一個小軟件需要用到虛擬攝像頭,在網上找了找虛擬攝像頭軟件 發現 Vcam 軟件有個API 可以用,有API當然是最好的啦,但是這個API只有C++和C#的。都說 “人生苦短,得用python”能用Python解決的事情盡量別用C++,於是萌生了自己寫個模塊的想法。
值得慶幸的是之前研究過一段時間C++。
先貼兩個python官方文檔鏈接
C API
第三方模塊開發指南

開發環境准備

  1. 由於虛擬攝像頭軟件只有windows驅動 所以開發平台為Windows
  2. VS2013 (剛好系統里面有安裝,據說水平好的可以直接用記事本寫,我這種層次的還是用IDE 比較好不然沒法玩了)
  3. Anaconda3 或官方Python 3.6 注意區分32位和64位版本 考慮到一些其他工具的兼容性 我使用的是32位版本Anaconda
  4. 注意編譯32位dll時必須用32位版本python的庫,64位必須用64位的庫,不同版本Python庫編譯出來的dll可能不通用

工程配置

  1. 建立win32 DLL工程
    ![](file://C:\Users\Rex\Desktop\博客\dll.png)

  2. 調整工程屬性

    • 由於本次使用的是32位Python所以直接將平台設置為win32
    • 配置屬性>>常規目標文件擴展名 設為.pyd 方便python直接調用
    • 配置屬性>>C++>>常規附加包含目錄 將python安裝目錄下的include文件夾包含進去
    • 配置屬性>>連接器>>常規附加庫目錄 添加python安裝目錄下的libs目錄
    • 配置屬性>>連接器>>輸入附加依賴項 添加python.lib
  3. 頭文件

    • 寫Python的C++擴展必須包含Python.hstructmember.h兩個頭文件

        #include <windows.h>
        #include <iostream>
        #include <sstream>
        #include <Python.h>            
        #include <structmember.h>
      
  4. API文件導入略過

Python模塊包含的類創建(上)

  1. 首先創建一個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)
    
  2. 寫兩個函數用來處理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);                //釋放對象/
    
  3. 寫供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_VaParseTupleAndKeywordsPyArg_UnpackTuple 等用法詳見手冊

先寫到這 后面再開一篇
如何用C++ 寫Python模塊擴展(二)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM