Linux中國

為 Python 寫一個 C++ 擴展模塊

使用 C 擴展Python 提供特定功能。

在前一篇文章中,我介紹了 六個 Python 解釋器。在大多數系統上,CPython 是默認的解釋器,而且根據民意調查顯示,它還是最流行的解釋器。Cpython 的獨有功能是使用擴展 API 用 C 語言編寫 Python 模塊。用 C 語言編寫 Python 模塊允許你將計算密集型代碼轉移到 C,同時保留 Python 的易用性。

在本文中,我將向你展示如何編寫一個 C++ 擴展模塊。使用 C++ 而不是 C,因為大多數編譯器通常都能理解這兩種語言。我必須提前說明缺點:以這種方式構建的 Python 模塊不能移植到其他解釋器中。它們只與 CPython 解釋器配合工作。因此,如果你正在尋找一種可移植性更好的與 C 語言模塊交互的方式,考慮下使用 ctypes 模塊。

源代碼

和往常一樣,你可以在 GitHub 上找到相關的源代碼。倉庫中的 C++ 文件有以下用途:

  • my_py_module.cpp: Python 模塊 MyModule 的定義
  • my_cpp_class.h: 一個頭文件 - 只有一個暴露給 Python 的 C++ 類
  • my_class_py_type.h/cpp: Python 形式的 C++ 類
  • pydbg.cpp: 用於調試的單獨應用程序

本文構建的 Python 模塊不會有任何實際用途,但它是一個很好的示例。

構建模塊

在查看源代碼之前,你可以檢查它是否能在你的系統上編譯。我使用 CMake 來創建構建的配置信息,因此你的系統上必須安裝 CMake。為了配置和構建這個模塊,可以讓 Python 去執行這個過程:

$ python3 setup.py build

或者手動執行:

$ cmake -B build
$ cmake --build build

之後,在 /build 子目錄下你會有一個名為 MyModule. so 的文件。

定義擴展模塊

首先,看一下 my_py_module.cpp 文件,尤其是 PyInit_MyModule 函數:

PyMODINIT_FUNC
PyInit_MyModule(void) {
    PyObject* module = PyModule_Create(&my_module);

    PyObject *myclass = PyType_FromSpec(&spec_myclass);
    if (myclass == NULL){
        return NULL;
    }
    Py_INCREF(myclass);

    if(PyModule_AddObject(module, "MyClass", myclass) < 0){
        Py_DECREF(myclass);
        Py_DECREF(module);
        return NULL;
    }
    return module;
}

這是本例中最重要的代碼,因為它是 CPython 的入口點。一般來說,當一個 Python C 擴展被編譯並作為共享對象二進位文件提供時,CPython 會在同名二進位文件中(<ModuleName>.so)搜索 PyInit_<ModuleName> 函數,並在試圖導入時執行它。

無論是聲明還是實例,所有 Python 類型都是 PyObject 的一個指針。在此函數的第一部分中,module 通過 PyModule_Create(...) 創建的。正如你在 module 詳述(my_py_module,同名文件)中看到的,它沒有任何特殊的功能。

之後,調用 PyType_FromSpec 為自定義類型 MyClass 創建一個 Python 堆類型 定義。一個堆類型對應於一個 Python 類,然後將它賦值給 MyModule 模塊。

注意,如果其中一個函數返回失敗,則必須減少以前創建的複製對象的引用計數,以便解釋器刪除它們。

指定 Python 類型

MyClass 詳述在 my_class_py_type.h 中可以找到,它作為 PyType_Spec 的一個實例:

static PyType_Spec spec_myclass = {
    "MyClass",                                  // name
    sizeof(MyClassObject) + sizeof(MyClass),    // basicsize
    0,                                          // itemsize
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,   // flags
    MyClass_slots                               // slots
};

它定義了一些基本類型信息,它的大小包括 Python 表示的大小(MyClassObject)和普通 C++ 類的大小(MyClass)。MyClassObject 定義如下:

typedef struct {
    PyObject_HEAD
    int         m_value;
    MyClass*    m_myclass;
} MyClassObject;

