Rust 嵌入式开发与 FFI 互操作:no_std、embedded-hal 与 C 接口

Rust 嵌入式与 FFI 完全指南:no_std 裸机开发、embedded-hal 硬件抽象、ESP32/STM32 实战、C 与 Rust 双向互操作(extern C/绑定生成/PyO3)、FFI 安全边界与内存布局。

目录

  1. 嵌入式 Rust 的定位
  2. no_std 环境搭建
  3. embedded-hal 硬件抽象
  4. ESP32 与 STM32 实战
  5. RTOS 与异步嵌入式
  6. FFI 基础:extern C
  7. C 调用 Rust
  8. Rust 调用 C:bindgen
  9. FFI 安全边界
  10. 速查表与最佳实践

1. 嵌入式 Rust 的定位

嵌入式是 Rust 的「原生战场」:no_std 环境无运行时、无 GC、无堆,所有权模型天然适合资源受限设备。

优势说明
内存安全编译期消除空指针/越界/数据竞争
零成本抽象抽象不引入运行时开销
无运行时可跑在 4KB RAM 的 MCU 上
生态成熟embedded-hal 统一硬件抽象

主流方案:

方案目标特点
ESP32(Rust)乐鑫芯片官方支持,wifi/bt 生态好
STM32(Rust)ST 芯片HAL 齐全,社区活跃
RP2040(Raspberry Pi)树莓派 Pico便宜,适合入门
nRF52Nordic BLE低功耗蓝牙首选

2. no_std 环境搭建

#![no_std]           // 不用标准库
#![no_main]          // 无 main(嵌入式入口由硬件启动代码决定)

Cargo.toml:

[package]
name = "my-firmware"
version = "0.1.0"

[profile.release]
debug = 1
opt-level = "s"
overflow-checks = true

[dependencies]
cortex-m-rt = "0.7"
cortex-m-semihosting = "0.5"
panic-halt = "0.5"

入口与中断:

#![no_std]
#![no_main]

use cortex_m_rt::entry;
use panic_halt as _;   // panic 时停机

#[entry]
fn main() -> ! {
    // 无限主循环
    loop {
        // 业务逻辑
    }
}

no_std 与 std 的差异:无 String/Vec(可用 alloc 手动接堆)、无文件系统、无网络(需外设)。


3. embedded-hal 硬件抽象

embedded-hal 定义统一的 GPIO、SPI、I2C、UART、PWM 等 trait,让驱动可跨芯片复用。

use embedded_hal::digital::OutputPin;
use embedded_hal::spi::SpiBus;
use embedded_hal::i2c::I2c;

// 任何实现了 OutputPin 的芯片都能跑这段代码
fn blink<LED>(mut led: LED) where LED: OutputPin {
    led.set_high().ok();
    // 延时
    led.set_low().ok();
}

// 通用 I2C 读取温度传感器
fn read_temp<I>(i2c: &mut I) -> Result<u16, ()>
where I: I2c {
    let mut buf = [0u8; 2];
    i2c.write_read(0x48, &[0x00], &mut buf).map_err(|_| ())?;
    Ok(u16::from_be_bytes([buf[0], buf[1]]))
}

HAL 生态分层:

层例子
芯片访问stm32f4xx_hal / esp32_hal
通用驱动embedded-hal trait
具体外设驱动ssd1306(OLED)、bme280(传感器)

4. ESP32 与 STM32 实战

4.1 ESP32:点亮 LED + 连接 WiFi

#![no_std]
#![no_main]

use esp_backtrace as _;
use esp_hal::{clock::ClockControl, peripherals::Peripherals, prelude::*, timer::TimerGroup};
use esp_wifi::{initialize, wifi::WifiAp};

#[entry]
fn main() -> ! {
    let peripherals = Peripherals::take();
    let system = peripherals.SYSTEM;
    let clocks = ClockControl::boot_defaults().freeze();

    // GPIO2 配置为输出
    let mut led = esp_hal::gpio::IO::new(peripherals.GPIO, peripherals.IO_MUX)
        .pins
        .gpio2
        .into_push_pull_output();

    loop {
        led.toggle();
        esp_hal::time::delay(500u32.millis());
    }
}

