您好,登錄后才能下訂單哦!
今天小編給大家分享一下怎么為Python寫一個C++擴展模塊的相關知識點,內容詳細,邏輯清晰,相信大部分人都還太了解這方面的知識,所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。
和往常一樣,你可以在 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_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;}
這是本例中最重要的代碼,因為它是 CPython 的入口點。一般來說,當一個 Python C 擴展被編譯并作為共享對象二進制文件提供時,CPython 會在同名二進制文件中(
)搜索 PyInit_
函數,并在試圖導入時執行它。
無論是聲明還是實例,所有 Python 類型都是 PyObject 的一個指針。在此函數的第一部分中,module
通過 PyModule_Create(...)
創建的。正如你在 module
詳述(my_py_module
,同名文件)中看到的,它沒有任何特殊的功能。
之后,調用 PyType_FromSpec 為自定義類型 MyClass
創建一個 Python 堆類型 定義。一個堆類型對應于一個 Python 類,然后將它賦值給 MyModule
模塊。
注意,如果其中一個函數返回失敗,則必須減少以前創建的復制對象的引用計數,以便解釋器刪除它們。
MyClass
詳述在 my_class_py_type.h 中可以找到,它作為 PyType_Spec 的一個實例:
static PyType_Spec spec_myclass = {"MyClass",// namesizeof(MyClassObject) + sizeof(MyClass),// basicsize0,// itemsizePy_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, // flagsMyClass_slots // slots};
它定義了一些基本類型信息,它的大小包括 Python 表示的大小(MyClassObject
)和普通 C++ 類的大小(MyClass
)。MyClassObject
定義如下:
typedef struct {PyObject_HEADint 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(self);unsigned long val = _self->m_myclass->addOne();return PyLong_FromUnsignedLong(val);}
同樣,PyObject
參數(self
)被強轉為 MyClassObject
類型以便訪問 m_myclass
,它指向 C++ 對應類實例的指針。有了這些信息,調用 addOne()
類方法,并且結果以 Python 整數對象 返回。
出于調試目的,在調試配置中編譯 CPython 解釋器是很有價值的。詳細描述參閱 官方文檔。只要下載了預安裝的解釋器的其他調試符號,就可以按照下面的步驟進行操作。
當然,老式的 GNU 調試器(GDB) 也可以派上用場。源碼中包含了一個 gdbinit 文件,定義了一些選項和斷點,另外還有一個 gdb.sh 腳本,它會創建一個調試構建并啟動一個 GDB 會話:
Gnu 調試器(GDB)對于 Python C 和 C++ 擴展非常有用
GDB 使用腳本文件 main.py 調用 CPython 解釋器,它允許你輕松定義你想要使用 Python 擴展模塊執行的所有操作。
另一種方法是將 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 deallocationreturn 0;}
使用 高級接口,可以導入擴展模塊并對其執行操作。它允許你在本地 IDE 環境中進行調試,還能讓你更好地控制傳遞或來自擴展模塊的變量。
缺點是創建一個額外的應用程序的成本很高。
使用像 CodeLLDB 這樣的調試器擴展可能是最方便的調試選項。倉庫包含了一些 VSCode/VSCodium 的配置文件,用于構建擴展,如 task.json、CMake Tools 和調用調試器(launch.json)。這種方法結合了前面幾種方法的優點:在圖形 IDE 中調試,在 Python 腳本文件中定義操作,甚至在解釋器提示符中動態定義操作。
VSCodium 有一個集成的調試器。
Python 的所有功能也可以從 C 或 C++ 擴展中獲得。雖然用 Python 寫代碼通常認為是一件容易的事情,但用 C 或 C++ 擴展 Python 代碼是一件痛苦的事情。另一方面,雖然原生 Python 代碼比 C++ 慢,但 C 或 C++ 擴展可以將計算密集型任務提升到原生機器碼的速度。
你還必須考慮 ABI 的使用。穩定的 ABI 提供了一種方法來保持舊版本 CPython 的向后兼容性,如 文檔 所述。
以上就是“怎么為Python寫一個C++擴展模塊”這篇文章的所有內容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會為大家更新不同的知識,如果還想學習更多的知識,請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。