| 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 基礎流程
Cargo.toml 加入 pyo3 並設定 crate-type = ["cdylib"]
- 使用
#[pyfunction]、#[pyclass]、#[pymethods] 標記
- 以
#[pymodule] 組裝模組
- 用
maturin develop 本機安裝或 maturin build --release 產出 wheel
- Python 端直接
import 使用
PyO3 0.23 重點變更:
Bound<'py, T> 取代舊版 &PyAny,生命週期更明確
#[pymodule] 可搭配宣告式巨集 #[pymodule_init]
- 錯誤轉換透過
PyErr::new::<PyValueError, _>(msg) 或實作 From<E> for PyErr
cbindgen / bindgen 工具鏈
cbindgen(Rust → C header):
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):
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)] 與記憶體佈局
#[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 安全轉換:
let c_string = std::ffi::CString::new("hello").expect("含有 null byte");
let ptr = c_string.into_raw();
unsafe { let _ = CString::from_raw(ptr); }
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:
use std::panic::catch_unwind;
#[no_mangle]
pub extern "C" fn safe_entry(x: i32) -> i32 {
match catch_unwind(|| {
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)]
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
#[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()
}
#[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,
(_) => std::ptr::(),
};
= (, name_str);
CString::(msg) {
(cs) => cs.(),
(_) => std::ptr::(),
}
}
(ptr: * c_char) {
!ptr.() {
{ = CString::(ptr); }
}
}
Intermediate:PyO3 Python Module
use pyo3::prelude::*;
use pyo3::exceptions::PyValueError;
#[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()
}
fn normalize(&self) -> PyResult<Self> {
let mag = self.magnitude();
if mag < f64::EPSILON {
return Err(PyValueError::new_err("無法正規化零向量"));
}
Ok( { x: .x / mag, y: .y / mag })
}
(&) {
(, .x, .y)
}
}
(points: <(, )>) PyResult<<>> {
points.() % != {
(PyValueError::());
}
= points.().(|pair| {
(x1, y1) = pair[];
(x2, y2) = pair[];
((x1 - x2).() + (y1 - y2).()).()
}).();
(dists)
}
(m: &Bound<, PyModule>) PyResult<()> {
m.add_class::<Vec2>()?;
m.(wrap_pyfunction!(batch_distances, m)?)?;
(())
}
Advanced:bindgen + cbindgen + catch_unwind
mod zlib_safe {
#![allow(non_upper_case_globals, non_camel_case_types)]
include!(concat!(env!("OUT_DIR"), "/zlib_bindings.rs"));
use std::ptr;
pub fn compress_data(input: &[u8]) -> Result<Vec<u8>, String> {
let mut dest_len = (input.len() + 128) as libc::c_ulong;
let mut dest = vec![0u8; dest_len as usize];
let ret = {
(
dest.(),
& dest_len * _,
input.(),
input.() libc::c_ulong,
)
};
ret != {
((, ret));
}
dest.(dest_len );
(dest)
}
}
std::ffi::{CStr, CString};
std::os::raw::{c_char, c_int, c_uchar};
std::panic::catch_unwind;
std::slice;
(
input: * c_uchar,
input_len: ,
output: * c_uchar,
output_cap: ,
out_len: * ,
) c_int {
= (|| {
input.() || output.() || out_len.() {
-;
}
= { slice::(input, input_len) };
zlib_safe::(in_slice) {
(compressed) => {
compressed.() > output_cap {
-;
}
{
ptr::(
compressed.(), output, compressed.()
);
*out_len = compressed.();
}
}
(_) => -,
}
});
result.(-)
}
(ptr: * c_char) {
!ptr.() {
{ = CString::(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::<T>() 並回傳明確錯誤訊息 |
bindgen: fatal error: header not found | wrapper.h 路徑錯誤或系統缺乏 C library 的 -dev 套件 | 安裝開發套件(如 apt install libz-dev),確認 build.rs 路徑 |
Cargo.toml 依賴模板
[package]
name = "my_ffi_lib"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib", "staticlib"]
[dependencies]
pyo3 = { version = "0.23", features = ["extension-module"] }
libc = "0.2"
[build-dependencies]
bindgen = "0.71"
cbindgen = "0.27"
maturin 搭配 PyO3 的 pyproject.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"]
參考來源
詳細來源清單見 references/sources.md。