Rust 讓 Python 更加偉大,隨著 Rust 的流行,反而讓 Python 的生產(chǎn)力提高了不少。因為有越來越多的 Python 工具,都選擇了 Rust 進行開發(fā),并且性能也優(yōu)于同類型的其它工具。比如: ruff:速度極快的代碼分析工具,以及代碼格式化工具; orjson:一個高性能的 JSON 解析庫; watchfiles:可以對指定目錄進行實時監(jiān)控; polars:和 pandas 類似的數(shù)據(jù)分析工具; pydantic:數(shù)據(jù)驗證工具; ......
總之現(xiàn)在 Rust + Python 已經(jīng)成為了一個趨勢,并且 Rust 也提供了一系列成熟好用的工具,比如 PyO3、Maturin,專門為 Python 編寫擴展。不過關于 PyO3 我們以后再聊,本篇文章先來介紹如何將 Rust 代碼編譯成動態(tài)庫,然后交給 Python 的 ctypes 模塊調用。 因為通過 ctypes 調用動態(tài)庫是最簡單的一種方式,它只對操作系統(tǒng)有要求,只要操作系統(tǒng)一致,那么任何提供了 ctypes 模塊的 Python 解釋器都可以調用。 當然這也側面要求,Rust 提供的接口不能太復雜,因為 ctypes 提供的交互能力還是比較有限的,最明顯的問題就是不同語言的數(shù)據(jù)類型不同,一些復雜的交互方式還是比較難做到的,還有多線程的控制問題等等。 之前說過使用 ctypes 調用 C 的動態(tài)庫,里面詳細介紹了 ctypes 的用法,因此本文關于 ctypes 就不做詳細介紹了。
下面我們舉個例子感受一下 Python 和 Rust 的交互過程,首先通過如下命令創(chuàng)建一個 Rust 項目: cargo new py_lib --lib
創(chuàng)建完之后修改 Cargo.toml,在里面加入如下內(nèi)容: [lib] # 編譯之后的動態(tài)庫的名稱 name = "py_lib" # 表示編譯成一個和 C 語言二進制接口(ABI)兼容的動態(tài)鏈接庫 crate-type = ["cdylib"]
cdylib 表示生成動態(tài)庫,如果想生成靜態(tài)庫,那么就指定為 staticlib。 下面開始編寫源代碼,在生成項目之后,src 目錄下會有一個 lib.rs,它是整個庫的入口點。我們的代碼比較簡單,直接寫在 lib.rs 里面即可。 #[no_mangle] pub extern "C" fn add(a: i32, b: i32) -> i32 { a + b }
#[no_mangle] pub extern "C" fn get_square_root(v: i32) -> f64 { (v as f64).sqrt() }
在定義函數(shù)時需要使用 pub extern "C" 進行聲明,它表示創(chuàng)建一個外部可見、遵循 C 語言調用約定的函數(shù),因為 Python 使用的是 C ABI。
此外還要給函數(shù)添加一個 #[no_mangle] 屬性,讓編譯器在將 Rust 函數(shù)導出為 C 函數(shù)時,不要改變函數(shù)的名稱。確保在編譯成動態(tài)庫后,函數(shù)名保持不變,否則在調用動態(tài)庫時就找不到指定的函數(shù)了。 Rust 有個名稱修飾(Name Mangling)的機制,在跨語言操作時,會修改函數(shù)名,增加一些額外信息。這種修改對 Rust 內(nèi)部使用沒有影響,但會干擾其它語言的調用,因此需要通過 #[no_mangle] 將該機制禁用掉。
代碼編寫完成,我們通過 cargo build 進行編譯,然后在 target/debug 目錄下就會生成相應的動態(tài)庫。由于庫的名稱我們指定為 py_lib,那么生成的庫文件名就叫 libpy_lib.dylib。 當功能全部實現(xiàn)并且測試通過時,最好重新編譯一次,并加上 --release 參數(shù)。這樣可以對代碼進行優(yōu)化,當然編譯時間也會稍微長一些,并且生成的庫文件會在 target/release 目錄中。
編譯器生成動態(tài)庫后,會自動加上一個 lib 前綴(Windows 系統(tǒng)除外),至于后綴則與操作系統(tǒng)有關。
然后我們通過 Python 進行調用。 import ctypes
# 使用 ctypes 很簡單,直接 import 進來 # 然后使用 ctypes.CDLL 這個類來加載動態(tài)鏈接庫 # 或者使用 ctypes.cdll.LoadLibrary 也是可以的 py_lib = ctypes.CDLL("../py_lib/target/debug/libpy_lib.dylib")
# 加載之后就得到了動態(tài)鏈接庫對象,我們起名為 py_lib # 然后通過屬性訪問的方式去調用里面的函數(shù) print(py_lib.add(11, 22)) """ 33 """
# 如果不確定函數(shù)是否存在,那么建議使用反射 # 因為函數(shù)不存在,通過 . 的方式獲取是會拋異常的 get_square_root = getattr(py_lib, "get_square_root", None) if get_square_root: print(get_square_root) """ <_FuncPtr object at 0x7fae30a2b040> """
# 不存在 sub 函數(shù),所以得到的結果為 None sub = getattr(py_lib, "sub", None) print(sub) """ None """
所以使用 ctypes 去調用動態(tài)鏈接庫非常方便,過程很簡單: 1)通過 ctypes.CDLL 去加載動態(tài)庫; 2)加載動態(tài)鏈接庫之后會返回一個對象,我們上面起名為 py_lib; 3)然后直接通過 py_lib 調用里面的函數(shù),但為了程序的健壯性,建議使用反射,確定調用的函數(shù)存在后才會調用;
我們以上就演示了如何通過 ctypes 模塊來調用 Rust 編譯生成的動態(tài)庫,但顯然目前還是遠遠不夠的,比如說: from ctypes import CDLL
py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")
square_root = py_lib.get_square_root(100) print(square_root) # 0
100 的平方根是 10,但卻返回了 0。這是因為 ctypes 在解析返回值的時候默認是按照整型來解析的,但當前的函數(shù)返回的是浮點型,因此函數(shù)在調用之前需要顯式地指定其返回值類型。 不過在這之前,我們需要先來看看 Python 類型和 Rust 類型之間的轉換關系。 使用 ctypes 調用動態(tài)鏈接庫,主要是調用庫里面使用 Rust 編寫好的函數(shù),但這些函數(shù)是需要參數(shù)的,還有返回值。而不同語言的變量類型不同,Python 不能直接往 Rust 編寫的函數(shù)中傳參,因此 ctypes 提供了大量的類,幫我們將 Python 的類型轉成 Rust 的類型。 與其說轉成 Rust 的類型,倒不如說轉成 C 的類型,因為 Rust 導出的函數(shù)要遵循 C 的調用約定。
下面來測試一下,首先編寫 Rust 代碼: #[no_mangle] pub extern "C" fn add_u32(a: u32) -> u32 { a + 1 } #[no_mangle] pub extern "C" fn add_isize(a: isize) -> isize { a + 1 } #[no_mangle] pub extern "C" fn add_f32(a: f32) -> f32 { a + 1. } #[no_mangle] pub extern "C" fn add_f64(a: f64) -> f64 { a + 1. } #[no_mangle] pub extern "C" fn reverse_bool(a: bool) -> bool { !a }
編譯之后 Python 進行調用。 from ctypes import *
py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")
print(py_lib.add_u32(123)) """ 124 """ print(py_lib.add_isize(666)) """ 667 """ try: print(py_lib.add_f32(3.14)) except Exception as e: print(e) """ <class 'TypeError'>: Don't know how to convert parameter 1 """ # 我們看到報錯了,告訴我們不知道如何轉化第 1 個參數(shù) # 因為 Python 的數(shù)據(jù)和 C 的數(shù)據(jù)不一樣,所以不能直接傳遞 # 但整數(shù)是個例外,除了整數(shù),其它數(shù)據(jù)都需要使用 ctypes 包裝一下 # 另外整數(shù)最好也包裝一下,因為不同整數(shù)之間,精度也有區(qū)別 print(py_lib.add_f32(c_float(3.14))) """ 1 """ # 雖然沒報錯,但是結果不對,結果應該是 3.14 + 1 = 4.14,而不是 1 # 因為 ctypes 調用函數(shù)時默認使用整型來解析,但該函數(shù)返回的不是整型 # 需要告訴 ctypes,add_f32 函數(shù)返回的是 c_float,請按照 c_float 來解析 py_lib.add_f32.restype = c_float print(py_lib.add_f32(c_float(3.14))) """ 4.140000343322754 """ # f32 和 f64 是不同的類型,占用的字節(jié)數(shù)也不一樣 # 所以 c_float 和 c_double 之間不可混用,雖然都是浮點數(shù) py_lib.add_f64.restype = c_double print(py_lib.add_f64(c_double(3.14))) """ 4.140000000000001 """
py_lib.reverse_bool.restype = c_bool print(py_lib.reverse_bool(c_bool(True))) print(py_lib.reverse_bool(c_bool(False))) """ False True """
不復雜,以上我們就實現(xiàn)了數(shù)值類型的傳遞。
字符類型有兩種,一種是 ASCII 字符,本質上是個 u8;一種是 Unicode 字符,本質上是個 u32。
編寫 Rust 代碼: #[no_mangle] pub extern "C" fn get_char(a: u8) -> u8 { a + 1 }
#[no_mangle] pub extern "C" fn get_unicode(a: u32) -> u32 { let chr = char::from_u32(a).unwrap(); if chr == '憨' { '批' as u32 } else { a } }
我們知道 Rust 專門提供了 4 個字節(jié) char 類型來表示 unicode 字符,但對于外部導出函數(shù)來說,使用 char 是不安全的,所以直接使用 u8 和 u32 就行。
編譯之后,Python 調用: from ctypes import *
py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")
# u8 除了可以使用 c_byte 包裝之外,還可以使用 c_char # 并且 c_byte 里面只能接收整數(shù),而 c_char 除了整數(shù),還可以接收長度為 1 的字節(jié)串 print(c_byte(97)) print(c_char(97)) print(c_char(b"a")) """ c_byte(97) c_char(b'a') c_char(b'a') """ # 以上三者是等價的,因為 char 說白了就是個 u8
# 指定返回值為 c_byte,會返回一個整數(shù) py_lib.get_char.restype = c_byte # c_byte(97)、c_char(97)、c_char(b"a") 都是等價的 # 因為它們本質上都是 u8,至于 97 也可以解析為 u8 print(py_lib.get_char(97)) # 98 # 指定返回值為 c_char,會返回一個字符(長度為 1 的 bytes 對象) py_lib.get_char.restype = c_char print(py_lib.get_char(97)) # b'b'
py_lib.get_unicode.restype = c_wchar print(py_lib.get_unicode(c_wchar("嘿"))) # 嘿 # 直接傳一個 u32 整數(shù)也可以,因為 unicode 字符底層就是個 u32 print(py_lib.get_unicode(ord("憨"))) # 批
以上就是字符類型的操作,比較簡單。
再來看看字符串,我們用 Rust 實現(xiàn)一個函數(shù),它接收一個字符串,然后返回大寫形式。 use std::ffi::{CStr, CString}; use std::os::raw::c_char;
#[no_mangle] pub extern "C" fn to_uppercase(s: *const c_char) -> *mut c_char { // 將 *const c_char 轉成 &CStr let s = unsafe { CStr::from_ptr(s) }; // 將 &CStr 轉成 &str // 然后調用 to_uppercase 轉成大寫,得到 String let s = s.to_str().unwrap().to_uppercase(); // 將 String 轉成 *mut char 返回 CString::new(s).unwrap().into_raw() }
解釋一下里面的 CStr 和 CString,在 Rust 中,CString 用于創(chuàng)建 C 風格的字符串(以 \0 結尾),擁有自己的內(nèi)存。關鍵的是,CString 擁有值的所有權,當實例離開作用域時,它的析構函數(shù)會被調用,相關內(nèi)存會被自動釋放。
而 CStr,它和 CString 之間的關系就像 str 和 String 的關系,所以 CStr 一般以引用的形式出現(xiàn)。并且 CStr 沒有 new 方法,不能直接創(chuàng)建,它需要通過 from_ptr 方法從原始指針轉化得到。 然后指針類型是 *const 和 *mut,分別表示指向 C 風格字符串的首字符的不可變指針和可變指針,它們的區(qū)別主要在于指向的數(shù)據(jù)是否可以被修改。如果不需要修改,那么使用 *const 會更安全一些。
我們編寫 Python 代碼測試一下。 from ctypes import *
py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib") s = "hello 古明地覺".encode("utf-8") # 默認是按照整型解析的,所以不指定返回值類型的話,會得到臟數(shù)據(jù) print(py_lib.to_uppercase(c_char_p(s))) """ 31916096 """ # 指定返回值為 c_char_p,表示按照 char * 來解析 py_lib.to_uppercase.restype = c_char_p print( py_lib.to_uppercase(c_char_p(s)).decode("utf-8") ) """ HELLO 古明地覺 """
從表面上看似乎挺順利的,但背后隱藏著內(nèi)存泄露的風險,因為 Rust 里面創(chuàng)建的 CString 還駐留在堆區(qū),必須要將它釋放掉。所以我們還要寫一個函數(shù),用于釋放字符串。 use std::ffi::{CStr, CString}; use std::os::raw::c_char;
#[no_mangle] pub extern "C" fn to_uppercase(s: *const c_char) -> *mut c_char { let s = unsafe { CStr::from_ptr(s) }; let s = s.to_str().unwrap().to_uppercase(); CString::new(s).unwrap().into_raw() }
#[no_mangle] pub extern "C" fn free_cstring(s: *mut c_char) { unsafe { if s.is_null() { return } // 基于原始指針創(chuàng)建 CString,拿到堆區(qū)字符串的所有權 // 然后離開作用域,自動釋放 CString::from_raw(s) }; }
然后來看看 Python 如何調用: from ctypes import *
py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")
s = "hello 古明地覺".encode("utf-8") # Rust 返回的是原始指針,這里必須要拿到它保存的地址 # 所以指定返回值為 c_void_p,如果指定為 c_char_p, # 那么會直接轉成 bytes 對象,這樣地址就拿不到了 py_lib.to_uppercase.restype = c_void_p # 拿到地址,此時的 ptr 是一個普通的整數(shù),但它和指針保存的地址是一樣的 ptr = py_lib.to_uppercase(c_char_p(s)) # 將 ptr 轉成 c_char_p,獲取 value 屬性,即可得到具體的 bytes 對象 print(cast(ptr, c_char_p).value.decode("utf-8")) """ HELLO 古明地覺 """ # 內(nèi)容我們拿到了,但堆區(qū)的字符串還沒有釋放,所以調用 free_cstring py_lib.free_cstring(c_void_p(ptr))
通過 CString 的 into_raw,可以基于 CString 創(chuàng)建原始指針 *mut,然后 Python 將指針指向的堆區(qū)數(shù)據(jù)拷貝一份,得到 bytes 對象。 但這個 CString 依舊駐留在堆區(qū),所以 Python 不能將返回值指定為 c_char_p,因為它會直接創(chuàng)建 bytes 對象,這樣就拿不到指針了。因此將返回值指定為 c_void_p,調用函數(shù)會得到一串整數(shù),這個整數(shù)就是指針保存的地址。
我們使用 cast 函數(shù)可以將地址轉成 c_char_p,獲取它的 value 屬性拿到具體的字節(jié)串。再通過 c_void_p 創(chuàng)建原始指針交給 Rust,調用 CString 的 from_raw,可以基于 *mut 創(chuàng)建 CString,從而將所有權奪回來,然后離開作用域時釋放堆內(nèi)存。
如果擴展函數(shù)里面接收的是指針,那么 Python 要怎么傳遞呢? #[no_mangle] pub extern "C" fn add(a: *mut i32, b: *mut i32) -> i32 { // 定義為 *mut,那么可以修改指針指向的值,定義為 *const,則不能修改 if a.is_null() || b.is_null() { 0 } else { let res = unsafe { *a + *b }; unsafe { // 這里將 *a 和 *b 給改掉 *a = 666; *b = 777; } res } }
定義了一個 add 函數(shù),接收兩個 i32 指針,返回解引用后相加的結果。但是在返回之前,我們將 *a 和 *b 的值也修改了。 from ctypes import *
py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")
a = c_int(22) b = c_int(33) # 計算 print(py_lib.add(pointer(a), pointer(b))) # 55 # 我們看到 a 和 b 也被修改了 print(a, a.value) # c_int(666) 666 print(b, b.value) # c_int(777) 777
非常簡單,那么問題來了,能不能返回一個指針呢?答案是當然可以,只不過存在一些注意事項。 由于 Rust 本身的內(nèi)存安全原則,直接從函數(shù)返回一個指向本地局部變量的指針是不安全的。因為該變量的作用域僅限于函數(shù)本身,一旦函數(shù)返回,該變量的內(nèi)存就會被回收,從而出現(xiàn)懸空指針。
為了避免這種情況出現(xiàn),我們應該在堆上分配內(nèi)存,但這又出現(xiàn)了之前 CString 的問題。Python 在拿到值之后,堆內(nèi)存依舊駐留在堆區(qū)。因此 Rust 如果想返回指針,那么同時還要定義一個釋放函數(shù)。 #[no_mangle] pub extern "C" fn add(a: *const i32, b: *const i32) -> *mut i32 { // 返回值的類型是 *mut i32,所以 res 不能直接返回,因此它是 i32 let res = unsafe {*a + *b}; // 創(chuàng)建智能指針(將 res 裝箱),然后返回原始指針 Box::into_raw(Box::new(res)) }
#[no_mangle] pub extern "C" fn free_i32(ptr: *mut i32) { if !ptr.is_null() { // 轉成 Box<i32>,同時拿到所有權,在離開作用域時釋放堆內(nèi)存 unsafe { let _ = Box::from_raw(ptr); } } }
然后 Python 進行調用: from ctypes import *
py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")
a, b = c_int(22), c_int(33) # 指定類型為 c_void_p py_lib.add.restype = c_void_p # 拿到指針保存的地址 ptr = py_lib.add(pointer(a), pointer(b)) # 將 c_void_p 轉成 POINTER(c_int) 類型,也就是 c_int * # 通過它的 contents 屬性拿到具體的值 print(cast(ptr, POINTER(c_int)).contents) # c_int(55) print(cast(ptr, POINTER(c_int)).contents.value) # 55 # 釋放堆內(nèi)存 py_lib.free_i32(c_void_p(ptr))
這樣我們就拿到了指針,并且也不會出現(xiàn)內(nèi)存泄露。但是單獨定義一個釋放函數(shù)還是有些麻煩的,所以 Rust 自動提供了一個 free 函數(shù),專門用于釋放堆內(nèi)存。舉個例子: use std::ffi::{CStr, CString}; use std::os::raw::c_char;
#[no_mangle] pub extern "C" fn to_uppercase(s: *const c_char) -> *mut c_char { let s = unsafe { CStr::from_ptr(s) }; let s = s.to_str().unwrap().to_uppercase(); CString::new(s).unwrap().into_raw() }
#[no_mangle] pub extern "C" fn add(a: *const i32, b: *const i32) -> *mut i32 { let res = unsafe {*a + *b}; Box::into_raw(Box::new(res)) }
這是出現(xiàn)過的兩個函數(shù),它們的內(nèi)存都申請在堆區(qū),但我們將內(nèi)存釋放函數(shù)刪掉了,因為 Rust 自動提供了一個 free 函數(shù),專門用于堆內(nèi)存的釋放。 from ctypes import *
py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")
# 返回值類型指定為 c_void_p,表示萬能指針 py_lib.to_uppercase.restype = c_void_p py_lib.add.restype = c_void_p
ptr1 = py_lib.to_uppercase( c_char_p("Serpen 老師".encode("utf-8")) ) ptr2 = py_lib.add( pointer(c_int(123)), pointer(c_int(456)) ) # 函數(shù)調用完畢,將地址轉成具體的類型的指針 print(cast(ptr1, c_char_p).value.decode("utf-8")) """ SERPEN 老師 """ print(cast(ptr2, POINTER(c_int)).contents.value) """ 579 """ # 釋放堆內(nèi)存,直接調用 free 函數(shù)即可,非常方便 py_lib.free(c_void_p(ptr1)) py_lib.free(c_void_p(ptr2))
以上我們就實現(xiàn)了指針的傳遞和返回,但對于整數(shù)、浮點數(shù)而言,直接返回它們的值即可,沒必要返回指針。
下面來看看如何傳遞數(shù)組,由于數(shù)組在作為參數(shù)傳遞的時候會退化為指針,所以數(shù)組的長度信息就丟失了,使用 sizeof 計算出來的結果就是一個指針的大小。因此將數(shù)組作為參數(shù)傳遞的時候,應該將當前數(shù)組的長度信息也傳遞過去,否則可能會訪問非法的內(nèi)存。 我們實現(xiàn)一個功能,Rust 接收一個 Python 數(shù)組,進行原地排序。 use std::slice;
#[no_mangle] pub extern "C" fn sort_array(arr: *mut i32, len: usize) { assert!(!arr.is_null());
unsafe { // 得到一個切片 &mut[i32] let slice = slice::from_raw_parts_mut(arr, len); slice.sort(); // 排序 } }
然后 Python 進行調用: from ctypes import *
py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")
# 一個列表 data = [3, 2, 1, 5, 4, 7, 6] # 但是列表不能傳遞,必須要轉成 C 數(shù)組 # Array_Type 就相當于 C 的 int array[len(data)] Array_Type = c_int * len(data) # 創(chuàng)建數(shù)組 array = Array_Type(*data) print(list(array)) # [3, 2, 1, 5, 4, 7, 6] py_lib.sort_array(array, len(array)) print(list(array)) # [1, 2, 3, 4, 5, 6, 7]
排序實現(xiàn)完成,這里的數(shù)組是 Python 傳過去的,并且進行了原地修改。那 Rust 可不可以返回數(shù)組給 Python 呢?從理論上來說可以,但實際不建議這么做,因為你不知道返回的數(shù)組的長度是多少? 如果你真的想返回數(shù)組的話,那么可以將數(shù)組拼接成字符串,然后返回。 use std::ffi::{c_char, CString};
#[no_mangle] pub extern "C" fn create_array() -> *mut c_char { // 篩選出 1 到 50 中,能被 3 整除的數(shù) // 并以逗號為分隔符,將這些整數(shù)拼接成字符串 let vec = (1..=50) .filter(|c| *c % 3 == 0) .map(|c| c.to_string()) .collect::<Vec<String>>() .join(","); CString::new(vec).unwrap().into_raw() }
編譯之后交給 Python 調用。
from ctypes import *
py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")
# 只要是需要釋放的堆內(nèi)存,都建議按照 c_void_p 來解析 py_lib.create_array.restype = c_void_p # 此時拿到的就是指針保存的地址,在 Python 里面就是一串整數(shù) ptr = py_lib.create_array() # 由于是字符串首字符的地址,所以轉成 char *,拿到具體內(nèi)容 print(cast(ptr, c_char_p).value.decode("utf-8")) """ 3,6,9,12,15,18,21,24,27,30,33,36,39,42,45,48 """ # 此時我們就將數(shù)組拼接成字符串返回了 # 但是堆區(qū)的 CString 還在,所以還要釋放掉,調用 free 函數(shù)即可 # 注意:ptr 只是一串整數(shù),或者說它就是 Python 的一個 int 對象 # 換句話說 ptr 只是保存了地址值,但它不具備指針的含義 # 因此需要再使用 c_void_p 包裝一下(轉成指針),才能傳給 free 函數(shù) py_lib.free(c_void_p(ptr))
因此雖然不建議返回數(shù)組,但將數(shù)組轉成字符串返回也不失為一個辦法,當然除了數(shù)組,你還可以將更復雜的結構轉成字符串返回。 結構體應該是 Rust 里面最重要的結構之一了,它要如何和外部交互呢? use std::ffi::c_char;
#[repr(C)] pub struct Girl { pub name: *mut c_char, pub age: u8, }
#[no_mangle] pub extern "C" fn create_struct(name: *mut c_char, age: u8) -> Girl { Girl { name, age } }
因為結構體實例要返回給外部,所以它的字段類型必須是兼容的,不能定義 C 理解不了的類型。然后還要設置 #[repr(C)] 屬性,來保證結構體的內(nèi)存布局和 C 是兼容的。
下面通過 cargo build 命令編譯成動態(tài)庫,Python 負責調用。 from ctypes import *
py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")
class Girl(Structure):
_fields_ = [ ("name", c_char_p), ("age", c_uint8), ]
# 指定 create_struct 的返回值類型為 Girl py_lib.create_struct.restype = Girl girl = py_lib.create_struct( c_char_p("S 老師".encode("utf-8")), c_uint8(18) ) print(girl.name.decode("utf-8")) # S 老師 print(girl.age) # 18
調用成功,并且此時是沒有內(nèi)存泄露的。 當通過 FFI 將數(shù)據(jù)從 Rust 傳遞到 Python 時,如果傳遞的是指針,那么會涉及內(nèi)存釋放的問題。但如果傳遞的是值,那么它會復制一份給 Python,而原始的值(這里是結構體實例)會被自動銷毀,所以無需擔心。
然后是結構體內(nèi)部的字段,雖然里面的 name 字段是 *mut c_char,但它的值是由 Python 傳過來的,而不是在 Rust 內(nèi)部創(chuàng)建的,因此沒有問題。
但如果將 Rust 代碼改一下: use std::ffi::{c_char, CString};
#[repr(C)] pub struct Girl { pub name: *mut c_char, pub age: u8, }
#[no_mangle] pub extern "C" fn create_struct() -> Girl { let name = CString::new("S 老師").unwrap().into_raw(); let age = 18; Girl { name, age } }
這時就尷尬了,此時的字符串是 Rust 里面創(chuàng)建的,轉成原始指針之后,Rust 將不再管理相應的堆內(nèi)存(因為 into_raw 將所有權轉移走了),此時就需要手動堆內(nèi)存了。 from ctypes import *
py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")
class Girl(Structure):
_fields_ = [ ("name", c_char_p), ("age", c_uint8), ]
# 指定 create_struct 的返回值類型為 Girl py_lib.create_struct.restype = Girl girl = py_lib.create_struct() print(girl.name.decode("utf-8")) # S 老師 print(girl.age) # 18 # 直接傳遞 girl 即可,會釋放 girl 里面的字段在堆區(qū)的內(nèi)存 py_lib.free(girl)
此時就不會出現(xiàn)內(nèi)存泄露了,在 free 的時候,將變量 girl 傳進去,釋放掉內(nèi)部字段占用的堆內(nèi)存。 當然,Rust 也可以返回結構體指針,通過 Box<T> 實現(xiàn)。 #[no_mangle] pub extern "C" fn create_struct() -> *mut Girl { let name = CString::new("S 老師").unwrap().into_raw(); let age = 18; Box::into_raw(Box::new(Girl { name, age })) }
注意:之前是 name 字段在堆上,但結構體實例在棧上,現(xiàn)在 name 字段和結構體實例都在堆上。
然后 Python 調用也很簡單,關鍵是釋放的問題。 from ctypes import *
py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")
class Girl(Structure):
_fields_ = [ ("name", c_char_p), ("age", c_uint8), ]
# 此時返回值類型就變成了 c_void_p # 當返回指針時,建議將返回值設置為 c_void_p py_lib.create_struct.restype = c_void_p # 拿到指針(一串整數(shù)) ptr = py_lib.create_struct() # 將指針轉成指定的類型,而類型顯然是 POINTER(Girl) # 調用 POINTER(T) 的 contents 方法,拿到相應的結構體實例 girl = cast(ptr, POINTER(Girl)).contents # 訪問具體內(nèi)容 print(girl.name.decode("utf-8")) # S 老師 print(girl.age) # 18
# 釋放堆內(nèi)存,這里的釋放分為兩步,并且順序不能錯 # 先 free(girl),釋放掉內(nèi)部字段(name)占用的堆內(nèi)存 # 然后 free(c_void_p(ptr)),釋放掉結構體實例 girl 占用的堆內(nèi)存 py_lib.free(girl) py_lib.free(c_void_p(ptr))
不難理解,只是在釋放結構體實例的時候需要多留意,如果內(nèi)部有字段占用堆內(nèi)存,那么需要先將這些字段釋放掉。而釋放的方式是將結構體實例作為參數(shù)傳給 free 函數(shù),然后再傳入 c_void_p 釋放結構體實例。
最后看一下 Python 如何傳遞函數(shù)給 Rust,因為 Python 和 Rust 之間使用的是 C ABI,所以函數(shù)必須遵循 C 的標準。 // calc 接收三個參數(shù),前兩個參數(shù)是 *const i32 // 最后一個參數(shù)是函數(shù),它接收兩個 *const i32,返回一個 i32 #[no_mangle] pub extern "C" fn calc( a: *const i32, b: *const i32, op: extern "C" fn(*const i32, *const i32) -> i32 ) -> i32 { op(a, b) }
然后看看 Python 如何傳遞回調函數(shù)。 from ctypes import *
py_lib = CDLL("../py_lib/target/debug/libpy_lib.dylib")
# 基于 Python 函數(shù)創(chuàng)建 C 函數(shù),通過 @CFUNCTYPE() 進行裝飾 # CFUNCTYPE 第一個參數(shù)是返回值類型,剩余的參數(shù)是參數(shù)類型 @CFUNCTYPE(c_int, POINTER(c_int), POINTER(c_int)) def add(a, b): # a、b 為 int *,通過 .contents.value 拿到具體的值 return a.contents.value + b.contents.value
@CFUNCTYPE(c_int, POINTER(c_int), POINTER(c_int)) def sub(a, b): return a.contents.value - b.contents.value
@CFUNCTYPE(c_int, POINTER(c_int), POINTER(c_int)) def mul(a, b): return a.contents.value * b.contents.value
@CFUNCTYPE(c_int, POINTER(c_int), POINTER(c_int)) def div(a, b): return a.contents.value // b.contents.value
a = pointer(c_int(10)) b = pointer(c_int(2)) print(py_lib.calc(a, b, add)) # 12 print(py_lib.calc(a, b, sub)) # 8 print(py_lib.calc(a, b, mul)) # 20 print(py_lib.calc(a, b, div)) # 5
成功實現(xiàn)了向 Rust 傳遞回調函數(shù),當然例子舉得有點刻意了,比如參數(shù)類型指定為 i32 即可,沒有必要使用指針。
以上我們就介紹了 Python 如何調用 Rust 編譯的動態(tài)庫,再次強調一下,通過 ctypes 調用動態(tài)庫是最方便、最簡單的方式。它和 Python 的版本無關,也不涉及底層的 C 擴展,它只是將 Rust 編譯成 C ABI 兼容的動態(tài)庫,然后交給 Python 進行調用。
因此這也側面要求,函數(shù)的參數(shù)和返回值的類型應該是 C 可以表示的類型,比如 Rust 函數(shù)不能返回一個 trait 對象??傊?span style="font-family: system-ui, -apple-system, BlinkMacSystemFont, "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;font-size: 15px;letter-spacing: 0.544px;text-wrap: wrap;background-color: rgb(255, 255, 255);">在調用動態(tài)庫的時候,庫函數(shù)內(nèi)部的邏輯可以很復雜,但是參數(shù)和返回值最好要簡單。 如果你發(fā)現(xiàn) Python 代碼存在大量的 CPU 密集型計算,并且不怎么涉及復雜的 Python 數(shù)據(jù)結構,那么不妨將這些計算交給 Rust。 以上就是本文的內(nèi)容,后續(xù)有空我們介紹如何用 Rust 的 PyO3 來為 Python 編寫擴展。PyO3 的定位類似于 Cython,用它來寫擴展非常的方便,后續(xù)有機會我們詳細聊一聊。
如果你覺得本文對你有幫助,就請點個贊吧,萬分感謝。
|