--- name: rust-ffi-interop description: > Rust FFI 互操作技能。涵蓋 PyO3 0.23 Rust ↔ Python 互操作、 cbindgen 生成 C header、bindgen 綁定 C 函式庫、extern "C" ABI、 #[repr(C)] 記憶體佈局、CString/CStr 安全轉換、 null pointer 處理、panic boundary(catch_unwind)、 記憶體所有權跨語言邊界管理、WASM 目標編譯。 觸發關鍵詞:FFI, PyO3, cbindgen, bindgen, extern C, repr C, CString, C interop, Python binding, WASM, foreign function, shared library, cdylib, staticlib --- # Rust FFI 互操作(Foreign Function Interface) ## 適用場景 - 需要從 Python 呼叫 Rust 高效能邏輯(PyO3) - 需要將 Rust 函式庫封裝為 C ABI 供其他語言呼叫(cbindgen) - 需要在 Rust 中呼叫既有 C/C++ 函式庫(bindgen) - 需要產出 `.so` / `.dll` / `.dylib` 動態函式庫 - 跨語言邊界的記憶體安全設計與 panic 隔離 - 將 Rust 編譯為 WASM 供瀏覽器或 Node.js 使用 ## 核心知識 ### FFI 策略選擇表 | 需求 | 工具 | crate-type | 說明 | |------|------|-----------|------| | Rust → Python | PyO3 + maturin | `cdylib` | 產出 `.pyd`/`.so`,pip install 即用 | | Rust → C header | cbindgen | `cdylib`/`staticlib` | 自動從 Rust 原始碼產生 `.h` | | C lib → Rust | bindgen | - | 從 `.h` 自動產生 Rust FFI 繫結 | | Rust → WASM | wasm-bindgen | `cdylib` | 搭配 wasm-pack 打包 | | 手動 FFI | `extern "C"` | `cdylib`/`staticlib` | 最底層控制 | ### PyO3 基礎流程 1. `Cargo.toml` 加入 `pyo3` 並設定 `crate-type = ["cdylib"]` 2. 使用 `#[pyfunction]`、`#[pyclass]`、`#[pymethods]` 標記 3. 以 `#[pymodule]` 組裝模組 4. 用 `maturin develop` 本機安裝或 `maturin build --release` 產出 wheel 5. Python 端直接 `import` 使用 **PyO3 0.23 重點變更:** - `Bound<'py, T>` 取代舊版 `&PyAny`,生命週期更明確 - `#[pymodule]` 可搭配宣告式巨集 `#[pymodule_init]` - 錯誤轉換透過 `PyErr::new::(msg)` 或實作 `From for PyErr` ### cbindgen / bindgen 工具鏈 **cbindgen(Rust → C header):** ```toml # cbindgen.toml language = "C" include_guard = "MY_LIB_H" no_includes = true ``` 執行 `cbindgen --config cbindgen.toml --crate my_lib --output my_lib.h` **bindgen(C header → Rust):** ```rust // build.rs fn main() { println!("cargo:rustc-link-lib=foo"); let bindings = bindgen::Builder::default() .header("wrapper.h") .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) .generate() .expect("無法產生繫結"); let out = std::env::var("OUT_DIR").unwrap(); bindings.write_to_file(std::path::PathBuf::from(out).join("bindings.rs")) .expect("寫入繫結失敗"); } ``` ### `#[repr(C)]` 與記憶體佈局 ```rust // 確保欄位順序與 C struct 一致,無填充重排 #[repr(C)] pub struct Point { pub x: f64, pub y: f64, } ``` **關鍵規則:** - FFI struct **必須** 標註 `#[repr(C)]`,否則 Rust 編譯器可能重排欄位 - 使用 `#[repr(C, packed)]` 可移除所有 padding(需小心對齊問題) - 列舉跨 FFI 應使用 `#[repr(i32)]` 或 `#[repr(u8)]` 明確指定底層型別 - `bool` 在 FFI 中應改用 `i32` 或 `u8`,因為 C `_Bool` 與 Rust `bool` 語意微妙差異 ### 跨語言所有權規則 | 配置方 | 釋放方 | 策略 | |--------|--------|------| | Rust | Rust | 提供 `free_*()` 函式,由外部語言呼叫 | | C | C | Rust 只借用指標,不釋放 | | Rust (Box) | 外部 | `Box::into_raw()` 轉移;回收時 `Box::from_raw()` | | C (malloc) | 外部 | Rust 用完後呼叫 C 的 `free()` | **CString / CStr 安全轉換:** ```rust // Rust String → C 字串(配置在 Rust 端) let c_string = std::ffi::CString::new("hello").expect("含有 null byte"); let ptr = c_string.into_raw(); // 所有權轉給外部 // 回收 unsafe { let _ = CString::from_raw(ptr); } // C 字串 → Rust(借用,不轉移所有權) unsafe { let c_str = std::ffi::CStr::from_ptr(ptr); let rust_str = c_str.to_str().expect("無效 UTF-8"); } ``` ### Panic Safety(catch_unwind) Rust panic 跨 FFI 邊界是**未定義行為**。所有 `extern "C"` 函式必須攔截 panic: ```rust use std::panic::catch_unwind; #[no_mangle] pub extern "C" fn safe_entry(x: i32) -> i32 { match catch_unwind(|| { // 可能 panic 的邏輯 if x < 0 { panic!("負數輸入"); } x * 2 }) { Ok(val) => val, Err(_) => -1, // 錯誤哨兵值 } } ``` **注意:** `catch_unwind` 只能攔截 `panic!`(unwind),無法攔截 `abort`。 設定 `panic = "abort"` 時 `catch_unwind` 無效。 ## 程式碼範例 ### Basic:extern "C" 函式 + CString + #[repr(C)] ```rust // 見 examples/basic.rs use std::ffi::{CStr, CString}; use std::os::raw::c_char; /// C 相容的二維點結構 #[repr(C)] pub struct Point { pub x: f64, pub y: f64, } /// 計算兩點之間的歐幾里得距離 #[no_mangle] pub extern "C" fn point_distance(a: &Point, b: &Point) -> f64 { ((a.x - b.x).powi(2) + (a.y - b.y).powi(2)).sqrt() } /// 從 C 字串建立問候訊息,回傳新配置的 C 字串 /// 呼叫方必須以 `greeting_free` 釋放回傳值 #[no_mangle] pub extern "C" fn greeting_new(name: *const c_char) -> *mut c_char { if name.is_null() { return std::ptr::null_mut(); } let c_str = unsafe { CStr::from_ptr(name) }; let name_str = match c_str.to_str() { Ok(s) => s, Err(_) => return std::ptr::null_mut(), }; let msg = format!("你好, {}!", name_str); match CString::new(msg) { Ok(cs) => cs.into_raw(), Err(_) => std::ptr::null_mut(), } } /// 釋放由 `greeting_new` 配置的 C 字串 #[no_mangle] pub extern "C" fn greeting_free(ptr: *mut c_char) { if !ptr.is_null() { unsafe { let _ = CString::from_raw(ptr); } } } ``` ### Intermediate:PyO3 Python Module ```rust // 見 examples/intermediate.rs use pyo3::prelude::*; use pyo3::exceptions::PyValueError; /// 二維向量,可從 Python 直接建構與操作 #[pyclass] #[derive(Clone)] struct Vec2 { #[pyo3(get, set)] x: f64, #[pyo3(get, set)] y: f64, } #[pymethods] impl Vec2 { #[new] fn new(x: f64, y: f64) -> Self { Self { x, y } } /// 向量長度 fn magnitude(&self) -> f64 { (self.x * self.x + self.y * self.y).sqrt() } /// 回傳單位向量;零向量時拋出 ValueError fn normalize(&self) -> PyResult { let mag = self.magnitude(); if mag < f64::EPSILON { return Err(PyValueError::new_err("無法正規化零向量")); } Ok(Self { x: self.x / mag, y: self.y / mag }) } fn __repr__(&self) -> String { format!("Vec2(x={}, y={})", self.x, self.y) } } /// 批次計算點對之間的距離 #[pyfunction] fn batch_distances(points: Vec<(f64, f64)>) -> PyResult> { if points.len() % 2 != 0 { return Err(PyValueError::new_err("點數必須為偶數")); } let dists = points.chunks(2).map(|pair| { let (x1, y1) = pair[0]; let (x2, y2) = pair[1]; ((x1 - x2).powi(2) + (y1 - y2).powi(2)).sqrt() }).collect(); Ok(dists) } /// Python 模組進入點 #[pymodule] fn my_geometry(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_function(wrap_pyfunction!(batch_distances, m)?)?; Ok(()) } ``` ### Advanced:bindgen + cbindgen + catch_unwind ```rust // 見 examples/advanced.rs — bindgen 繫結 + 安全包裝 + panic 邊界 // --- build.rs(bindgen 從 C header 產生繫結)--- // fn main() { // println!("cargo:rustc-link-lib=zlib"); // let bindings = bindgen::Builder::default() // .header("wrapper.h") // #include // .allowlist_function("compress|uncompress|zlibVersion") // .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) // .generate().expect("bindgen 失敗"); // let out = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap()); // bindings.write_to_file(out.join("zlib_bindings.rs")).unwrap(); // } // --- 安全包裝層 --- mod zlib_safe { #![allow(non_upper_case_globals, non_camel_case_types)] include!(concat!(env!("OUT_DIR"), "/zlib_bindings.rs")); use std::ptr; /// 對 zlib compress 的安全封裝 pub fn compress_data(input: &[u8]) -> Result, String> { let mut dest_len = (input.len() + 128) as libc::c_ulong; let mut dest = vec![0u8; dest_len as usize]; let ret = unsafe { compress( dest.as_mut_ptr(), &mut dest_len as *mut _, input.as_ptr(), input.len() as libc::c_ulong, ) }; if ret != 0 { return Err(format!("壓縮失敗,錯誤碼: {}", ret)); } dest.truncate(dest_len as usize); Ok(dest) } } // --- cbindgen 匯出層(帶 catch_unwind)--- use std::ffi::{CStr, CString}; use std::os::raw::{c_char, c_int, c_uchar}; use std::panic::catch_unwind; use std::slice; /// 回傳壓縮後的資料;成功回傳 0,失敗回傳 -1 /// `out_len` 會被寫入實際輸出長度 #[no_mangle] pub extern "C" fn mylib_compress( input: *const c_uchar, input_len: usize, output: *mut c_uchar, output_cap: usize, out_len: *mut usize, ) -> c_int { // 攔截所有 panic,防止跨 FFI 傳播 let result = catch_unwind(|| { // 空指標檢查 if input.is_null() || output.is_null() || out_len.is_null() { return -1; } let in_slice = unsafe { slice::from_raw_parts(input, input_len) }; match zlib_safe::compress_data(in_slice) { Ok(compressed) => { if compressed.len() > output_cap { return -2; // 緩衝區不足 } unsafe { ptr::copy_nonoverlapping( compressed.as_ptr(), output, compressed.len() ); *out_len = compressed.len(); } 0 } Err(_) => -1, } }); result.unwrap_or(-99) // panic 時回傳 -99 } /// 釋放由本函式庫配置的字串 #[no_mangle] pub extern "C" fn mylib_free_string(ptr: *mut c_char) { if !ptr.is_null() { unsafe { let _ = CString::from_raw(ptr); } } } ``` ## 常見錯誤對照表 | 錯誤訊息 / 症狀 | 原因 | 修復方式 | |-----------------|------|---------| | `undefined symbol: my_func` | 缺少 `#[no_mangle]` 或 `extern "C"`,或 crate-type 未設定 `cdylib` | 確認 `#[no_mangle] pub extern "C"`,`Cargo.toml` 含 `crate-type = ["cdylib"]` | | Segfault(呼叫時崩潰) | 傳入 null 指標未檢查,或 `CStr::from_ptr` 收到非法指標 | 進入函式後立即檢查 `ptr.is_null()`,不信任外部指標有效性 | | `panic across FFI boundary is UB` | Rust panic 未被攔截就跨越 `extern "C"` 邊界 | 所有 FFI 入口包裹 `std::panic::catch_unwind`,回傳錯誤碼 | | 記憶體洩漏(Valgrind 報告) | `CString::into_raw()` 後未提供對應的 `free` 函式 | 匯出配對的 `*_free()` 函式,文件中明確標示呼叫方責任 | | `expected struct X, found struct X`(型別不匹配) | C 端 struct 佈局與 Rust 不一致,缺少 `#[repr(C)]` | FFI struct 必須加 `#[repr(C)]`;用 `cbindgen` 自動產生 header 確保一致 | | `thread 'main' panicked: interior nul byte` | `CString::new()` 的輸入含 `\0` | 先過濾或以 `CString::new(s)` 的 `Err` 分支處理 | | PyO3: `PyErr { type: TypeError }` | Python 傳入型別與 Rust 函式簽名不匹配 | 使用 `PyAny` + 手動 `extract::()` 並回傳明確錯誤訊息 | | bindgen: `fatal error: header not found` | `wrapper.h` 路徑錯誤或系統缺乏 C library 的 `-dev` 套件 | 安裝開發套件(如 `apt install libz-dev`),確認 `build.rs` 路徑 | ## Cargo.toml 依賴模板 ```toml [package] name = "my_ffi_lib" version = "0.1.0" edition = "2024" [lib] # cdylib: 動態函式庫(.so/.dll/.dylib),供 C/Python/其他語言載入 # staticlib: 靜態函式庫(.a),鏈結進 C/C++ 專案 crate-type = ["cdylib", "staticlib"] [dependencies] # PyO3:Rust ↔ Python 互操作框架 pyo3 = { version = "0.23", features = ["extension-module"] } # libc:C 標準型別定義 libc = "0.2" [build-dependencies] # bindgen:從 C header 自動產生 Rust FFI 繫結 bindgen = "0.71" # cbindgen:從 Rust 原始碼自動產生 C header cbindgen = "0.27" # --- 單獨使用時可選擇性啟用 --- # [features] # python = ["pyo3/extension-module"] # c-bindings = [] ``` **maturin 搭配 PyO3 的 `pyproject.toml`:** ```toml [build-system] requires = ["maturin>=1.7,<2"] build-backend = "maturin" [project] name = "my_geometry" requires-python = ">=3.9" [tool.maturin] features = ["pyo3/extension-module"] ``` ## 參考來源 | # | 來源 | 說明 | |---|------|------| | 1 | [The Rustonomicon — FFI](https://doc.rust-lang.org/nomicon/ffi.html) | 官方 FFI 不安全操作完整指南 | | 2 | [PyO3 User Guide](https://pyo3.rs/v0.23/) | PyO3 0.23 官方文件 | | 3 | [cbindgen User Guide](https://github.com/mozilla/cbindgen/blob/master/docs.md) | Mozilla 維護的 C header 產生器文件 | | 4 | [bindgen User Guide](https://rust-lang.github.io/rust-bindgen/) | 官方 C → Rust 繫結產生器指南 | | 5 | [Rust Reference — Type Layout](https://doc.rust-lang.org/reference/type-layout.html) | repr(C) 與記憶體佈局官方規格 | | 6 | [maturin Documentation](https://www.maturin.rs/) | PyO3 建置工具 maturin 完整文件 | 詳細來源清單見 `references/sources.md`。