文章目录

每日一句正能量
好的关系向来是热烈有度,留白有余,彼此依靠又各自独立。
热情但不灼人,关心但不控制。给彼此空间和沉默的权利,不必填满每一秒。我可以信赖你,但摔倒了也能自己站起来。这种关系像两棵树,根系在深处相连,枝叶却各自伸向天空。
摘要
摘要:本文系统探讨在现有C嵌入式项目中渐进式引入Rust模块的工程实践。从FFI(Foreign Function Interface)双向调用机制出发,深入分析bindgen自动生成C→Rust绑定、cbindgen生成Rust→C头文件的工作流程,结合内存安全封装、构建系统集成(CMake+Cargo)以及渐进式迁移策略,为嵌入式开发者提供从C到Rust的安全过渡路径。
一、Rust与C互操作的架构概览
在嵌入式领域,完全重写现有C代码库往往不现实。更务实的路径是渐进式迁移——在保持现有C代码运行的同时,逐步引入Rust模块替换高风险或高价值组件。

图1:Rust与C互操作架构概览
1.1 典型混合架构
C代码层(现有代码):
- 硬件驱动(HAL/BSP)
- 通信协议栈(TCP/IP、BLE、CAN)
- 第三方库(mbedtls、lwip、FreeRTOS)
- 遗留业务逻辑
Rust代码层(新模块):
- 安全关键组件(加密/认证)
- 新算法实现(ML推理、信号处理)
- 协议解析器(安全内存保证)
- 状态机/工作流引擎
FFI边界层:
- C→Rust:
extern "C" fn+#[no_mangle] - Rust→C:
unsafe { libc::... } - 工具链:bindgen(C头→Rust绑定)、cbindgen(Rust→C头)
1.2 核心设计原则
“fronteira pequena, camada segura”(小边界,安全层):保持FFI边界尽可能小,将
unsafe代码隔离在少数模块中,对外暴露安全的Rust API。
三层架构设计:
- 原始绑定层:
bindgen生成的FFI声明,直接映射C API - 内部封装层:验证参数、转换错误、文档化不变量
- 公共API层:安全的Rust接口,团队其他成员直接使用
二、FFI双向调用与数据类型映射
2.1 调用方向

图2:FFI双向调用与数据流
C调用Rust(C → Rust):
// Rust侧:导出函数供C调用
#[no_mangle] // 禁止名称修饰,保持C可见的符号名
pub extern "C" fn rust_process_data(
data: *const u8,
len: usize,
out_buf: *mut u8,
out_len: usize
) -> i32 {
// 安全检查:空指针
if data.is_null() || out_buf.is_null() {
return -1; // 错误码:空指针
}
// 安全转换:裸指针 → Rust切片
let input = unsafe { core::slice::from_raw_parts(data, len) };
let output = unsafe { core::slice::from_raw_parts_mut(out_buf, out_len) };
// 业务逻辑(安全Rust代码)
match process(input, output) {
Ok(n) => n as i32,
Err(_) => -2, // 错误码:处理失败
}
}
// C侧:声明并调用Rust函数
extern int rust_process_data(
const uint8_t* data,
size_t len,
uint8_t* out_buf,
size_t out_len
);
// 使用
uint8_t result[256];
int ret = rust_process_data(input, input_len, result, sizeof(result));
if (ret < 0) {
// 错误处理
}
Rust调用C(Rust → C):
// Rust侧:声明C函数
extern "C" {
fn c_hal_init(baudrate: u32) -> i32;
fn c_hal_send(data: *const u8, len: usize) -> i32;
fn c_hal_receive(buf: *mut u8, max_len: usize) -> i32;
}
// 安全封装
pub fn hal_init(baudrate: u32) -> Result<(), HalError> {
let ret = unsafe { c_hal_init(baudrate) };
if ret == 0 {
Ok(())
} else {
Err(HalError::from_raw(ret))
}
}
2.2 数据类型映射
| C类型 | Rust类型 | 说明 |
|---|---|---|
uint8_t | u8 | 无符号8位整数 |
int16_t | i16 | 有符号16位整数 |
uint32_t | u32 | 无符号32位整数 |
float | f32 | IEEE 754单精度 |
double | f64 | IEEE 754双精度 |
bool | bool | Rust bool = u8 (C99 _Bool) |
char* | *const c_char | C字符串指针 |
void* | *const c_void | 通用指针 |
struct Foo | #[repr(C)] struct Foo | 必须指定C内存布局 |
enum Status | #[repr(C)] enum Status | C兼容枚举 |
关键规则:所有跨FFI边界的struct必须使用#[repr(C)]确保内存布局兼容。Rust的默认struct布局是未指定的,编译器可能重排字段或插入不同padding。
三、bindgen自动生成绑定
3.1 工作流程

