賬號為華為云開發(fā)者社區(qū)官方運營賬號,提供全面深入的云計算前景分析、豐富的技術(shù)干貨、程序樣例,分享華為云前沿資訊動態(tài)
本文分享自華為云社區(qū)《Python 綁定:從 Python 調(diào)用 C 或 C++ |【生長吧!Python!】》,原文作者:Yuchuan 。
您是擁有想要從 Python 中使用的C或 C++ 庫的 Python 開發(fā)人員嗎?如果是這樣,那么Python 綁定允許您調(diào)用函數(shù)并將數(shù)據(jù)從 Python 傳遞到C或C++,讓您利用這兩種語言的優(yōu)勢。在本教程中,您將看到一些可用于創(chuàng)建 Python 綁定的工具的概述。
在本教程中,您將了解:
本教程面向中級 Python 開發(fā)人員。它假定讀者具備 Python 的基本知識,并對 C 或 C++ 中的函數(shù)和數(shù)據(jù)類型有所了解。您可以通過單擊下面的鏈接獲取本教程中將看到的所有示例代碼:
在深入研究如何從 Python 調(diào)用 C之前,最好花一些時間了解為什么. 有幾種情況下,創(chuàng)建 Python 綁定來調(diào)用 C 庫是一個好主意:
以上所有都是學習創(chuàng)建 Python 綁定以與 C 庫交互的重要原因。
注意:在本教程中,您將創(chuàng)建到 C和C++ 的Python 綁定。大多數(shù)通用概念適用于兩種語言,因此除非兩種語言之間存在特定差異,否則將使用 C。通常,每個工具都支持 C或C++,但不能同時支持兩者。
讓我們開始吧!
等待!在開始編寫 Python 綁定之前,先看看 Python 和 C 如何存儲數(shù)據(jù)以及這會導致哪些類型的問題。首先,讓我們定義編組。這個概念由維基百科定義如下:
將對象的內(nèi)存表示轉(zhuǎn)換為適合存儲或傳輸?shù)臄?shù)據(jù)格式的過程。
出于您的目的,編組是 Python 綁定在準備數(shù)據(jù)以將其從 Python 移動到 C 或反之亦然時所做的工作。Python 綁定需要進行編組,因為 Python 和 C 以不同的方式存儲數(shù)據(jù)。C 在內(nèi)存中以最緊湊的形式存儲數(shù)據(jù)。如果您使用uint8_t,那么它總共將只使用 8 位內(nèi)存。
另一方面,在 Python 中,一切都是對象。這意味著每個整數(shù)在內(nèi)存中使用幾個字節(jié)。多少取決于您運行的 Python 版本、操作系統(tǒng)和其他因素。這意味著您的 Python 綁定將需要為每個跨邊界傳遞的整數(shù)將C 整數(shù)轉(zhuǎn)換為Python 整數(shù)。
其他數(shù)據(jù)類型在這兩種語言之間具有相似的關系。讓我們依次來看看:
除了數(shù)據(jù)類型轉(zhuǎn)換之外,在構(gòu)建 Python 綁定時還需要考慮其他問題。讓我們繼續(xù)探索它們。
除了所有這些數(shù)據(jù)類型之外,您還必須了解 Python 對象如何可變或不可變。當談到傳值或傳引用時,C 有一個類似的函數(shù)參數(shù)概念。在 C 中,所有參數(shù)都是按值傳遞的。如果要允許函數(shù)更改調(diào)用方中的變量,則需要傳遞指向該變量的指針。
您可能想知道是否可以通過使用指針將不可變對象簡單地傳遞給 C 來繞過不可變限制。除非你走到丑陋和不可移植的極端,否則Python 不會給你一個指向 object 的指針,所以這行不通。如果您想用 C 修改 Python 對象,那么您需要采取額外的步驟來實現(xiàn)這一點。這些步驟將取決于您使用的工具,如下所示。
因此,您可以將不變性添加到您創(chuàng)建 Python 綁定時要考慮的項目清單中。在創(chuàng)建此清單的宏偉之旅中,您的最后一站是如何處理 Python 和 C 處理內(nèi)存管理的不同方式。
C 和 Python管理內(nèi)存的方式不同。在 C 中,開發(fā)人員必須管理所有內(nèi)存分配并確保它們被釋放一次且僅一次。Python 使用垃圾收集器為您處理這個問題。
雖然這些方法中的每一種都有其優(yōu)點,但它確實為創(chuàng)建 Python 綁定添加了額外的麻煩。您需要知道每個對象的內(nèi)存分配在哪里,并確保它只在語言障礙的同一側(cè)被釋放。
例如,當您設置x=3. 用于此的內(nèi)存在 Python 端分配,需要進行垃圾收集。幸運的是,使用 Python 對象,很難做任何其他事情。看看 C 中的逆向,直接分配一塊內(nèi)存:
int* iPtr=(int*)malloc(sizeof(int));
執(zhí)行此操作時,您需要確保在 C 中釋放此指針。這可能意味著手動將代碼添加到 Python 綁定中以執(zhí)行此操作。
這完善了您的一般主題清單。讓我們開始設置您的系統(tǒng),以便您可以編寫一些代碼!
在本教程中,您將使用來自 Real Python GitHub 存儲庫的預先存在的 C 和 C++ 庫來展示每個工具的測試。目的是您將能夠?qū)⑦@些想法用于任何 C 庫。要遵循此處的所有示例,您需要具備以下條件:
最后一個對你來說可能是新的,所以讓我們仔細看看它。
invoke是您將在本教程中用于構(gòu)建和測試 Python 綁定的工具。它具有類似的目的,make但使用 Python 而不是 Makefiles。您需要invoke使用pip以下命令在虛擬環(huán)境中安裝:
$ python3 -m pip install invoke
要運行它,請鍵入invoke后跟要執(zhí)行的任務:
$ invoke build-cmult===================================================Building C Library
* Complete
要查看哪些任務可用,請使用以下--list選項:
$ invoke --list
Available tasks:
all Build and run all tests
build-cffi Build the CFFI Python bindings
build-cmult Build the shared library for the sample C code
build-cppmult Build the shared library for the sample C++ code
build-cython Build the cython extension module
build-pybind11 Build the pybind11 wrapper library
clean Remove any built objects
test-cffi Run the script to test CFFI
test-ctypes Run the script to test ctypes
test-cython Run the script to test Cython
test-pybind11 Run the script to test PyBind11
請注意,當您查看定義任務的tasks.py文件時invoke,您會看到列出的第二個任務的名稱是build_cffi. 但是,來自的輸出將其--list顯示為build-cffi. 減號 ( -) 不能用作 Python 名稱的一部分,因此該文件使用下劃線 ( _) 代替。
對于您將檢查的每個工具,都會定義一個build-和一個test-任務。例如,要運行 的代碼CFFI,您可以鍵入invoke build-cffi test-cffi。一個例外是ctypes,因為 沒有構(gòu)建階段ctypes。此外,為了方便,還添加了兩個特殊任務:
既然您已經(jīng)對如何運行代碼有所了解,那么在查看工具概述之前,讓我們先看一下您將要包裝的 C 代碼。
在下面的每個示例部分中,您將為C 或 C++ 中的相同函數(shù)創(chuàng)建 Python 綁定。這些部分旨在讓您體驗每種方法的外觀,而不是有關該工具的深入教程,因此您將封裝的函數(shù)很小。您將為其創(chuàng)建 Python 綁定的函數(shù)將 anint和 afloat作為輸入?yún)?shù)并返回一個float是兩個數(shù)字的乘積:
// cmult.c
float cmult(int int_param, float float_param) {
float return_value=int_param * float_param;
printf(" In cmult : int: %d float %.1f returning %.1f\n", int_param,
float_param, return_value);
return return_value;
}
C 和 C++ 函數(shù)幾乎相同,它們之間的名稱和字符串略有不同。您可以通過單擊以下鏈接獲取所有代碼的副本:
現(xiàn)在您已經(jīng)克隆了 repo 并安裝了工具,您可以構(gòu)建和測試這些工具。因此,讓我們深入了解下面的每個部分!
您將從 開始ctypes,它是標準庫中用于創(chuàng)建 Python 綁定的工具。它提供了一個低級工具集,用于在 Python 和 C 之間加載共享庫和編組數(shù)據(jù)。
的一大優(yōu)點ctypes是它是 Python 標準庫的一部分。它是在 Python 2.5 版中添加的,因此您很可能已經(jīng)擁有它。您可以import像使用sys或time模塊一樣。
加載 C 庫和調(diào)用函數(shù)的所有代碼都將在 Python 程序中。這很棒,因為您的過程中沒有額外的步驟。您只需運行您的程序,一切都會得到處理。要在 中創(chuàng)建 Python 綁定ctypes,您需要執(zhí)行以下步驟:
您將依次查看其中的每一個。
ctypes為您提供了多種加載共享庫的方法,其中一些是特定于平臺的。對于您的示例,您將ctypes.CDLL通過傳入所需共享庫的完整路徑來直接創(chuàng)建對象:
# ctypes_test.py
import ctypes
import pathlib
if __name__=="__main__":
# Load the shared library into ctypes
libname=pathlib.Path().absolute() / "libcmult.so"
c_lib=ctypes.CDLL(libname)
這適用于共享庫與 Python 腳本位于同一目錄中的情況,但在嘗試加載來自 Python 綁定以外的包的庫時要小心。在ctypes特定于平臺和特定情況的文檔中,有許多關于加載庫和查找路徑的詳細信息。
注意:在庫加載過程中可能會出現(xiàn)許多特定于平臺的問題。最好在示例工作后進行增量更改。
現(xiàn)在您已將庫加載到 Python 中,您可以嘗試調(diào)用它!
請記住,您的 C 函數(shù)的函數(shù)原型如下:
// cmult.h
float cmult(int int_param, float float_param);
您需要傳入一個整數(shù)和一個浮點數(shù),并且可以期望得到一個浮點數(shù)返回。整數(shù)和浮點數(shù)在 Python 和 C 中都有本機支持,因此您希望這種情況適用于合理的值。
將庫加載到 Python 綁定中后,該函數(shù)將成為 的屬性c_lib,即CDLL您之前創(chuàng)建的對象。您可以嘗試這樣稱呼它:
x, y=6, 2.3
answer=c_lib.cmult(x, y)
哎呀!這不起作用。此行在示例 repo 中被注釋掉,因為它失敗了。如果您嘗試使用該調(diào)用運行,那么 Python 會報錯:
$ invoke test-ctypes
Traceback (most recent call last):
File "ctypes_test.py", line 16, in <module>
answer=c_lib.cmult(x, y)
ctypes.ArgumentError: argument 2: <class 'TypeError'>: Don't know how to convert parameter 2
看起來您需要說明ctypes任何不是整數(shù)的參數(shù)。ctypes除非您明確告訴它,否則您對該函數(shù)一無所知。任何未以其他方式標記的參數(shù)都假定為整數(shù)。ctypes不知道如何將2.3存儲的值轉(zhuǎn)換為y整數(shù),所以它失敗了。
要解決此問題,您需要c_float從號碼中創(chuàng)建一個。您可以在調(diào)用函數(shù)的行中執(zhí)行此操作:
# ctypes_test.py
answer=c_lib.cmult(x, ctypes.c_float(y))
print(f" In Python: int: {x} float {y:.1f} return val {answer:.1f}")
現(xiàn)在,當您運行此代碼時,它會返回您傳入的兩個數(shù)字的乘積:
$ invoke test-ctypes
In cmult : int: 6 float 2.3 returning 13.8
In Python: int: 6 float 2.3 return val 48.0
等一下……6乘以2.3不是48.0!
事實證明,就像輸入?yún)?shù)一樣,ctypes 假設您的函數(shù)返回一個int. 實際上,您的函數(shù)返回 a float,它被錯誤地編組。就像輸入?yún)?shù)一樣,您需要告訴ctypes使用不同的類型。這里的語法略有不同:
# ctypes_test.py
c_lib.cmult.restype=ctypes.c_float
answer=c_lib.cmult(x, ctypes.c_float(y))
print(f" In Python: int: {x} float {y:.1f} return val {answer:.1f}")
這應該夠了吧。讓我們運行整個test-ctypes目標,看看你有什么。請記住,輸出的第一部分是在restype將函數(shù)固定為浮點數(shù)之前:
$ invoke test-ctypes===================================================Building C Library
* Complete===================================================Testing ctypes Module
In cmult : int: 6 float 2.3 returning 13.8
In Python: int: 6 float 2.3 return val 48.0
In cmult : int: 6 float 2.3 returning 13.8
In Python: int: 6 float 2.3 return val 13.8
這樣更好!雖然第一個未更正的版本返回錯誤的值,但您的固定版本與 C 函數(shù)一致。C 和 Python 都得到相同的結(jié)果!現(xiàn)在它可以工作了,看看為什么您可能想或不想使用ctypes.
ctypes與您將在此處檢查的其他工具相比,最大的優(yōu)勢在于它內(nèi)置于標準庫中。它還不需要額外的步驟,因為所有工作都是作為 Python 程序的一部分完成的。
此外,所使用的概念是低級的,這使得像您剛剛做的那樣的練習易于管理。然而,由于缺乏自動化,更復雜的任務變得繁瑣。在下一部分中,您將看到一個工具,該工具為流程添加了一些自動化。
CFFI是Python的C 外來函數(shù)接口。生成 Python 綁定需要更自動化的方法。CFFI有多種方式可以構(gòu)建和使用 Python 綁定。有兩種不同的選項可供選擇,為您提供四種可能的模式:
對于此示例,您將使用 API 外聯(lián)模式,它生成最快的代碼,并且通常看起來類似于您將在本教程后面創(chuàng)建的其他 Python 綁定。
由于CFFI不是標準庫的一部分,您需要在您的機器上安裝它。建議您為此創(chuàng)建一個虛擬環(huán)境。幸運的是,CFFI安裝有pip:
$ python3 -m pip install cffi
這會將軟件包安裝到您的虛擬環(huán)境中。如果您已經(jīng)從 安裝requirements.txt,那么應該注意這一點。您可以requirements.txt通過訪問以下鏈接中的 repo 來查看:
獲取示例代碼: 單擊此處獲取您將用于在本教程中了解 Python 綁定的示例代碼。
現(xiàn)在你已經(jīng)CFFI安裝好了,是時候試一試了!
與 不同的是ctypes,CFFI您正在創(chuàng)建一個完整的 Python 模塊。您將能夠import像標準庫中的任何其他模塊一樣使用該模塊。您需要做一些額外的工作來構(gòu)建 Python 模塊。要使用CFFIPython 綁定,您需要執(zhí)行以下步驟:
這可能看起來需要做很多工作,但您將完成這些步驟中的每一個,并了解它是如何工作的。
CFFI提供讀取C 頭文件的方法,以在生成 Python 綁定時完成大部分工作。在 的文檔中CFFI,執(zhí)行此操作的代碼放置在單獨的 Python 文件中。對于此示例,您將直接將該代碼放入構(gòu)建工具中invoke,該工具使用 Python 文件作為輸入。要使用CFFI,您首先要創(chuàng)建一個cffi.FFI對象,該對象提供了您需要的三種方法:
# tasks.py
import cffi
...
""" Build the CFFI Python bindings """
print_banner("Building CFFI Module")
ffi=cffi.FFI()
擁有 FFI 后,您將使用.cdef()來自動處理頭文件的內(nèi)容。這會為您創(chuàng)建包裝函數(shù)以從 Python 封送數(shù)據(jù):
# tasks.py
this_dir=pathlib.Path().absolute()
h_file_name=this_dir / "cmult.h"
with open(h_file_name) as h_file:
ffi.cdef(h_file.read())
讀取和處理頭文件是第一步。之后,您需要使用.set_source()來描述CFFI將生成的源文件:
# tasks.py
ffi.set_source(
"cffi_example",
# Since you're calling a fully-built library directly, no custom source
# is necessary. You need to include the .h files, though, because behind
# the scenes cffi generates a .c file that contains a Python-friendly
# wrapper around each of the functions.
'#include "cmult.h"',
# The important thing is to include the pre-built lib in the list of
# libraries you're linking against:
libraries=["cmult"],
library_dirs=[this_dir.as_posix()],
extra_link_args=["-Wl,-rpath,."],
)
以下是您傳入的參數(shù)的細分:
調(diào)用.set_source()不會構(gòu)建 Python 綁定。它只設置元數(shù)據(jù)來描述將生成的內(nèi)容。要構(gòu)建 Python 綁定,您需要調(diào)用.compile():
# tasks.py
ffi.compile()
這通過生成.c文件、.o文件和共享庫來完成。在invoke你剛走通過任務可以在上運行命令行構(gòu)建Python綁定:
$ invoke build-cffi===================================================Building C Library
* Complete===================================================Building CFFI Module
* Complete
你有你的CFFIPython 綁定,所以是時候運行這段代碼了!
在您為配置和運行CFFI編譯器所做的所有工作之后,使用生成的 Python 綁定看起來就像使用任何其他 Python 模塊一樣:
# cffi_test.py
import cffi_example
if __name__=="__main__":
# Sample data for your call
x, y=6, 2.3
answer=cffi_example.lib.cmult(x, y)
print(f" In Python: int: {x} float {y:.1f} return val {answer:.1f}")
你導入新模塊,然后就可以cmult()直接調(diào)用了。要對其進行測試,請使用以下test-cffi任務:
$ invoke test-cffi===================================================Testing CFFI Module
In cmult : int: 6 float 2.3 returning 13.8
In Python: int: 6 float 2.3 return val 13.8
這將運行您的cffi_test.py程序,該程序會測試您使用CFFI. 關于編寫和使用CFFIPython 綁定的部分到此結(jié)束。
ctypes與CFFI您剛剛看到的示例相比,這似乎需要更少的工作。雖然這對于這個用例來說是正確的,CFFI但與ctypes由于大部分功能包裝的自動化相比,它可以更好地擴展到更大的項目。
CFFI也產(chǎn)生了完全不同的用戶體驗。ctypes允許您將預先存在的 C 庫直接加載到您的 Python 程序中。CFFI,另一方面,創(chuàng)建一個可以像其他 Python 模塊一樣加載的新 Python 模塊。
更重要的是,使用上面使用的外部 API方法,創(chuàng)建 Python 綁定的時間損失在您構(gòu)建它時完成一次,并且不會在每次運行代碼時發(fā)生。對于小程序來說,這可能不是什么大問題,但也可以通過CFFI這種方式更好地擴展到更大的項目。
就像ctypes, usingCFFI只允許您直接與 C 庫交互。C++ 庫需要大量的工作才能使用。在下一節(jié)中,您將看到一個專注于 C++ 的 Python 綁定工具。
PyBind11使用完全不同的方法來創(chuàng)建 Python 綁定。除了將重點從 C 轉(zhuǎn)移到 C++ 之外,它還使用 C++ 來指定和構(gòu)建模塊,使其能夠利用 C++ 中的元編程工具。像 一樣CFFI,生成的 Python 綁定PyBind11是一個完整的 Python 模塊,可以直接導入和使用。
PyBind11以Boost::Python庫為藍本并具有類似的界面。但是,它將其使用限制為 C++11 和更新版本,與支持所有內(nèi)容的 Boost 相比,這使其能夠簡化和加快處理速度。
文檔的“第一步”部分將PyBind11引導您了解如何下載和構(gòu)建PyBind11. 雖然這似乎不是嚴格要求,但完成這些步驟將確保您設置了正確的 C++ 和 Python 工具。
注:大部分示例PyBind11使用cmake,是構(gòu)建 C 和 C++ 項目的好工具。但是,對于此演示,您將繼續(xù)使用該invoke工具,該工具遵循文檔的手動構(gòu)建部分中的說明。
您需要將此工具安裝到您的虛擬環(huán)境中:
$ python3 -m pip install pybind11
PyBind11是一個全頭庫,類似于 Boost 的大部分內(nèi)容。這允許pip將庫的實際 C++ 源代碼直接安裝到您的虛擬環(huán)境中。
在您深入研究之前,請注意您使用的是不同的 C++ 源文件, cppmult.cpp,而不是您用于前面示例的 C 文件。兩種語言的功能基本相同。
與 類似CFFI,您需要創(chuàng)建一些代碼來告訴該工具如何構(gòu)建您的 Python 綁定。與 不同CFFI,此代碼將使用 C++ 而不是 Python。幸運的是,只需要很少的代碼:
// pybind11_wrapper.cpp
#include <pybind11/pybind11.h>
#include <cppmult.hpp>
PYBIND11_MODULE(pybind11_example, m) {
m.doc()="pybind11 example plugin"; // Optional module docstring
m.def("cpp_function", &cppmult, "A function that multiplies two numbers");
}
讓我們一次一個地看,因為PyBind11將大量信息打包成幾行。
前兩行包括pybind11.hC++ 庫的文件和頭文件cppmult.hpp. 之后,你就有了PYBIND11_MODULE宏。這將擴展為PyBind11源代碼中詳細描述的 C++ 代碼塊:
此宏創(chuàng)建入口點,當 Python 解釋器導入擴展模塊時將調(diào)用該入口點。模塊名稱作為第一個參數(shù)給出,不應用引號引起來。第二個宏參數(shù)定義了一個py::module可用于初始化模塊的類型變量。(來源)
這對您來說意味著,在本例中,您正在創(chuàng)建一個名為的模塊pybind11_example,其余代碼將m用作py::module對象的名稱。在下一行,在您定義的 C++ 函數(shù)中,您為模塊創(chuàng)建一個文檔字符串。雖然這是可選的,但讓您的模塊更加Pythonic是一個不錯的選擇。
最后,你有m.def()電話。這將定義一個由您的新 Python 綁定導出的函數(shù),這意味著它將在 Python 中可見。在此示例中,您將傳遞三個參數(shù):
現(xiàn)在您已經(jīng)有了 Python 綁定的代碼,接下來看看如何將其構(gòu)建到 Python 模塊中。
用于構(gòu)建 Python 綁定的工具PyBind11是 C++ 編譯器本身。您可能需要修改編譯器和操作系統(tǒng)的默認值。
首先,您必須構(gòu)建要為其創(chuàng)建綁定的 C++ 庫。對于這么小的示例,您可以將cppmult庫直接構(gòu)建到 Python 綁定庫中。但是,對于大多數(shù)實際示例,您將有一個要包裝的預先存在的庫,因此您將cppmult單獨構(gòu)建該庫。構(gòu)建是對編譯器的標準調(diào)用以構(gòu)建共享庫:
# tasks.py
invoke.run(
"g++ -O3 -Wall -Werror -shared -std=c++11 -fPIC cppmult.cpp "
"-o libcppmult.so "
)
運行這個invoke build-cppmult產(chǎn)生libcppmult.so:
$ invoke build-cppmult===================================================Building C++ Library
* Complete
另一方面,Python 綁定的構(gòu)建需要一些特殊的細節(jié):
1# tasks.py
2invoke.run(
3 "g++ -O3 -Wall -Werror -shared -std=c++11 -fPIC "
4 "`python3 -m pybind11 --includes` "
5 "-I /usr/include/python3.7 -I . "
6 "{0} "
7 "-o {1}`python3.7-config --extension-suffix` "
8 "-L. -lcppmult -Wl,-rpath,.".format(cpp_name, extension_name)
9)
讓我們逐行瀏覽一下。第 3 行包含相當標準的 C++ 編譯器標志,指示幾個細節(jié),包括您希望捕獲所有警告并將其視為錯誤、您需要共享庫以及您使用的是 C++11。
第 4 行是魔法的第一步。它調(diào)用pybind11模塊使其include為PyBind11. 您可以直接在控制臺上運行此命令以查看它的作用:
$ python3 -m pybind11 --includes
-I/home/jima/.virtualenvs/realpython/include/python3.7m
-I/home/jima/.virtualenvs/realpython/include/site/python3.7
您的輸出應該相似但顯示不同的路徑。
在編譯調(diào)用的第 5 行,您可以看到您還添加了 Python dev 的路徑includes。雖然建議您不要鏈接 Python 庫本身,但源代碼需要一些代碼Python.h才能發(fā)揮其魔力。幸運的是,它使用的代碼在 Python 版本中相當穩(wěn)定。
第 5 行還用于-I .將當前目錄添加到include路徑列表中。這允許#include <cppmult.hpp>解析包裝器代碼中的行。
第 6 行指定源文件的名稱,即pybind11_wrapper.cpp. 然后,在第 7 行,您會看到更多的構(gòu)建魔法正在發(fā)生。此行指定輸出文件的名稱。Python 在模塊命名上有一些特別的想法,包括 Python 版本、機器架構(gòu)和其他細節(jié)。Python 還提供了一個工具來幫助解決這個問題python3.7-config:
$ python3.7-config --extension-suffix
.cpython-37m-x86_64-linux-gnu.so
如果您使用的是不同版本的 Python,則可能需要修改該命令。如果您使用不同版本的 Python 或在不同的操作系統(tǒng)上,您的結(jié)果可能會發(fā)生變化。
構(gòu)建命令的最后一行,第 8 行,將鏈接器指向libcppmult您之前構(gòu)建的庫。該rpath部分告訴鏈接器向共享庫添加信息以幫助操作系統(tǒng)libcppmult在運行時查找。最后,您會注意到此字符串的格式為cpp_name和extension_name。Cython在下一節(jié)中構(gòu)建 Python 綁定模塊時,您將再次使用此函數(shù)。
運行此命令以構(gòu)建綁定:
$ invoke build-pybind11===================================================Building C++ Library
* Complete===================================================Building PyBind11 Module
* Complete
就是這樣!您已經(jīng)使用PyBind11. 是時候測試一下了!
與CFFI上面的示例類似,一旦您完成了創(chuàng)建 Python 綁定的繁重工作,調(diào)用您的函數(shù)看起來就像普通的 Python 代碼:
# pybind11_test.py
import pybind11_example
if __name__=="__main__":
# Sample data for your call
x, y=6, 2.3
answer=pybind11_example.cpp_function(x, y)
print(f" In Python: int: {x} float {y:.1f} return val {answer:.1f}")
由于您pybind11_example在PYBIND11_MODULE宏中用作模塊的名稱,因此這就是您導入的名稱。在m.def()您告訴PyBind11將cppmult函數(shù)導出為 的調(diào)用中cpp_function,這就是您用來從 Python 調(diào)用它的方法。
你也可以測試它invoke:
$ invoke test-pybind11===================================================Testing PyBind11 Module
In cppmul: int: 6 float 2.3 returning 13.8
In Python: int: 6 float 2.3 return val 13.8
這就是PyBind11看起來的樣子。接下來,您將了解何時以及為何PyBind11是適合該工作的工具。
PyBind11專注于 C++ 而不是 C,這使得它不同于ctypes和CFFI。它有幾個特性使其對 C++ 庫非常有吸引力:
話雖如此,您需要進行大量設置和配置才能PyBind11啟動和運行。正確安裝和構(gòu)建可能有點挑剔,但一旦完成,它似乎相當可靠。此外,PyBind11要求您至少使用 C++11 或更高版本。對于大多數(shù)項目來說,這不太可能是一個很大的限制,但它可能是您的一個考慮因素。
最后,創(chuàng)建 Python 綁定需要編寫的額外代碼是用 C++ 編寫的,而不是用 Python 編寫的。這可能是也可能不是你的問題,但它是比你在這里看到的其他工具不同。在下一節(jié)中,您將繼續(xù)討論Cython,它采用完全不同的方法來解決這個問題。
該方法Cython需要創(chuàng)建Python綁定使用類Python語言來定義綁定,然后生成的C或C ++代碼可被編譯成模塊。有幾種方法可以使用Cython. 最常見的一種是使用setupfrom distutils。對于此示例,您將堅持使用該invoke工具,它允許您使用運行的確切命令。
Cython是一個 Python 模塊,可以從PyPI安裝到您的虛擬環(huán)境中:
$ python3 -m pip install cython
同樣,如果您已將該requirements.txt文件安裝到虛擬環(huán)境中,則該文件已經(jīng)存在。您可以requirements.txt通過單擊以下鏈接獲取副本:
獲取示例代碼: 單擊此處獲取您將用于在本教程中了解 Python 綁定的示例代碼。
這應該讓你準備好與之合作Cython!
要使用 構(gòu)建 Python 綁定Cython,您將遵循與用于CFFI和 的步驟類似的步驟PyBind11。您將編寫綁定、構(gòu)建它們,然后運行 ?Python 代碼來調(diào)用它們。Cython可以同時支持 C 和 C++。對于本示例,您將使用cppmult您在PyBind11上面的示例中使用的庫。
聲明模塊的最常見形式Cython是使用.pyx文件:
1# cython_example.pyx
2""" Example cython interface definition """
3
4cdef extern from "cppmult.hpp":
5 float cppmult(int int_param, float float_param)
6
7def pymult( int_param, float_param ):
8 return cppmult( int_param, float_param )
這里有兩個部分:
這里使用的語言是 C、C++ 和 Python 的特殊組合。不過,對于 Python 開發(fā)人員來說,它看起來相當熟悉,因為其目標是使過程更容易。
第一部分 withcdef extern...告訴Cython下面的函數(shù)聲明也可以在cppmult.hpp文件中找到。這對于確保根據(jù)與 C++ 代碼相同的聲明構(gòu)建 Python 綁定非常有用。第二部分看起來像一個普通的 Python 函數(shù)——因為它是!本節(jié)創(chuàng)建一個可以訪問 C++ 函數(shù)的 Python 函數(shù)cppmult。
現(xiàn)在您已經(jīng)定義了 Python 綁定,是時候構(gòu)建它們了!
構(gòu)建過程Cython與您使用的構(gòu)建過程相似PyBind11。您首先Cython在.pyx文件上運行以生成.cpp文件。完成此操作后,您可以使用用于以下內(nèi)容的相同函數(shù)對其進行編譯PyBind11:
1# tasks.py
2def compile_python_module(cpp_name, extension_name):
3 invoke.run(
4 "g++ -O3 -Wall -Werror -shared -std=c++11 -fPIC "
5 "`python3 -m pybind11 --includes` "
6 "-I /usr/include/python3.7 -I . "
7 "{0} "
8 "-o {1}`python3.7-config --extension-suffix` "
9 "-L. -lcppmult -Wl,-rpath,.".format(cpp_name, extension_name)
10 )
11
12def build_cython(c):
13 """ Build the cython extension module """
14 print_banner("Building Cython Module")
15 # Run cython on the pyx file to create a .cpp file
16 invoke.run("cython --cplus -3 cython_example.pyx -o cython_wrapper.cpp")
17
18 # Compile and link the cython wrapper library
19 compile_python_module("cython_wrapper.cpp", "cython_example")
20 print("* Complete")
您首先運行cython您的.pyx文件。您可以在此命令上使用幾個選項:
生成 C++ 文件后,您可以使用 C++ 編譯器生成 Python 綁定,就像您為PyBind11. 請注意,include使用該pybind11工具生成額外路徑的調(diào)用仍在該函數(shù)中。在這里不會有任何傷害,因為您的來源不需要這些。
在 中運行此任務invoke會產(chǎn)生以下輸出:
$ invoke build-cython===================================================Building C++ Library
* Complete===================================================Building Cython Module
* Complete
可以看到它構(gòu)建了cppmult庫,然后構(gòu)建了cython模塊來包裝它。現(xiàn)在你有了CythonPython 綁定。(試著說的是迅速...)它的時間來測試一下吧!
調(diào)用新 Python 綁定的 Python 代碼與用于測試其他模塊的代碼非常相似:
1# cython_test.py
2import cython_example
3
4# Sample data for your call
5x, y=6, 2.3
6
7answer=cython_example.pymult(x, y)
8print(f" In Python: int: {x} float {y:.1f} return val {answer:.1f}")
第 2 行導入新的 Python 綁定模塊,并pymult()在第 7 行調(diào)用。請記住,該.pyx文件提供了一個 Python 包裝器cppmult()并將其重命名為pymult. 使用 invoke 運行您的測試會產(chǎn)生以下結(jié)果:
$ invoke test-cython===================================================Testing Cython Module
In cppmul: int: 6 float 2.3 returning 13.8
In Python: int: 6 float 2.3 return val 13.8
你得到和以前一樣的結(jié)果!
Cython是一個相對復雜的工具,可以在為 C 或 C++ 創(chuàng)建 Python 綁定時為您提供更深層次的控制。雖然您沒有在此處深入介紹它,但它提供了一種 Python 式的方法來編寫手動控制GIL 的代碼,這可以顯著加快某些類型的問題的處理速度。
然而,這種 Python 風格的語言并不完全是 Python,因此當您要快速確定 C 和 Python 的哪些部分適合何處時,會有一個輕微的學習曲線。
在研究本教程時,我遇到了幾種用于創(chuàng)建 Python 綁定的不同工具和選項。雖然我將此概述限制為一些更常見的選項,但我偶然發(fā)現(xiàn)了其他幾種工具。下面的列表并不全面。如果上述工具之一不適合您的項目,這只是其他可能性的一個示例。
PyBindGen為 C 或 C++ 生成 Python 綁定并用 Python 編寫。它旨在生成可讀的 C 或 C++ 代碼,這應該可以簡化調(diào)試問題。目前尚不清楚這是否最近已更新,因為文檔將 Python 3.4 列為最新的測試版本。然而,在過去的幾年里,每年都有發(fā)布。
Boost.Python有一個類似于PyBind11您在上面看到的界面。這不是巧合,因為PyBind11它基于這個庫!Boost.Python是用完整的 C++ 編寫的,并且在大多數(shù)平臺上支持大多數(shù)(如果不是全部)C++ 版本。相比之下,PyBind11僅限于現(xiàn)代 C++。
SIP是為PyQt項目開發(fā)的用于生成 Python 綁定的工具集。wxPython項目也使用它來生成它們的綁定。它有一個代碼生成工具和一個額外的 Python 模塊,為生成的代碼提供支持功能。
cppyy是一個有趣的工具,它的設計目標與您目前所見略有不同。用包作者的話來說:
“cppyy 背后的最初想法(追溯到 2001 年)是允許生活在 C++ 世界中的 Python 程序員訪問那些 C++ 包,而不必直接接觸 C++(或等待 C++ 開發(fā)人員過來并提供綁定) 。” (來源)
Shiboken是為與 Qt 項目關聯(lián)的 PySide 項目開發(fā)的用于生成 Python 綁定的工具。雖然它被設計為該項目的工具,但文檔表明它既不是 Qt 也不是 PySide 特定的,可用于其他項目。
SWIG是與此處列出的任何其他工具不同的工具。它是一個通用工具,用于為許多其他語言(而不僅僅是 Python)創(chuàng)建到 C 和 C++ 程序的綁定。這種為不同語言生成綁定的能力在某些項目中非常有用。當然,就復雜性而言,它會帶來成本。
恭喜!您現(xiàn)在已經(jīng)大致了解了用于創(chuàng)建Python 綁定的幾個不同選項。您已經(jīng)了解了編組數(shù)據(jù)以及創(chuàng)建綁定時需要考慮的問題。您已經(jīng)了解了如何使用以下工具從 Python 調(diào)用 C 或 C++ 函數(shù):
您現(xiàn)在知道,雖然ctypes允許您直接加載 DLL 或共享庫,但其他三個工具需要額外的步驟,但仍會創(chuàng)建完整的 Python 模塊。作為獎勵,您還使用了invoke從 Python 運行命令行任務的工具。
點擊關注,第一時間了解華為云新鮮技術(shù)~華為云博客_大數(shù)據(jù)博客_AI博客_云計算博客_開發(fā)者中心-華為云
由于C和C++代碼在編譯時生成的符號不同,而我們經(jīng)常會在C代碼里調(diào)用C++的代碼,
或者在C++代碼里調(diào)用C的代碼,下面就簡單總結(jié)一下二者相互調(diào)用時的語法。
最主要的就是在C++代碼里添加 extern “C”
1、首先C代碼調(diào)用C++
main.c
#include<stdio.h>
int sum(int a,int b);//此時sum函數(shù)生成的符號是C規(guī)則下的符號,
int main()
{
int ret=0;
ret=sum(1,2);
printf("%d\n",ret);
return 0;
}
test.cpp
extern "C"
{
int sum(int a, int b)//此時CPP文件函數(shù)在extern “C”里,CPP文件不再按CPP規(guī)則生成符號,
{ //而是按照C規(guī)則生成符號,這樣和main.c里的sum符號名相同,在鏈接的時候
return a+b; //能夠找到test.cpp里sum的定義。如果不加extern“C”,生成的符號為C++規(guī)則下 //的符號,main.c里的sum和這里的sum符號不同,在連接時, 就找不到符號的定義,就會報鏈接時的錯誤
}
}
2、C++調(diào)用C
main.cpp
#include<iostream>
using namespace std;
extern "C" int sum(int a,int b);//告訴編譯器,按C規(guī)則生成sum符號,這樣才會和test.c里的sum一樣
int main() //鏈接時才能找到符號的定義
{
int ret=0;
ret=sum(1,2);
cout<<ret<<endl;
return 0;
}
test.c
int sum(int a, int b)//C規(guī)則生成sum符號
{
return a+b;
}
由上我們可以看出extern “C”只有在C++文件才有的語法,而C文件不存在,所以處理的時候都是在C++的代碼里進行處理。C++不僅能生成C++規(guī)則的符號,還能生成C規(guī)則的符號。
… 但是,但是,一般情況下,一個公司購買別的公司的軟件產(chǎn)品,別人不會把自己辛辛苦苦寫的源代碼給你的,給你的只是個接口,一般情況下是一個庫,那這個時候我們就看不到源代碼,更用不了源代碼。此時又該如何實現(xiàn)C和C++的相互調(diào)用呢,由以上知道源碼的情況下,我們知道,C和C++相互調(diào)用時,C源碼并沒有發(fā)生任何改變,改變的只是C++的代碼,所以,在沒有源碼的情況下,C++調(diào)用C和上面的情況一樣。接下來我們就詳細討論一下如何在只有一個C++庫的情況下,用C調(diào)用C++庫
假如sum.cpp如下,實際上用戶是看不見的。
intsum(int a,int b)
{
return a+b;
}
g++ -shared -fpic -o libsum.so sum.cpp 之后,我們得到一個libsum.so庫。我們知道這一個庫,它的源碼我們是不知道的,那此時我們該如何用C調(diào)用它來實現(xiàn)自己的加法呢。此時我們就借助計算機世界里一句名言:計算機科學領域的任何問題都可以通過增加一個間接的中間層來解決。我們想辦法把libsum.so里的sum封裝起來。如下:
mysum.cpp
int sum(int a,int b);
extern "C"
{
int mysum(int a,int b)
{
return sum(a,b);//此時調(diào)用mysum()就相當于調(diào)用sum();
}
}
那我在C里再次調(diào)用sum時,我就調(diào)用封裝的mysum(),
#include<stdio.h>
int mysum(int a,int b);
int main()
{
int ret=0;
ret=mysum(10,20);
printf("%d\n",ret);
}
gcc -o run main.c mysum.cpp ./libsum.so生成run 大功告成。