Python 表示的話就是 PyObject 類型,由 PyObject_HEAD 宏和其他一些成員定義。成員 m_value 視為普通類成員,而成員 m_myclass 只能在 C++ 代碼內部訪問。

PyType_Slot 定義了一些其他功能:

static PyType_Slot MyClass_slots[] = {
    {Py_tp_new,     (void*)MyClass_new},
    {Py_tp_init,    (void*)MyClass_init},
    {Py_tp_dealloc, (void*)MyClass_Dealloc},
    {Py_tp_members, MyClass_members},
    {Py_tp_methods, MyClass_methods},
    {0, 0} /* Sentinel */
};

在這裡,設置了一些初始化和析構函數的跳轉,還有普通的類方法和成員,還可以設置其他功能,如分配初始屬性字典,但這是可選的。這些定義通常以一個哨兵結束,包含 NULL 值。

要完成類型詳述,還包括下面的方法和成員表:

static PyMethodDef MyClass_methods[] = {
    {"addOne", (PyCFunction)MyClass_addOne, METH_NOARGS,  PyDoc_STR("Return an incrmented integer")},
    {NULL, NULL} /* Sentinel */
};

static struct PyMemberDef MyClass_members[] = {
    {"value", T_INT, offsetof(MyClassObject, m_value)},
    {NULL} /* Sentinel */
};

在方法表中,定義了 Python 方法 addOne,它指向相關的 C++ 函數 MyClass_addOne。它充當了一個包裝器,它在 C++ 類中調用 addOne() 方法。

在成員表中,只有一個為演示目的而定義的成員。不幸的是,在 PyMemberDef 中使用的 offsetof 不允許添加 C++ 類型到 MyClassObject。如果你試圖放置一些 C++ 類型的容器(如 std::optional),編譯器會抱怨一些內存布局相關的警告。

初始化和析構

MyClass_new 方法只為 MyClassObject 提供一些初始值,並為其類型分配內存:

PyObject *MyClass_new(PyTypeObject *type, PyObject *args, PyObject *kwds){
    std::cout << "MtClass_new() called!" << std::endl;

    MyClassObject *self;
    self = (MyClassObject*) type->tp_alloc(type, 0);
    if(self != NULL){ // -> 分配成功
        // 賦初始值
        self->m_value   = 0;
        self->m_myclass = NULL; 
    }
    return (PyObject*) self;
}

實際的初始化發生在 MyClass_init 中,它對應於 Python 中的 init() 方法:

int MyClass_init(PyObject *self, PyObject *args, PyObject *kwds){

    ((MyClassObject *)self)->m_value = 123;

    MyClassObject* m = (MyClassObject*)self;
    m->m_myclass = (MyClass*)PyObject_Malloc(sizeof(MyClass));

    if(!m->m_myclass){
        PyErr_SetString(PyExc_RuntimeError, "Memory allocation failed");
        return -1;
    }

    try {
        new (m->m_myclass) MyClass();
    } catch (const std::exception& ex) {
        PyObject_Free(m->m_myclass);
        m->m_myclass = NULL;
        m->m_value   = 0;
        PyErr_SetString(PyExc_RuntimeError, ex.what());
        return -1;
    } catch(...) {
        PyObject_Free(m->m_myclass);
        m->m_myclass = NULL;
        m->m_value   = 0;
        PyErr_SetString(PyExc_RuntimeError, "Initialization failed");
        return -1;
    }

    return 0;
}

如果你想在初始化過程中傳遞參數,必須在此時調用 PyArg_ParseTuple。簡單起見,本例將忽略初始化過程中傳遞的所有參數。在函數的第一部分中,PyObject 指針(self)被強轉為 MyClassObject 類型的指針,以便訪問其他成員。此外,還分配了 C++ 類的內存,並執行了構造函數。

注意,為了防止內存泄漏,必須仔細執行異常處理和內存分配(還有釋放)。當引用計數將為零時,MyClass_dealloc 函數負責釋放所有相關的堆內存。在文檔中有一個章節專門講述關於 C 和 C++ 擴展的內存管理。