4.2 STM32:串口输出

use stm32f4xx_hal::{pac, prelude::*, serial::Serial};

fn main() {
    let dp = pac::Peripherals::take().unwrap();
    let rcc = dp.RCC.constrain();
    let clocks = rcc.cfgr.freeze();
    let mut gpioa = dp.GPIOA.split();
    let tx = gpioa.pa2.into_alternate();
    let rx = gpioa.pa3.into_alternate();
    let serial = Serial::new(dp.USART2, (tx, rx), 115200.bps(), clocks);
    let (mut tx, _rx) = serial.split();
    loop {
        tx.write_str("hello from STM32\r\n").ok();
        delay();
    }
}

5. RTOS 与异步嵌入式

5.1 RTOS 集成

方案说明
embassyRust 原生异步嵌入式,无 RTOS
RTIC实时中断驱动框架
FreeRTOS通过 FFI 使用 C RTOS

5.2 Embassy 异步

#![no_std]
#![no_main]

use embassy_executor::Spawner;
use embassy_time::{Duration, Timer};

#[embassy_executor::main]
async fn main(_spawner: Spawner) {
    loop {
        // 异步定时任务,不阻塞
        Timer::after(Duration::from_millis(1000)).await;
        // 周期执行逻辑
    }
}

// 多个并发任务
#[embassy_executor::task]
async fn blink_task() {
    loop {
        // LED 闪烁
        Timer::after(Duration::from_millis(500)).await;
    }
}

异步 vs RTOS:Embassy 用协作式调度,任务数量无上限、零内核开销,正在成为 Rust 嵌入式异步的标准。


6. FFI 基础:extern C

FFI(Foreign Function Interface)让 Rust 与 C 互调。基础是 extern "C" 块 + #[no_mangle]。

// 调用 C 库函数
extern "C" {
    fn strlen(s: *const u8) -> usize;
    fn malloc(size: usize) -> *mut u8;
}

fn c_len(s: &str) -> usize {
    unsafe { strlen(s.as_ptr()) }
}

C ABI 内存布局:

#[repr(C)]           // 保证字段布局与 C 一致
struct CPoint {
    x: f64,
    y: f64,
}

#[repr(C, u8)]       // 带 tag 的枚举(类似 C union + tag)
enum CEvent {
    Mouse(u32, u32),
    Key(u8),
}

警告:普通 Rust enum 布局不定,FFI 必须用 #[repr(C)] 或 #[repr(C, u8)]。


7. C 调用 Rust

把 Rust 函数导出给 C 用:#[no_mangle] + extern "C" + 手动管理不安全接口。

// lib.rs
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
    a + b
}

// 导出内存操作(供 C 分配/释放)
#[no_mangle]
pub extern "C" fn my_alloc(size: usize) -> *mut u8 {
    let mut v = Vec::with_capacity(size);
    let ptr = v.as_mut_ptr();
    std::mem::forget(v);   // 交给 C 管理
    ptr
}

#[no_mangle]
pub extern "C" fn my_free(ptr: *mut u8) {
    unsafe {
        let v = Vec::from_raw_parts(ptr, 0, 0);  // 简化示例
        drop(v);
    }
}

C 侧调用:

// main.c
#include <stdio.h>
extern int add(int, int);

int main() {
    printf("%d\n", add(2, 3));
    return 0;
}
# 编译链接
rustc --crate-type=cdylib src/lib.rs -o librustlib.so
gcc main.c -L. -lrustlib -o app

8. Rust 调用 C:bindgen

用 bindgen 从 C 头文件自动生成 Rust 绑定。

# 安装
cargo install bindgen-cli

# 生成绑定
bindgen include/my_lib.h -o src/bindings.rs \
  --allowlist-function "my_.*" \
  --allowlist-type "MyStruct"
// my_lib.h
typedef struct {
    int width;
    int height;
} MyRect;

int my_area(const MyRect *rect);
void my_fill(MyRect *rect, int w, int h);
// 生成的 bindings.rs
#[repr(C)]
pub struct MyRect {
    pub width: ::std::os::raw::c_int,
    pub height: ::std::os::raw::c_int,
}
extern "C" {
    pub fn my_area(rect: *const MyRect) -> ::std::os::raw::c_int;
    pub fn my_fill(rect: *mut MyRect, w: ::std::os::raw::c_int, h: ::std::os::raw::c_int);
}

