Rust 以其高效能和記憶體安全聞名,而 Python 則因其簡單易用和廣泛的套件支持成為許多開發者的首選語言。在某些情況下,開發者可能希望將這兩個語言結合使用,利用 Python 的靈活性和 Rust 的高效能。這就需要用到 FFI(Foreign Function Interface,外部函數介面)。
在這篇文章中,我們將探討如何透過 FFI 讓 Rust 與 Python 進行雙向互操作。具體來說,我們會展示如何在 Rust 中調用 Python 函數,以及如何從 Python 調用 Rust 函數,實現兩者互通。
當我們提到 FFI(Foreign Function Interface,外部函數介面),其實是在說一個能夠讓不同程式語言「彼此溝通」的橋樑。簡單來說,FFI 是一個讓一個程式語言可以「借用」另一個語言所寫的函數或庫的技術。例如,使用 FFI,你可以在 Python 程式裡運行用 Rust 寫的高效能函數,或者從 Rust 調用用 Python 寫的函數。
FFI 的原理很簡單,就是把一個語言的函式轉換成另一個語言可以理解的格式。大部分程式語言都會把函式轉換成和 C 語言兼容的形式,因為 C 語言的 ABI(應用程式二進位介面)是大家普遍遵守的標準。所以,當我們在講 FFI 時,基本上就是利用這個 C 語言的標準,讓不同語言能夠順利交流。
來個更生活化的比喻:想像 Python 是一位擅長多樣料理的廚師,能快速做出很多不同的菜色;但當需要非常精細的刀工時,他可能會請專業的「刀工師傅」(也就是 Rust)來幫忙。這兩位專家怎麼配合呢?就是透過 FFI 這個「溝通橋梁」:廚師把需要處理的食材交給刀工師傅,師傅切完後再把食材交還給廚師,達到各司其職的作用,這樣的合作方式,就是 FFI 的運作模式!
理解了一些關於FFI的概念後,下面我們進入實際操作的部分。
通過 FFI,就像大谷翔平可以玩二刀流,在需要高效能的地方使用 Rust ,在能夠使用 Python 函式庫支援的地方使用 Python ,讓 Python 的運算限制不再是開發的極限,也可以讓 Python 的廣大資源輔助 Rust 應用開發,這麼棒還有什麼理由不用呢,就讓我們看看簡單的範例吧。
首先,我們建立一個 Rust 專案,並定義我們要讓 Python 調用的函數。
cargo new rust_python_ffi --lib
cd rust_python_ffi
Cargo.toml
在 Cargo.toml
中,我們需要配置專案,將其編譯為共享套件,這樣 Python 才能加載並調用該套件。我們還需要加入 libc
來幫助處理與 C 的互通。
[package]
name = "rust_python_ffi"
version = "0.1.0"
edition = "2021"
[dependencies]
libc = "0.2"
[lib]
crate-type = ["cdylib"]
在這段配置中,有幾個關鍵點需要注意:
crate-type = ["cdylib"]
:這一行的作用是告訴 Rust 編譯器將專案編譯為一個C 動態庫(cdylib
),這樣 Python 或其他語言才能透過 FFI 來加載並調用 Rust 中的函數。如果沒有這行設定,Rust 會將專案編譯為靜態庫或執行檔,這樣就無法進行跨語言的互操作。
libc
依賴:libc
是一個常見的 Rust crate,它提供了與 C 標準庫的接口,幫助我們在 Rust 中更方便地與 C 語言互動。由於 Python 的 FFI 介面基於 C 語言的 ABI,因此我們需要 libc
來處理一些底層的數據型別轉換,如 c_int
、c_char
等。
edition = "2021"
:Rust 的 Edition 是語言進化過程中的一種版本標識,它定義了該專案使用的語言功能和編譯器行為。這裡使用的是 Rust 2021 Edition,它包含了最新的語言改進和功能。這並不直接影響 FFI,但確保我們能夠使用到 Rust 最新的語言特性。
這些配置完成後,我們便可以開始編寫 Rust 函數並將其導出為共享庫,供 Python 程式加載和使用。
接下來,我們在 src/lib.rs
中編寫一個簡單的數學函數,並將其導出為 FFI 函數供 Python 調用。
明白了,我會在程式碼中加上簡單註解,並且在程式碼後面附上詳細的文字說明:
use libc::c_int; // 使用 C 的整數類型
use std::ffi::CString; // 使用 CString 處理 C 字串
use std::os::raw::c_char; // 使用 C 的字元類型
#[no_mangle] // 確保函數名稱不被編譯器修改
pub extern "C" fn add_two_numbers(a: c_int, b: c_int) -> c_int {
a + b // 返回兩個整數相加的結果
}
#[no_mangle] // 確保函數名稱不被編譯器修改
pub extern "C" fn hello_from_rust() -> *const c_char {
let hello = CString::new("Hello from Rust!").expect("CString creation failed"); // 創建 CString
hello.into_raw() // 返回 CString 的指針,供其他語言使用
}
use libc::c_int
:
c_int
,它是一個與 C 語言中的 int
相對應的數據型別。當我們在 Rust 中需要傳遞整數給 C 語言的函數或從 C 語言接收整數時,必須使用 c_int
來保證數據的正確性。use std::ffi::CString
:
CString
是 Rust 標準庫中的一個結構體,用於創建 C 語言格式的字串。C 語言的字串是以 null
結尾的字元陣列,而 Rust 的字串(&str
)是 UTF-8 編碼且不以 null
結尾。因此,我們需要使用 CString
來將 Rust 的字串轉換成 C 語言格式,以便能夠通過 FFI 傳遞給其他語言使用。use std::os::raw::c_char
:
c_char
是 C 語言中的字元型別,它與 Rust 中的 char
不同。當我們要將 Rust 的字串傳遞給 C 語言或從 C 語言接收字串時,需要使用 c_char
來表示 C 語言中的單個字元。這行程式碼引入了 c_char
,以便在 FFI 函數中使用它來處理 C 語言的字串。#[no_mangle]
:
#[no_mangle]
指示編譯器不要對函數名稱進行修飾,這樣函數在編譯後的名稱仍然是我們在程式碼中定義的原始名稱。pub extern "C" fn add_two_numbers(a: c_int, b: c_int) -> c_int
:
add_two_numbers
,它接受兩個 C 語言的整數(c_int
)作為參數,並返回它們的和。pub
關鍵字表示這個函數是公開的,任何其他模組都可以調用它。extern "C"
告訴編譯器這是一個外部函數,它使用 C 語言的調用約定,這樣我們可以通過 FFI 將它導出給 C 或 Python 調用。pub extern "C" fn hello_from_rust() -> *const c_char
:
CString
來創建一個包含 "Hello from Rust!" 的 C 語言字串,並將這個字串的裸指針返回。這樣,其他語言可以通過這個指針來訪問這個字串。CString::new
用來創建字串,expect
方法用來處理可能出現的錯誤(例如字串中包含 null
字元)。hello.into_raw()
將 CString
轉換為裸指針,這是必需的,因為我們需要將這個字串傳遞給其他語言使用。接下來,我們要將 Rust 函數編譯成跨語言的動態庫
cargo build --release
當我們執行 cargo build --release
指令後,Rust 會將專案編譯成為一個動態共享庫。這些動態庫的名稱和格式取決於操作系統的類型:在 Linux 系統上,編譯結果會是 librust_python_ffi.so
;而在 Windows 系統上,則會是 librust_python_ffi.dll
。
rust_python_ffi/
├── Cargo.toml # 專案的配置文件
├── src/
│ └── lib.rs # Rust 函數定義
└── target/
└── release/
├── librust_python_ffi.so # Linux 上生成的動態共享庫
└── librust_python_ffi.dll # Windows 上生成的動態共享庫
接下來讓我們建立一個 rustFunction.py
並使用 Python 的 ctypes
模組來調用這些 Rust 函數:
import ctypes
# rust_lib = ctypes.CDLL('./target/release/rust_python_ffi.so') # Linux
rust_lib = ctypes.CDLL('./target/release/rust_python_ffi.dll') # Windows
# 調用 add_two_numbers
rust_lib.add_two_numbers.argtypes = [ctypes.c_int, ctypes.c_int]
rust_lib.add_two_numbers.restype = ctypes.c_int
result = rust_lib.add_two_numbers(5, 10)
print(f"5 + 10 = {result}")
# 調用 hello_from_rust
rust_lib.hello_from_rust.restype = ctypes.c_char_p
greeting = rust_lib.hello_from_rust()
print(greeting.decode('utf-8'))
我們直接嘗試執行這個 Python 程式,會得到以下結果
5 + 10 = 15
Hello from Rust!
這就是我們使用 Python 執行 Rust 函數的一個簡單範例,接下來我們攻守互換。
PyO3
首先,讓我們重新建立一個專案
cargo new rust_ffi_pyo3
準備 Rust 專案並安裝 PyO3
,我們可以直接在 Cargo.toml
中加入依賴:
[dependencies]
pyo3 = { version = "0.22.3", features = ["auto-initialize"] }
這樣就可以讓 Rust 自動初始化 Python 環境,並準備好執行 Python 程式碼。
現在,讓我們來看看如何在 Rust 程式中調用 Python 的 print
函數。這裡的目標是:讓 Rust 執行 Python 程式碼,就像平常在 .py
文件中執行一樣,我們建立一個 main.rs
,並寫入下面範例。
use pyo3::prelude::*; // 引入 PyO3 的功能
fn main() -> PyResult<()> {
// 這裡的 Python::with_gil 就像進入 Python 環境一樣,確保我們可以執行 Python 程式碼
Python::with_gil(|py| {
// 在 Python 中執行內建的 print 函數,顯示 "Hello from Python!"
py.eval_bound("print('Hello from Python!')", None, None)?;
Ok(())
})
}
Python::with_gil
:
py.eval_bound()
:
print('Hello from Python!')
。簡單來說,這行程式碼是把一段 Python 程式碼字串直接丟給 Python 解釋器來執行。我們可以像平常一樣用 Rust 執行這段程式碼:
cargo run
結果就會在終端中顯示:
Hello from Python!
我們還可以在 Rust 中調用 Python 的自定義函數。假設我們有一個簡單的 Python 程式 greet.py
,它包含一個問候函數:
# greet.py
def greet(name):
print(f"Hello, {name}!")
建立的 greet.py
位置如下
rust_ffi_pyo3/
├── Cargo.toml
├── src/
│ └── main.rs
└── greet.py # Python 文件放在項目根目錄
在 Rust 中,我們可以調用這個 Python 函數,就像平常在 Python 中調用函數一樣:
use pyo3::prelude::*; // 引入 PyO3 的基本功能
use pyo3::types::PyModule; // 引入 PyModule,用於操作 Python 模組
use std::fs; // 引入標準庫的檔案系統模組,用於讀取檔案
fn main() -> PyResult<()> {
Python::with_gil(|py| { // 進入 Python 的 GIL(全域鎖),確保安全地執行 Python 程式碼
// 讀取 greet.py 文件,並將其內容以字串的形式載入
let code = fs::read_to_string("./greet.py").expect("Failed to read greet.py");
// 從讀取到的 Python 程式碼字串中創建一個 Python 模組
let greet_module = PyModule::from_code(py, &code, "greet.py", "greet")?;
// 獲取 Python 模組中的 greet 函數,並調用該函數,傳入 "Rust" 作為參數
greet_module.getattr("greet")?.call1(("Rust",))?;
Ok(())
})
}
use pyo3::prelude::*
和 use pyo3::types::PyModule
:
fs::read_to_string("./greet.py")
:
greet.py
文件的內容,將其讀入為字串。如果讀取失敗,則拋出錯誤訊息 "Failed to read greet.py"。PyModule::from_code_bound
:
greet.py
檔案內容作為 Python 程式碼執行,並創建一個 Python 模組。getattr("greet")
:
greet
的函數。call1(("Rust",))
:
greet
函數並傳入一個參數 "Rust"
。當你運行這段 Rust 程式時,結果會顯示:
Hello, Rust!
用 FFI 把 Python 的自由度和 Rust 的高效能結合起來,是程式開發中的夢幻組合,你可以把重度運算交給 Rust 來加速,讓它跑得飛快,而複雜、靈活的邏輯處理,則交給 Python 來完成。這樣兩者互補,幫助你打造出高效又強大開發體驗。
不過,使用 FFI 時也要記得注意:數據類型的轉換要處理得當,記憶體管理也不能疏忽。而且,GIL(全域鎖)可能會在多執行緒下拖慢效能,要適時解決這些潛在問題。簡單來說:Rust 負責粗重工作(例如效能瓶頸的計算),Python 則掌控靈活的部分,這樣你就能享受兩者的優勢。
還等什麼呢?趕快開啟你的 FFI 之旅,感受 Rust 和 Python 合作的魔力吧!