溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

怎么為Python寫一個(gè)C++擴(kuò)展模塊

發(fā)布時(shí)間:2023-04-19 11:35:00 來源:億速云 閱讀:102 作者:iii 欄目:編程語言

今天小編給大家分享一下怎么為Python寫一個(gè)C++擴(kuò)展模塊的相關(guān)知識(shí)點(diǎn),內(nèi)容詳細(xì),邏輯清晰,相信大部分人都還太了解這方面的知識(shí),所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。

源代碼

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

  • my_py_module.cpp: Python 模塊MyModule 的定義

  • my_cpp_class.h: 一個(gè)頭文件 - 只有一個(gè)暴露給 Python 的 C++ 類

  • my_class_py_type.h/cpp: Python 形式的 C++ 類

  • pydbg.cpp: 用于調(diào)試的單獨(dú)應(yīng)用程序

本文構(gòu)建的 Python 模塊不會(huì)有任何實(shí)際用途,但它是一個(gè)很好的示例。

構(gòu)建模塊

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

$ python3 setup.py build

或者手動(dòng)執(zhí)行:

$ cmake -B build$ cmake --build build

之后,在 /build 子目錄下你會(huì)有一個(gè)名為 MyModule. so 的文件。

定義擴(kuò)展模塊

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

PyMODINIT_FUNCPyInit_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;}

這是本例中最重要的代碼,因?yàn)樗?CPython 的入口點(diǎn)。一般來說,當(dāng)一個(gè) Python C 擴(kuò)展被編譯并作為共享對(duì)象二進(jìn)制文件提供時(shí),CPython 會(huì)在同名二進(jìn)制文件中(.so)搜索 PyInit_ 函數(shù),并在試圖導(dǎo)入時(shí)執(zhí)行它。

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

之后,調(diào)用 PyType_FromSpec 為自定義類型 MyClass 創(chuàng)建一個(gè) Python 堆類型 定義。一個(gè)堆類型對(duì)應(yīng)于一個(gè) Python 類,然后將它賦值給 MyModule 模塊。

注意,如果其中一個(gè)函數(shù)返回失敗,則必須減少以前創(chuàng)建的復(fù)制對(duì)象的引用計(jì)數(shù),以便解釋器刪除它們。

指定 Python 類型

MyClass 詳述在 my_class_py_type.h 中可以找到,它作為 PyType_Spec 的一個(gè)實(shí)例:

static PyType_Spec spec_myclass = {"MyClass",// namesizeof(MyClassObject) + sizeof(MyClass),// basicsize0,// itemsizePy_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, // flagsMyClass_slots // slots};

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

typedef struct {PyObject_HEADint m_value;MyClass*m_myclass;} MyClassObject;

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

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 */};

在這里,設(shè)置了一些初始化和析構(gòu)函數(shù)的跳轉(zhuǎn),還有普通的類方法和成員,還可以設(shè)置其他功能,如分配初始屬性字典,但這是可選的。這些定義通常以一個(gè)哨兵結(jié)束,包含 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,它指向相關(guān)的 C++ 函數(shù) MyClass_addOne。它充當(dāng)了一個(gè)包裝器,它在 C++ 類中調(diào)用 addOne() 方法。

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

初始化和析構(gòu)

MyClass_new 方法只為 MyClassObject 提供一些初始值,并為其類型分配內(nèi)存:

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;}

實(shí)際的初始化發(fā)生在 MyClass_init 中,它對(duì)應(yīng)于 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;}

如果你想在初始化過程中傳遞參數(shù),必須在此時(shí)調(diào)用 PyArg_ParseTuple。簡(jiǎn)單起見,本例將忽略初始化過程中傳遞的所有參數(shù)。在函數(shù)的第一部分中,PyObject 指針(self)被強(qiáng)轉(zhuǎn)為 MyClassObject 類型的指針,以便訪問其他成員。此外,還分配了 C++ 類的內(nèi)存,并執(zhí)行了構(gòu)造函數(shù)。