图3:bindgen自动生成Rust绑定工作流程
bindgen通过libclang解析C头文件,自动生成Rust FFI绑定代码。
配置步骤:
// build.rs
use std::env;
use std::path::PathBuf;
fn main() {
// 告诉Cargo链接C库
println!("cargo:rustc-link-lib=hal");
println!("cargo:rustc-link-search=native=/path/to/lib");
println!("cargo:rerun-if-changed=wrapper.h");
// 生成绑定
let bindings = bindgen::Builder::default()
.header("wrapper.h") // 包含所有需要绑定的C头
.clang_arg("--target=thumbv7em-none-eabihf") // 交叉编译目标
.allowlist_function("hal_.*") // 仅绑定hal_前缀的函数
.allowlist_type("hal_.*") // 仅绑定hal_前缀的类型
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
.generate()
.expect("Unable to generate bindings");
// 写入输出目录
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("Couldn't write bindings");
}
// wrapper.h
#include "hal_gpio.h"
#include "hal_uart.h"
#include "hal_spi.h"
// src/lib.rs
mod ffi {
// 包含生成的绑定
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
}
// 安全封装层
pub struct HalDevice {
raw: *mut ffi::hal_device_t,
}
impl HalDevice {
pub fn new() -> Result<Self, HalError> {
let raw = unsafe { ffi::hal_create_device() };
if raw.is_null() {
return Err(HalError::OutOfMemory);
}
Ok(Self { raw })
}
pub fn send(&mut self, data: &[u8]) -> Result<(), HalError> {
let ret = unsafe {
ffi::hal_send(self.raw, data.as_ptr(), data.len())
};
if ret == 0 { Ok(()) } else { Err(HalError::Io) }
}
}
impl Drop for HalDevice {
fn drop(&mut self) {
unsafe { ffi::hal_destroy_device(self.raw); }
}
}
3.2 bindgen高级配置
let bindings = bindgen::Builder::default()
.header("wrapper.h")
// 类型映射定制
.blocklist_type("uint32_t") // 使用Rust原生u32
.size_t_is_usize(true) // size_t → usize
// 枚举处理
.rustified_enum("hal_status_t") // 生成Rust枚举而非常量
// 常量处理
.constified_enum_module("hal_flags") // 模块级常量
// 函数处理
.ignore_functions() // 手动实现安全封装
.generate()
.unwrap();
四、内存安全封装与生命周期管理
4.1 C内存管理风险 vs Rust安全方案

图4:FFI内存安全封装与生命周期管理
C代码的典型风险:
// 内存泄漏风险
hal_device_t* dev = hal_create_device();
hal_send_data(dev, buf, len);
// 忘记调用 hal_destroy_device(dev) → 内存泄漏
// 双重释放风险
hal_destroy_device(dev);
hal_destroy_device(dev); // 未定义行为!
// Use-After-Free风险
hal_destroy_device(dev);
hal_send_data(dev, buf, len); // 崩溃!
Rust封装方案:
// 不透明指针封装
pub struct HalDevice {
raw: NonNull<ffi::hal_device_t>,
_marker: PhantomData<ffi::hal_device_t>,
}
impl HalDevice {
pub fn new() -> Result<Self, HalError> {
let raw = unsafe { ffi::hal_create_device() };
let raw = NonNull::new(raw).ok_or(HalError::OutOfMemory)?;
Ok(Self { raw, _marker: PhantomData })
}
pub fn send(&mut self, data: &[u8]) -> Result<(), HalError> {
let ret = unsafe {
ffi::hal_send(self.raw.as_ptr(), data.as_ptr(), data.len())
};
if ret == 0 { Ok(()) } else { Err(HalError::Io) }
}
}
// Drop自动释放:确保无泄漏
impl Drop for HalDevice {
fn drop(&mut self) {
unsafe { ffi::hal_destroy_device(self.raw.as_ptr()); }
}
}
// 不可Copy、不可Clone:确保唯一所有权
// 编译期防止:双重释放、Use-After-Free
4.2 字符串安全传递
use core::ffi::CStr;
use core::ffi::c_char;
// C → Rust:接收C字符串
pub fn get_version() -> Result<&'static str, Utf8Error> {
let ptr = unsafe { ffi::hal_get_version() };
if ptr.is_null() {
return Err(Utf8Error::NullPointer);
}
let cstr = unsafe { CStr::from_ptr(ptr) };
cstr.to_str()
}
// Rust → C:传递字符串到C
pub fn set_name(name: &str) -> Result<(), HalError> {
// 确保以null结尾
let c_name = CString::new(name).map_err(|_| HalError::InvalidName)?;
let ret = unsafe { ffi::hal_set_name(c_name.as_ptr()) };
if ret == 0 { Ok(()) } else { Err(HalError::Io) }
}
4.3 回调函数传递
// C侧回调类型
// typedef void (*hal_callback_t)(int event, void* user_data);
// Rust侧:将闭包转换为C回调
pub fn register_callback<F>(&mut self, callback: F)
where
F: FnMut(i32) + Send + 'static
{
// 将闭包装箱为trait对象
let boxed = Box::new(callback);
let user_data = Box::into_raw(boxed) as *mut c_void;
unsafe {
ffi::hal_register_callback(
self.raw.as_ptr(),
Some(trampoline::<F>),
user_data
);
}
}
// 蹦床函数:C回调 → Rust闭包
extern "C" fn trampoline<F>(event: i32, user_data: *mut c_void)
where
F: FnMut(i32)
{
let closure = unsafe { &mut *(user_data as *mut F) };
closure(event);
}
五、构建系统集成:CMake + Cargo
5.1 混合构建流程

