Mode d’emploi
- Copiez la commande et examinez le paquet avant de l’installer dans votre environnement agent.
- Consultez le document original pour les outils, identifiants et environnements requis.
- Décrivez les entrées et le livrable souhaité ; comparez la sortie aux sources.
npx skills add https://github.com/luckyegg168/rust-skill --skill rust-ffi-interopAvant de commencer
- L’installation n’inclut ni abonnement au modèle, ni quota API, ni accès aux fichiers.
- Étoiles et installations reflètent l’activité, pas un test indépendant de qualité ou de sécurité.
Rendu depuis le dépôt source en conservant titres, exemples, code, tableaux, liens et images.
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):
# 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):
// 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)] 與記憶體佈局
// 確保欄位順序與 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與 Rustbool語意微妙差異
跨語言所有權規則
| 配置方 | 釋放方 | 策略 |
|---|---|---|
| Rust | Rust | 提供 free_*() 函式,由外部語言呼叫 |
| C | C | Rust 只借用指標,不釋放 |
| Rust (Box) | 外部 | Box::into_raw() 轉移;回收時 Box::from_raw() |
| C (malloc) | 外部 | Rust 用完後呼叫 C 的 free() |
CString / CStr 安全轉換:
// 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:
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)]
// 見 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
// 見 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<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)
}
/// Python 模組進入點
#[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
// 見 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 <zlib.h>
// .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<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)
}
}
// --- 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::<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]
# 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`:
[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 | 官方 FFI 不安全操作完整指南 |
| 2 | PyO3 User Guide | PyO3 0.23 官方文件 |
| 3 | cbindgen User Guide | Mozilla 維護的 C header 產生器文件 |
| 4 | bindgen User Guide | 官方 C → Rust 繫結產生器指南 |
| 5 | Rust Reference — Type Layout | repr(C) 與記憶體佈局官方規格 |
| 6 | maturin Documentation | PyO3 建置工具 maturin 完整文件 |
詳細來源清單見 references/sources.md。

