进哥聊编程头像
关注
Rust与C的互操作:在现有嵌入式项目中引入Rust模块——FFI、绑定生成封面图

Rust与C的互操作:在现有嵌入式项目中引入Rust模块——FFI、绑定生成


在这里插入图片描述

每日一句正能量

好的关系向来是热烈有度,留白有余,彼此依靠又各自独立。
热情但不灼人,关心但不控制。给彼此空间和沉默的权利,不必填满每一秒。我可以信赖你,但摔倒了也能自己站起来。这种关系像两棵树,根系在深处相连,枝叶却各自伸向天空。

摘要

摘要:本文系统探讨在现有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。

三层架构设计:

  1. 原始绑定层bindgen生成的FFI声明,直接映射C API
  2. 内部封装层:验证参数、转换错误、文档化不变量
  3. 公共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_tu8无符号8位整数
int16_ti16有符号16位整数
uint32_tu32无符号32位整数
floatf32IEEE 754单精度
doublef64IEEE 754双精度
boolboolRust bool = u8 (C99 _Bool)
char**const c_charC字符串指针
void**const c_void通用指针
struct Foo#[repr(C)] struct Foo必须指定C内存布局
enum Status#[repr(C)] enum StatusC兼容枚举

关键规则:所有跨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无缝混合编译
迁移策略低风险模块优先渐进式、可控风险

核心原则

  1. 小边界:FFI接口尽可能少,每个接口职责单一
  2. 安全层:所有unsafe封装在安全API之后,不直接暴露
  3. 零panic:FFI边界绝不panic,所有错误通过错误码返回
  4. 文档化:明确内存所有权、生命周期、线程安全假设

转载自:
欢迎 👍点赞✍评论⭐收藏,欢迎指正

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/u014727709/article/details/162675035

文章来源crawl

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--