图5:CMake + Cargo混合构建系统集成流程
方案一:CMake主导,Cargo生成静态库
# CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(firmware)
# 编译C代码
add_subdirectory(c_hal)
add_subdirectory(c_drivers)
# 导入Rust crate
find_package(Corrosion REQUIRED)
corrosion_import_crate(MANIFEST_PATH rust_module/Cargo.toml)
# 顶层可执行文件
add_executable(firmware
src/main.c
src/app_logic.c
)
target_link_libraries(firmware
PRIVATE
c_hal
c_drivers
rust_module # Rust静态库
)
# 统一链接脚本
set_target_properties(firmware PROPERTIES
LINK_FLAGS "-T ${CMAKE_SOURCE_DIR}/memory.x"
)
# rust_module/Cargo.toml
[package]
name = "rust_module"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["staticlib"] # 生成静态库 .a
[dependencies]
# 嵌入式依赖
cortex-m = "0.7"
panic-halt = "0.2"
[build-dependencies]
bindgen = "0.69"
cc = "1.0"
方案二:Cargo主导,编译C代码
// build.rs
fn main() {
// 编译C代码
cc::Build::new()
.file("c_src/hal_gpio.c")
.file("c_src/hal_uart.c")
.include("c_inc")
.flag("-mthumb")
.flag("-mcpu=cortex-m4")
.compile("chal");
// 生成绑定
let bindings = bindgen::Builder::default()
.header("c_inc/hal.h")
.generate()
.unwrap();
bindings
.write_to_file(PathBuf::from(env::var("OUT_DIR").unwrap()).join("bindings.rs"))
.unwrap();
println!("cargo:rustc-link-lib=chal");
}
5.2 链接器配置
// .cargo/config.toml
[target.thumbv7em-none-eabihf]
runner = "probe-rs run --chip STM32F407VG"
rustflags = [
"-C", "link-arg=-Tmemory.x",
"-C", "link-arg=-Map=firmware.map",
]
[build]
target = "thumbv7em-none-eabihf"
/* memory.x */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
六、渐进式迁移策略
6.1 三阶段迁移模型