安全封装(调用方只看到 safe API):

mod cbindings;   // 生成的绑定

pub struct Rect { width: i32, height: i32 }

impl Rect {
    pub fn new(w: i32, h: i32) -> Self { Self { width: w, height: h } }
    pub fn area(&self) -> i32 {
        let r = cbindings::MyRect { width: self.width, height: self.height };
        unsafe { cbindings::my_area(&r) }
    }
}

跨语言场景:调用 C 库(OpenSSL、zlib、SQLite C API)、Python(PyO3)、Node(napi-rs)。


9. FFI 安全边界

FFI 是 unsafe 的最高危区域,必须遵守严格边界。

风险规避
空指针传入前检查 is_null()
悬垂指针生命周期由 C 管理时 mem::forget 防提前 drop
数据竞争C 多线程访问 Rust 数据要加锁或原子
内存泄漏配对 alloc/free 必须对称
UB(未定义行为)不把 &mut 传给可能改数据的 C 函数
// 安全封装示例:确保指针生命周期
pub struct CStringGuard(*mut c_char);

impl CStringGuard {
    pub fn from_raw(ptr: *mut c_char) -> Option<Self> {
        (!ptr.is_null()).then(|| Self(ptr))
    }
    pub fn as_str(&self) -> &str {
        unsafe { std::ffi::CStr::from_ptr(self.0) }
            .to_str()
            .unwrap_or("")
    }
}

impl Drop for CStringGuard {
    fn drop(&mut self) {
        unsafe { free(self.0) }   // 配套 C 的 free
    }
}

FFI 设计原则:

  1. unsafe 代码最小化:只在薄薄一层 FFI 边界用 unsafe。
  2. 边界内做安全封装,暴露 safe API。
  3. 文档写明:谁拥有内存、谁负责释放、是否线程安全。
  4. 用 cargo geiger 统计 unsafe 覆盖,保持受控。

10. 速查表与最佳实践

任务工具
no_std 项目#![no_std] + cortex-m-rt
硬件抽象embedded-hal trait
ESP32esp-hal + esp-idf-hal
STM32stm32f4xx-hal
异步嵌入式embassy
C 绑定生成bindgen
Rust 导出给 C#[no_mangle] + extern "C"
Python 互操作pyo3
Node 互操作napi-rs
unsafe 审计cargo geiger

最佳实践清单:

  • 嵌入式:优先 embassy 异步,少写手写状态机。
  • FFI:所有跨边界结构用 #[repr(C)]。
  • 指针:谁分配谁释放,用 RAII 封装。
  • 测试:FFI 层做集成测试,安全封装做单元测试。
  • 文档:# Safety 段写清每个 unsafe 调用的前置条件。

一句话记忆:嵌入式 = no_std + embedded-hal + 异步运行时;FFI = extern “C” + repr(C) + 最小 unsafe 边界 + RAII 封装,把危险关进笼子里。

延伸阅读

  • https://plumephp.com/rust-systems-programming/ — 系统级编程与性能
  • https://plumephp.com/rust-desktop-wasm/ — WASM 与跨平台
  • https://plumephp.com/rust-cli-development/ — 命令行工具(也常需 FFI)
  • https://plumephp.com/posts/os/ — 操作系统与裸机编程
  • https://plumephp.com/posts/cpp/ — C++ 与 Rust 互操作对比
  • [[ai]] — Rust AI 推理中的 FFI 场景

从 MCU 上 4KB RAM 的 LED 闪烁,到通过 PyO3 把 Rust 库接入 Python 生态——嵌入式与 FFI 是把 Rust 的优势延伸到「更底层」和「更互联」两个方向的必经之路。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「rust」更多文章

  1. Rust 宏系统与元编程:声明宏、过程宏与 derive 实战
  2. Rust 学习路线与资源导航:从 The Book 到生产级实战
  3. Rust 工具链精讲:Cargo 高级特性、交叉编译与质量门禁