為 Python 寫一個(gè) C++ 擴(kuò)展模塊
使用 C 擴(kuò)展為 Python 提供特定功能。
在前一篇文章中,我介紹了 ??六個(gè) Python 解釋器??。在大多數(shù)系統(tǒng)上,CPython 是默認(rèn)的解釋器,而且根據(jù)民意調(diào)查顯示,它還是最流行的解釋器。Cpython 的獨(dú)有功能是使用擴(kuò)展 API 用 C 語言編寫 Python 模塊。用 C 語言編寫 Python 模塊允許你將計(jì)算密集型代碼轉(zhuǎn)移到 C,同時(shí)保留 Python 的易用性。
在本文中,我將向你展示如何編寫一個(gè) C++ 擴(kuò)展模塊。使用 C++ 而不是 C,因?yàn)榇蠖鄶?shù)編譯器通常都能理解這兩種語言。我必須提前說明缺點(diǎn):以這種方式構(gòu)建的 Python 模塊不能移植到其他解釋器中。它們只與 CPython 解釋器配合工作。因此,如果你正在尋找一種可移植性更好的與 C 語言模塊交互的方式,考慮下使用 ??ctypes?? 模塊。
源代碼
和往常一樣,你可以在 ??GitHub?? 上找到相關(guān)的源代碼。倉(cāng)庫(kù)中的 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è)過程:
或者手動(dòng)執(zhí)行:
之后,在 ??/build?
? 子目錄下你會(huì)有一個(gè)名為 ??MyModule. so?
? 的文件。
定義擴(kuò)展模塊
首先,看一下 ??my_py_module.cpp?
? 文件,尤其是 ??PyInit_MyModule?
? 函數(shù):
這是本例中最重要的代碼,因?yàn)樗?CPython 的入口點(diǎn)。一般來說,當(dāng)一個(gè) Python C 擴(kuò)展被編譯并作為共享對(duì)象二進(jìn)制文件提供時(shí),CPython 會(huì)在同名二進(jìn)制文件中(??<ModuleName>.so?
?)搜索 ??PyInit_<ModuleName>?
? 函數(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í)例:
它定義了一些基本類型信息,它的大小包括 Python 表示的大小(??MyClassObject?
?)和普通 C++ 類的大小(??MyClass?
?)。??MyClassObject?
? 定義如下:
Python 表示的話就是 ??PyObject?? 類型,由 ??PyObject_HEAD?
? 宏和其他一些成員定義。成員 ??m_value?
? 視為普通類成員,而成員 ??m_myclass?
? 只能在 C++ 代碼內(nèi)部訪問。
??PyType_Slot?? 定義了一些其他功能:
在這里,設(shè)置了一些初始化和析構(gòu)函數(shù)的跳轉(zhuǎn),還有普通的類方法和成員,還可以設(shè)置其他功能,如分配初始屬性字典,但這是可選的。這些定義通常以一個(gè)哨兵結(jié)束,包含 ??NULL?
? 值。
要完成類型詳述,還包括下面的方法和成員表:
在方法表中,定義了 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)存:
實(shí)際的初始化發(fā)生在 ??MyClass_init?
? 中,它對(duì)應(yīng)于 Python 中的 ??__init__()?? 方法:
如果你想在初始化過程中傳遞參數(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?
? 參數(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ì)話:
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)用程序中。可以在倉(cāng)庫(kù)的 ??pydbg.cpp?? 文件中找到:
使用 ??高級(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)。倉(cāng)庫(kù)包含了一些 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)定義操作。
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 的向后兼容性,如 ??文檔?? 所述。
最后,你必須自己權(quán)衡利弊。如果你決定使用 C 語言來擴(kuò)展 Python 中的一些功能,你已經(jīng)看到了如何實(shí)現(xiàn)它。