图6:渐进式引入Rust的迁移策略
阶段一:独立模块(低风险、高价值)
选择无状态、纯计算的模块优先迁移:
- 加密算法(AES、SHA、RSA)
- CRC校验、哈希计算
- 数学运算库
特点:C调用Rust,单向数据流,无状态依赖。
阶段二:替换驱动(中风险、中价值)
逐步替换设备驱动:
- 传感器驱动(I2C/SPI设备)
- 协议解析器(Modbus、CAN)
- 文件系统层
特点:双向调用,需处理硬件时序、中断、DMA。
阶段三:核心重构(高风险、低收益)
最后处理核心业务逻辑:
- 状态机引擎
- 任务调度器
- 遗留业务规则
特点:Rust为主,C为遗留接口,或保持C不变。
6.2 关键决策点
| 决策维度 | 评估标准 | 建议 |
|---|---|---|
| 模块边界 | 接口是否清晰、数据流是否单向 | 清晰的接口是迁移成功的前提 |
| 测试覆盖 | 是否有完善的单元测试 | 迁移前后对比验证行为一致性 |
| 团队能力 | Rust熟练度、代码审查机制 | 建立培训与审查流程 |
| 构建系统 | 是否支持混合编译 | 提前验证CMake/Cargo集成 |
| 风险收益 | 安全收益 vs 迁移成本 | 优先迁移安全关键组件 |
七、cbindgen:Rust导出到C
当需要将Rust模块提供给C代码使用时,cbindgen自动生成C头文件:
// Rust侧:导出函数
#[repr(C)]
pub struct RustConfig {
pub baudrate: u32,
pub timeout_ms: u32,
pub enable_crc: bool,
}
#[no_mangle]
pub extern "C" fn rust_module_init(config: *const RustConfig) -> i32 {
if config.is_null() {
return -1;
}
let config = unsafe { &*config };
// 初始化逻辑
0 // 成功
}
#[no_mangle]
pub extern "C" fn rust_module_process(
input: *const u8,
input_len: usize,
output: *mut u8,
output_cap: usize,
output_len: *mut usize
) -> i32 {
// 参数验证
if input.is_null() || output.is_null() || output_len.is_null() {
return -1;
}
let input_slice = unsafe { core::slice::from_raw_parts(input, input_len) };
let output_slice = unsafe { core::slice::from_raw_parts_mut(output, output_cap) };
match process(input_slice, output_slice) {
Ok(n) => {
unsafe { *output_len = n; }
0
}
Err(_) => -2,
}
}
# cbindgen.toml
language = "C"
include_guard = "RUST_MODULE_H"
autogen_warning = "/* Auto-generated by cbindgen. Do not modify. */"
[export]
include = ["rust_module_init", "rust_module_process", "RustConfig"]
# 生成头文件
cbindgen --config cbindgen.toml --crate rust_module --output rust_module.h
// 生成的 rust_module.h
#ifndef RUST_MODULE_H
#define RUST_MODULE_H
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
uint32_t baudrate;
uint32_t timeout_ms;
bool enable_crc;
} RustConfig;
int rust_module_init(const RustConfig* config);
int rust_module_process(
const uint8_t* input,
size_t input_len,
uint8_t* output,
size_t output_cap,
size_t* output_len
);
#ifdef __cplusplus
}
#endif
#endif
八、调试与验证
8.1 符号验证
# 查看ELF符号表
arm-none-eabi-nm firmware.elf | grep rust_
arm-none-eabi-nm firmware.elf | grep hal_
# 验证Rust函数是否正确导出
arm-none-eabi-objdump -t firmware.elf | grep "rust_module"
# 检查未定义符号
arm-none-eabi-ld firmware.o -o firmware.elf 2>&1 | grep "undefined reference"
8.2 大小分析
# 分析固件大小
cargo bloat --release -n 20
# 按模块分析
arm-none-eabi-size firmware.elf
# 详细段分析
arm-none-eabi-objdump -h firmware.elf
8.3 运行时调试
// 使用defmt进行零成本日志
use defmt::info;
#[no_mangle]
pub extern "C" fn rust_process(data: *const u8, len: usize) {
info!("Processing {} bytes", len);
// ...
}
九、总结与最佳实践
| 维度 | 关键实践 | 收益 |
|---|---|---|
| 绑定生成 | bindgen + 白名单过滤 | 自动化、减少手写错误 |
| 内存安全 | RAII封装 + Drop trait | 消除泄漏、双重释放、UAF |
| 类型安全 | #[repr(C)] + 显式布局 | 跨语言内存兼容 |
| 错误处理 | 错误码返回 + Result映射 | 清晰的错误传播路径 |
| 构建集成 | Corrosion / cc crate | 无缝混合编译 |
| 迁移策略 | 低风险模块优先 | 渐进式、可控风险 |
核心原则:
- 小边界:FFI接口尽可能少,每个接口职责单一
- 安全层:所有
unsafe封装在安全API之后,不直接暴露 - 零panic:FFI边界绝不panic,所有错误通过错误码返回
- 文档化:明确内存所有权、生命周期、线程安全假设
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/u014727709/article/details/162675035




