| 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,
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(),
}
}
#[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
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(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<Vec<f64>> {
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)
}
#[pymodule]
fn my_geometry(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<Vec2>()?;
m.add_function(wrap_pyfunction!(batch_distances, m)?)?;
Ok(())
}
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 = 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)
}
}
use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_int, c_uchar};
use std::panic::catch_unwind;
use std::slice;
#[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 {
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)
}
#[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::<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。