Rust 2026 经验谈 - FFI 实战:C 调用 Rust
上一篇我们讨论了 Rust 调用 C——用 bindgen 生成绑定、处理布局和回调。本文反过来:让 C 调用 Rust。这不是”把 Rust 代码抄一遍”那么简单——你需要设计 C 友好的 API、用 cbindgen 导出头文件、管理 opaque 类型的生命周期、把 cargo 构建嵌入 CMake 系统。实战中踩坑极多,本文系统总结。
cbindgen 导出 C 头文件
为什么需要 cbindgen
当 Rust 库要被 C 调用时,C 侧需要一个头文件声明导出的函数和类型。手写头文件极易与 Rust 侧脱节——改了 Rust 签名忘了改 .h,编译通过但运行时 UB。cbindgen 从 Rust 源码自动生成 C 头文件,保证同步。
基本用法
[package]name = "my_lib"version = "0.1.0"edition = "2024"lib = { crate-type = ["cdylib", "staticlib"] }
[build-dependencies]cbindgen = "0.27"fn main() { let crate_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
cbindgen::Builder::new() .with_crate(crate_dir) .with_language(cbindgen::Language::C) .generate() .expect("Unable to generate bindings") .write_to_file("include/my_lib.h");}#[unsafe(no_mangle)]pub extern "C" fn my_lib_add(a: i32, b: i32) -> i32 { a + b}
#[unsafe(no_mangle)]pub extern "C" fn my_lib_greet(name: *const i8) { if name.is_null() { return; } let c_str = unsafe { std::ffi::CStr::from_ptr(name) }; let rust_str = c_str.to_string_lossy(); println!("Hello, {}!", rust_str);}cbindgen 生成的头文件:
#ifndef MY_LIB_H#define MY_LIB_H
#include <stdint.h>#include <stdbool.h>
int32_t my_lib_add(int32_t a, int32_t b);void my_lib_greet(const char *name);
#endif /* MY_LIB_H */cbindgen 配置
cbindgen 支持 cbindgen.toml 配置文件,放在 crate 根目录:
language = "C"include_guard = "MY_LIB_H"autogen_warning = "/* Warning: this file is auto-generated by cbindgen. Do not modify. */"no_include = truesys_includes = ["stdint.h", "stdbool.h", "stddef.h"]includes = ["my_lib_types.h"]tab_width = 4style = "both"
[defines]"feature = logging" = "MY_LIB_LOGGING"
[export]prefix = "MyLib"include = ["MyContext", "MyConfig"]exclude = ["InternalState"]在 build.rs 中使用配置文件:
fn main() { let crate_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let config = cbindgen::Config::from_file("cbindgen.toml") .expect("Unable to read cbindgen.toml");
cbindgen::Builder::new() .with_crate(crate_dir) .with_config(config) .generate() .expect("Unable to generate bindings") .write_to_file("include/my_lib.h");}踩坑:cbindgen 不看条件编译
// cbindgen 会忽略 #[cfg],总是生成所有符号的声明#[cfg(feature = "experimental")]#[unsafe(no_mangle)]pub extern "C" fn my_lib_experimental() {}
// 生成的 .h 中总是包含 my_lib_experimental 声明// 但如果 feature 没开,链接时找不到符号——C 侧编译失败
// 变通:用 cbindgen.toml 的 [defines] 或在 build.rs 中动态配置#[unsafe(no_mangle)] + extern “C” 导出函数约定
基本规则
#[unsafe(no_mangle)]pub extern "C" fn exported_function(x: i32) -> i32 { x * 2}#[unsafe(no_mangle)]:阻止 Rust 的 name mangling,让链接器看到exported_function而非_ZN11my_lib17exported_functionE之类的符号。Rust 2024 Edition 要求把no_mangle写成 unsafe attribute,因为开发者必须保证导出符号名不会和其他库冲突。pub extern "C":使用 C 调用约定(cdecl),保证 C 侧能正确调用pub:符号必须对链接器可见
可用类型
extern "C" 函数的参数和返回值只能是 C 兼容类型:
| Rust 类型 | C 类型 | 说明 |
|---|---|---|
i32 / u32 | int32_t / uint32_t | 固定宽度 |
i64 / u64 | int64_t / uint64_t | 固定宽度 |
f32 / f64 | float / double | 浮点 |
*mut T / *const T | T* / const T* | 原始指针 |
bool | bool(C23) | 注意 ABI 兼容性 |
c_int, c_long 等 | int, long 等 | 平台相关 |
不兼容的类型(编译器会报错或产生 UB):
// 错误!String 不是 C 兼容类型// #[unsafe(no_mangle)]// pub extern "C" fn bad(s: String) {}
// 错误!&str 不是 C 兼容类型// #[unsafe(no_mangle)]// pub extern "C" fn bad2(s: &str) {}
// 错误!泛型在 extern "C" 中不允许// #[unsafe(no_mangle)]// pub extern "C" fn bad3<T>(t: T) {}踩坑:Rust bool vs C bool
// Rust 的 bool 是 1 字节,值为 0 或 1// C99 没有 bool,C11 的 _Bool 也是 1 字节// 但很多 C 代码用 int 表示布尔值——任何非零为 true
// 危险!C 传 int 2 作为 bool,Rust 视为 true 但可能 UB#[unsafe(no_mangle)]pub extern "C" fn my_lib_set_enabled(enabled: bool) { // 如果 C 侧传了 2 作为 bool: // Rust 保证 bool 必须是 0 或 1 // 传其他值是 UB! if enabled { println!("enabled"); }}
// 安全做法:C 侧用 c_int,Rust 侧转换#[unsafe(no_mangle)]pub extern "C" fn my_lib_set_enabled_safe(enabled: std::os::raw::c_int) { let enabled = enabled != 0; if enabled { println!("enabled"); }}导出 opaque 类型:Box 透传为 void*
opaque 模式
当 C 侧只需持有指针而不需访问内部字段时,用 opaque 类型。Rust 侧用 Box<T> 管理内存,C 侧只看到 void*:
pub struct MyContext { data: Vec<u8>, config: Config, connected: bool,}
pub struct Config { max_size: usize, timeout_ms: u64,}
#[unsafe(no_mangle)]pub extern "C" fn my_lib_context_create(max_size: usize, timeout_ms: u64) -> *mut MyContext { let ctx = Box::new(MyContext { data: Vec::with_capacity(max_size), config: Config { max_size, timeout_ms }, connected: false, }); Box::into_raw(ctx)}
#[unsafe(no_mangle)]pub extern "C" fn my_lib_context_destroy(ctx: *mut MyContext) { if !ctx.is_null() { unsafe { drop(Box::from_raw(ctx)); } }}
#[unsafe(no_mangle)]pub extern "C" fn my_lib_context_connect(ctx: *mut MyContext) -> i32 { if ctx.is_null() { return -1; } let ctx = unsafe { &mut *ctx }; ctx.connected = true; 0}
#[unsafe(no_mangle)]pub extern "C" fn my_lib_context_write(ctx: *mut MyContext, data: *const u8, len: usize) -> i32 { if ctx.is_null() || data.is_null() { return -1; } let ctx = unsafe { &mut *ctx }; let slice = unsafe { std::slice::from_raw_parts(data, len) }; if slice.len() > ctx.config.max_size { return -2; } ctx.data.extend_from_slice(slice); 0}cbindgen 生成的头文件:
typedef struct MyContext MyContext;
MyContext *my_lib_context_create(size_t max_size, uint64_t timeout_ms);void my_lib_context_destroy(MyContext *ctx);int32_t my_lib_context_connect(MyContext *ctx);int32_t my_lib_context_write(MyContext *ctx, const uint8_t *data, size_t len);C 侧不透明——只知道 MyContext*,无法访问字段。
踩坑:Box::from_raw 必须用同一类型
struct Inner { x: i32 }struct Wrapper { inner: Inner, extra: i32 }
#[unsafe(no_mangle)]pub extern "C" fn create() -> *mut Wrapper { Box::into_raw(Box::new(Wrapper { inner: Inner { x: 0 }, extra: 0 }))}
// 错误!用 Inner 释放 Wrapper// #[unsafe(no_mangle)]// pub extern "C" fn destroy_bad(ptr: *mut Inner) {// unsafe { drop(Box::from_raw(ptr)); } // 类型不匹配——UB!// }
// 正确:类型必须一致#[unsafe(no_mangle)]pub extern "C" fn destroy(ptr: *mut Wrapper) { if !ptr.is_null() { unsafe { drop(Box::from_raw(ptr)); } }}踩坑:多线程访问 opaque 指针
use std::sync::Mutex;
pub struct SharedContext { inner: Mutex<ContextInner>,}
struct ContextInner { data: Vec<u8>, count: u64,}
#[unsafe(no_mangle)]pub extern "C" fn my_lib_shared_create() -> *mut SharedContext { let ctx = Box::new(SharedContext { inner: Mutex::new(ContextInner { data: Vec::new(), count: 0, }), }); Box::into_raw(ctx)}
#[unsafe(no_mangle)]pub extern "C" fn my_lib_shared_increment(ctx: *mut SharedContext) -> u64 { if ctx.is_null() { return 0; } let ctx = unsafe { &*ctx }; let mut inner = ctx.inner.lock().unwrap(); inner.count += 1; inner.count}关键:&*ctx 获取 &SharedContext(共享引用),Mutex 提供内部可变性。不要用 &mut *ctx——多个 C 线程可能同时调用。
C API 设计模式
错误码返回
pub const MY_LIB_OK: i32 = 0;pub const MY_LIB_ERR_NULL: i32 = -1;pub const MY_LIB_ERR_OOM: i32 = -2;pub const MY_LIB_ERR_INVALID: i32 = -3;pub const MY_LIB_ERR_IO: i32 = -4;
#[unsafe(no_mangle)]pub extern "C" fn my_lib_process(ctx: *mut MyContext, input: *const u8, input_len: usize) -> i32 { if ctx.is_null() { return MY_LIB_ERR_NULL; } if input.is_null() && input_len > 0 { return MY_LIB_ERR_INVALID; }
let ctx = unsafe { &mut *ctx }; let data = if input.is_null() || input_len == 0 { &[] } else { unsafe { std::slice::from_raw_parts(input, input_len) } };
match ctx.process(data) { Ok(()) => MY_LIB_OK, Err(e) => match e { MyError::OutOfMemory => MY_LIB_ERR_OOM, MyError::InvalidInput => MY_LIB_ERR_INVALID, MyError::Io(_) => MY_LIB_ERR_IO, }, }}错误信息获取
use std::ffi::CString;use std::sync::atomic::{AtomicPtr, Ordering};
static LAST_ERROR: AtomicPtr<i8> = AtomicPtr::new(std::ptr::null_mut());
fn set_last_error(msg: &str) { let c_string = CString::new(msg).unwrap_or_else(|_| CString::new("error contains null byte").unwrap()); let ptr = c_string.into_raw(); let old = LAST_ERROR.swap(ptr, Ordering::SeqCst); if !old.is_null() { unsafe { drop(CString::from_raw(old)); } }}
#[unsafe(no_mangle)]pub extern "C" fn my_lib_get_last_error() -> *const i8 { LAST_ERROR.load(Ordering::SeqCst)}
#[unsafe(no_mangle)]pub extern "C" fn my_lib_clear_error() { let old = LAST_ERROR.swap(std::ptr::null_mut(), Ordering::SeqCst); if !old.is_null() { unsafe { drop(CString::from_raw(old)); } }}init / destroy 生命周期
pub struct MyLib { contexts: Vec<*mut MyContext>,}
static mut GLOBAL_INSTANCE: *mut MyLib = std::ptr::null_mut();
#[unsafe(no_mangle)]pub extern "C" fn my_lib_init() -> i32 { let lib = Box::new(MyLib { contexts: Vec::new(), }); unsafe { if !GLOBAL_INSTANCE.is_null() { return -1; // already initialized } GLOBAL_INSTANCE = Box::into_raw(lib); } 0}
#[unsafe(no_mangle)]pub extern "C" fn my_lib_destroy() { unsafe { if GLOBAL_INSTANCE.is_null() { return; } let lib = Box::from_raw(GLOBAL_INSTANCE); for &ctx in &lib.contexts { if !ctx.is_null() { drop(Box::from_raw(ctx)); } } GLOBAL_INSTANCE = std::ptr::null_mut(); }}更安全的替代:用 Option<Box<...>> 和 OnceLock:
use std::sync::OnceLock;
static GLOBAL: OnceLock<MyLib> = OnceLock::new();
pub struct MyLib { initialized: bool,}
#[unsafe(no_mangle)]pub extern "C" fn my_lib_init() -> i32 { match GLOBAL.set(MyLib { initialized: true }) { Ok(()) => 0, Err(_) => -1, }}输出参数模式
#[unsafe(no_mangle)]pub extern "C" fn my_lib_compute(ctx: *mut MyContext, result: *mut i32) -> i32 { if ctx.is_null() || result.is_null() { return MY_LIB_ERR_NULL; } let ctx = unsafe { &*ctx }; let value = ctx.compute(); unsafe { *result = value; } MY_LIB_OK}C 侧使用:
int32_t result;int rc = my_lib_compute(ctx, &result);if (rc != MY_LIB_OK) { const char *err = my_lib_get_last_error(); fprintf(stderr, "Error: %s\n", err);}跨语言构建系统集成:CMake 调用 cargo
方案一:ExternalProject_Add
cmake_minimum_required(VERSION 3.20)project(MyApp C)
include(ExternalProject)
set(RUST_LIB_DIR "${CMAKE_BINARY_DIR}/rust-target")
ExternalProject_Add( my_rust_lib SOURCE_DIR "${CMAKE_SOURCE_DIR}/rust/" BUILD_IN_SOURCE 1 CONFIGURE_COMMAND "" BUILD_COMMAND cargo build --release --target-dir ${RUST_LIB_DIR} INSTALL_COMMAND "")
set(RUST_LIB_PATH "${RUST_LIB_DIR}/release/libmy_lib.a")add_library(my_lib STATIC IMPORTED)set_target_properties(my_lib PROPERTIES IMPORTED_LOCATION ${RUST_LIB_PATH})add_dependencies(my_lib my_rust_lib)
add_executable(my_app main.c)target_link_libraries(my_app my_lib)target_include_directories(my_app PRIVATE "${CMAKE_SOURCE_DIR}/rust/include")方案二:corrosion(推荐)
corrosion 是专门的 CMake-Cargo 集成工具:
cmake_minimum_required(VERSION 3.20)project(MyApp C)
# Fetch corrosionFetchContent_Declare( corrosion GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git GIT_TAG v0.5)FetchContent_MakeAvailable(corrosion)
# 导入 Rust cratecorrosion_import_crate( MANIFEST_PATH rust/Cargo.toml CRATE_TYPES staticlib)
add_executable(my_app main.c)target_link_libraries(my_app my_lib)target_include_directories(my_app PRIVATE "${CMAKE_SOURCE_DIR}/rust/include")corrosion 的优势:
- 自动处理 cargo 构建目录
- 自动链接 Rust 静态库
- 支持交叉编译
- 支持 profile 和 feature 传递
方案三:cargo 构建脚本 + CMake 消费
# 先在 CI 中构建 Rust 库# cargo build --release -p my_lib
# CMakeLists.txt 只链接产物cmake_minimum_required(VERSION 3.20)project(MyApp C)
add_executable(my_app main.c)
# 假设 cargo 产物在 ${RUST_OUTPUT_DIR}target_link_directories(my_app PRIVATE ${RUST_OUTPUT_DIR})target_link_libraries(my_app my_lib pthread dl m)target_include_directories(my_app PRIVATE include)踩坑:链接 Rust 静态库需要系统库
Rust 静态库依赖系统库(Linux 上是 pthread、dl、m),漏链会报 undefined reference:
if(UNIX AND NOT APPLE) target_link_libraries(my_app my_lib pthread dl m)elseif(APPLE) target_link_libraries(my_app my_lib dl)endif()踩坑:Windows 上的 MSVC vs GNU
# Rust 默认用 MSVC 工具链(windows-msvc)# 如果 C 代码用 MinGW 编译——ABI 不兼容!# 确保两侧使用同一工具链
# 检查 Rust 工具链# rustup target list --installed# rustup default stable-x86_64-pc-windows-msvc
# CMake 中强制 MSVCif(MSVC) # OK:匹配else() message(WARNING "C compiler is not MSVC, may not match Rust toolchain")endif()实战:将 Rust 库封装为 C SDK
项目结构
my-sdk/├── Cargo.toml├── cbindgen.toml├── build.rs├── src/│ └── lib.rs├── include/│ └── my_sdk.h # cbindgen 生成├── examples/│ └── c/│ ├── CMakeLists.txt│ └── main.c└── tests/ └── c_test.cRust 核心
use std::ffi::{CStr, CString};use std::os::raw::c_int;
pub struct MySdk { buffer: Vec<u8>, capacity: usize,}
#[repr(C)]pub struct MySdkResult { pub data: *const u8, pub len: usize, pub error_code: c_int,}
const MY_SDK_OK: c_int = 0;const MY_SDK_ERR_NULL: c_int = -1;const MY_SDK_ERR_CAPACITY: c_int = -2;const MY_SDK_ERR_INVALID: c_int = -3;
#[unsafe(no_mangle)]pub extern "C" fn my_sdk_create(capacity: usize) -> *mut MySdk { let sdk = Box::new(MySdk { buffer: Vec::with_capacity(capacity), capacity, }); Box::into_raw(sdk)}
#[unsafe(no_mangle)]pub extern "C" fn my_sdk_destroy(sdk: *mut MySdk) { if !sdk.is_null() { unsafe { drop(Box::from_raw(sdk)); } }}
#[unsafe(no_mangle)]pub extern "C" fn my_sdk_append(sdk: *mut MySdk, data: *const u8, len: usize) -> c_int { if sdk.is_null() { return MY_SDK_ERR_NULL; } if data.is_null() && len > 0 { return MY_SDK_ERR_INVALID; } let sdk = unsafe { &mut *sdk }; let slice = if data.is_null() || len == 0 { &[] } else { unsafe { std::slice::from_raw_parts(data, len) } }; if sdk.buffer.len() + slice.len() > sdk.capacity { return MY_SDK_ERR_CAPACITY; } sdk.buffer.extend_from_slice(slice); MY_SDK_OK}
#[unsafe(no_mangle)]pub extern "C" fn my_sdk_get_result(sdk: *mut MySdk) -> MySdkResult { if sdk.is_null() { return MySdkResult { data: std::ptr::null(), len: 0, error_code: MY_SDK_ERR_NULL, }; } let sdk = unsafe { &*sdk }; MySdkResult { data: sdk.buffer.as_ptr(), len: sdk.buffer.len(), error_code: MY_SDK_OK, }}
#[unsafe(no_mangle)]pub extern "C" fn my_sdk_clear(sdk: *mut MySdk) -> c_int { if sdk.is_null() { return MY_SDK_ERR_NULL; } let sdk = unsafe { &mut *sdk }; sdk.buffer.clear(); MY_SDK_OK}
#[unsafe(no_mangle)]pub extern "C" fn my_sdk_version() -> *const i8 { let v = CString::new(env!("CARGO_PKG_VERSION")).unwrap(); v.into_raw()}
#[unsafe(no_mangle)]pub extern "C" fn my_sdk_free_string(s: *mut i8) { if !s.is_null() { unsafe { drop(CString::from_raw(s)); } }}C 测试
#include "my_sdk.h"#include <stdio.h>#include <assert.h>#include <string.h>
int main(void) { // 版本 const char *version = my_sdk_version(); printf("SDK version: %s\n", version); my_sdk_free_string((char *)version);
// 创建 MySdk *sdk = my_sdk_create(1024); assert(sdk != NULL);
// 追加数据 const char *hello = "Hello, "; int rc = my_sdk_append(sdk, (const uint8_t *)hello, strlen(hello)); assert(rc == 0);
const char *world = "World!"; rc = my_sdk_append(sdk, (const uint8_t *)world, strlen(world)); assert(rc == 0);
// 获取结果 MySdkResult result = my_sdk_get_result(sdk); assert(result.error_code == 0); printf("Result (%zu bytes): %.*s\n", result.len, (int)result.len, result.data);
// 超容量测试 uint8_t big[2048]; memset(big, 'x', sizeof(big)); rc = my_sdk_append(sdk, big, sizeof(big)); assert(rc == -2); // ERR_CAPACITY
// 清空 rc = my_sdk_clear(sdk); assert(rc == 0);
// 销毁 my_sdk_destroy(sdk); printf("All tests passed!\n"); return 0;}CI 构建
name: C SDK
on: [push, pull_request]
jobs: build: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - name: Build Rust library run: cargo build --release - name: Build C test run: | mkdir build && cd build cmake .. -DCMAKE_BUILD_TYPE=Release cmake --build . - name: Run C test run: ./build/c_test踩坑总结
1. panic 不能跨越 FFI 边界
#[unsafe(no_mangle)]pub extern "C" fn my_sdk_risky_operation(sdk: *mut MySdk) -> c_int { // 如果内部 panic,unwind 到 C 栈帧 = UB! // 必须用 catch_unwind 包裹 std::panic::catch_unwind(|| { let sdk = unsafe { &mut *sdk }; sdk.risky_op() }).unwrap_or_else(|_| { eprintln!("Rust panicked in FFI"); -999 })}2. 导出函数不能返回 Rust 分配的堆内存而不提供释放函数
// 错误!C 侧 free() 释放 Rust 分配的内存——分配器不匹配// #[unsafe(no_mangle)]// pub extern "C" fn bad() -> *mut u8 {// let v = vec![0u8; 100];// Box::into_raw(v.into_boxed_slice()) as *mut u8// }
// 正确:提供配对的释放函数#[unsafe(no_mangle)]pub extern "C" fn my_sdk_alloc(size: usize) -> *mut u8 { let mut v = Vec::with_capacity(size); let ptr = v.as_mut_ptr(); std::mem::forget(v); ptr}
#[unsafe(no_mangle)]pub extern "C" fn my_sdk_free(ptr: *mut u8, size: usize) { if !ptr.is_null() { unsafe { drop(Vec::from_raw_parts(ptr, 0, size)); } }}3. cdylib vs staticlib
cdylib:生成动态链接库(.so/.dll/.dylib),只导出#[unsafe(no_mangle)] pub extern "C"的符号,适合作为 C SDKstaticlib:生成静态库(.a/.lib),包含所有符号包括 Rust 标准库,适合静态链接lib(默认):Rust crate 格式,不能被 C 直接链接
[lib]crate-type = ["cdylib"] # 只生成动态库# crate-type = ["staticlib"] # 只生成静态库# crate-type = ["cdylib", "staticlib"] # 两种都生成4. C 结构体中包含 Rust 分配的指针
#[repr(C)]pub struct MySdkConfig { pub max_connections: u32, pub endpoint: *mut i8, // Rust CString}
// C 侧设置 endpoint 后,必须用 my_sdk_free_string 释放// 不能用 C 的 free()!支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!
TinyZ's Blog