包裝方法

從 Python 類中調用相關的 C++ 類方法很簡單:

PyObject* MyClass_addOne(PyObject *self, PyObject *args){
    assert(self);

    MyClassObject* _self = reinterpret_cast<MyClassObject*>(self);
    unsigned long val = _self->m_myclass->addOne();
    return PyLong_FromUnsignedLong(val);
}

同樣,PyObject 參數(self)被強轉為 MyClassObject 類型以便訪問 m_myclass,它指向 C++ 對應類實例的指針。有了這些信息,調用 addOne() 類方法,並且結果以 Python 整數對象 返回。

3 種方法調試

出於調試目的,在調試配置中編譯 CPython 解釋器是很有價值的。詳細描述參閱 官方文檔。只要下載了預安裝的解釋器的其他調試符號,就可以按照下面的步驟進行操作。

GNU 調試器

當然,老式的 GNU 調試器(GDB) 也可以派上用場。源碼中包含了一個 gdbinit 文件,定義了一些選項和斷點,另外還有一個 gdb.sh 腳本,它會創建一個調試構建並啟動一個 GDB 會話:

Gnu 調試器(GDB)對於 Python C 和 C++ 擴展非常有用

GDB 使用腳本文件 main.py 調用 CPython 解釋器,它允許你輕鬆定義你想要使用 Python 擴展模塊執行的所有操作。

C++ 應用

另一種方法是將 CPython 解釋器嵌入到一個單獨的 C++ 應用程序中。可以在倉庫的 pydbg.cpp 文件中找到:

int main(int argc, char *argv[], char *envp[])
{
    Py_SetProgramName(L"DbgPythonCppExtension");
    Py_Initialize();

    PyObject *pmodule = PyImport_ImportModule("MyModule");
    if (!pmodule) {
        PyErr_Print();
        std::cerr << "Failed to import module MyModule" << std::endl;
        return -1;
    }

    PyObject *myClassType = PyObject_GetAttrString(pmodule, "MyClass");
    if (!myClassType) {
        std::cerr << "Unable to get type MyClass from MyModule" << std::endl;
        return -1;
    }

    PyObject *myClassInstance = PyObject_CallObject(myClassType, NULL);

    if (!myClassInstance) {
        std::cerr << "Instantioation of MyClass failed" << std::endl;
        return -1;
    }

    Py_DecRef(myClassInstance); // invoke deallocation
    return 0;
}

使用 高級介面,可以導入擴展模塊並對其執行操作。它允許你在本地 IDE 環境中進行調試,還能讓你更好地控制傳遞或來自擴展模塊的變數。

缺點是創建一個額外的應用程序的成本很高。

VSCode 和 VSCodium LLDB 擴展

使用像 CodeLLDB 這樣的調試器擴展可能是最方便的調試選項。倉庫包含了一些 VSCode/VSCodium 的配置文件,用於構建擴展,如 task.jsonCMake Tools 和調用調試器(launch.json)。這種方法結合了前面幾種方法的優點:在圖形 IDE 中調試,在 Python 腳本文件中定義操作,甚至在解釋器提示符中動態定義操作。

VSCodium 有一個集成的調試器。

用 C++ 擴展 Python

Python 的所有功能也可以從 C 或 C++ 擴展中獲得。雖然用 Python 寫代碼通常認為是一件容易的事情,但用 C 或 C++ 擴展 Python 代碼是一件痛苦的事情。另一方面,雖然原生 Python 代碼比 C++ 慢,但 C 或 C++ 擴展可以將計算密集型任務提升到原生機器碼的速度。

你還必須考慮 ABI 的使用。穩定的 ABI 提供了一種方法來保持舊版本 CPython 的向後兼容性,如 文檔 所述。

最後,你必須自己權衡利弊。如果你決定使用 C 語言來擴展 Python 中的一些功能,你已經看到了如何實現它。

via: https://opensource.com/article/22/11/extend-c-python

作者:Stephan Avenwedde 選題:lkxed 譯者:MjSeven 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國