注意,為了防止內(nèi)存泄漏,必須仔細(xì)執(zhí)行異常處理和內(nèi)存分配(還有釋放)。當(dāng)引用計(jì)數(shù)將為零時(shí),MyClass_dealloc 函數(shù)負(fù)責(zé)釋放所有相關(guān)的堆內(nèi)存。在文檔中有一個(gè)章節(jié)專門講述關(guān)于 C 和 C++ 擴(kuò)展的內(nèi)存管理。

包裝方法

從 Python 類中調(diào)用相關(guān)的 C++ 類方法很簡(jiǎn)單:

PyObject* MyClass_addOne(PyObject *self, PyObject *args){assert(self);MyClassObject* _self = reinterpret_cast(self);unsigned long val = _self->m_myclass->addOne();return PyLong_FromUnsignedLong(val);}

同樣,PyObject 參數(shù)(self)被強(qiáng)轉(zhuǎn)為 MyClassObject 類型以便訪問 m_myclass,它指向 C++ 對(duì)應(yīng)類實(shí)例的指針。有了這些信息,調(diào)用 addOne() 類方法,并且結(jié)果以 Python 整數(shù)對(duì)象 返回。

3 種方法調(diào)試

出于調(diào)試目的,在調(diào)試配置中編譯 CPython 解釋器是很有價(jià)值的。詳細(xì)描述參閱 官方文檔。只要下載了預(yù)安裝的解釋器的其他調(diào)試符號(hào),就可以按照下面的步驟進(jìn)行操作。

GNU 調(diào)試器

當(dāng)然,老式的 GNU 調(diào)試器(GDB) 也可以派上用場(chǎng)。源碼中包含了一個(gè) gdbinit 文件,定義了一些選項(xiàng)和斷點(diǎn),另外還有一個(gè) gdb.sh 腳本,它會(huì)創(chuàng)建一個(gè)調(diào)試構(gòu)建并啟動(dòng)一個(gè) GDB 會(huì)話:

怎么為Python寫一個(gè)C++擴(kuò)展模塊

Gnu 調(diào)試器(GDB)對(duì)于 Python C 和 C++ 擴(kuò)展非常有用

GDB 使用腳本文件 main.py 調(diào)用 CPython 解釋器,它允許你輕松定義你想要使用 Python 擴(kuò)展模塊執(zhí)行的所有操作。

C++ 應(yīng)用

另一種方法是將 CPython 解釋器嵌入到一個(gè)單獨(dú)的 C++ 應(yīng)用程序中。可以在倉庫的 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 deallocationreturn 0;}

使用 高級(jí)接口,可以導(dǎo)入擴(kuò)展模塊并對(duì)其執(zhí)行操作。它允許你在本地 IDE 環(huán)境中進(jìn)行調(diào)試,還能讓你更好地控制傳遞或來自擴(kuò)展模塊的變量。

缺點(diǎn)是創(chuàng)建一個(gè)額外的應(yīng)用程序的成本很高。

VSCode 和 VSCodium LLDB 擴(kuò)展

使用像 CodeLLDB 這樣的調(diào)試器擴(kuò)展可能是最方便的調(diào)試選項(xiàng)。倉庫包含了一些 VSCode/VSCodium 的配置文件,用于構(gòu)建擴(kuò)展,如 task.json、CMake Tools 和調(diào)用調(diào)試器(launch.json)。這種方法結(jié)合了前面幾種方法的優(yōu)點(diǎn):在圖形 IDE 中調(diào)試,在 Python 腳本文件中定義操作,甚至在解釋器提示符中動(dòng)態(tài)定義操作。

怎么為Python寫一個(gè)C++擴(kuò)展模塊

VSCodium 有一個(gè)集成的調(diào)試器。

用 C++ 擴(kuò)展 Python

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

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

以上就是“怎么為Python寫一個(gè)C++擴(kuò)展模塊”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會(huì)為大家更新不同的知識(shí),如果還想學(xué)習(xí)更多的知識(shí),請(qǐng)關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI