第 2 章:Rust 速成——C 工程师最需要的 20% 语法
Part1 了解嵌入式rust
第 2 章:Rust 速成——C 工程师最需要的 20% 语法#
本章要回答的问题:C 工程师需要掌握哪些 Rust 概念才能读懂后续章节?这些概念和 C 的什么机制对应?
本章定位:这不是完整的 Rust 教程,而是”C 工程师的最小必要知识集”。只讲后续章节会用到的概念,用 C 的类比帮助理解。
2.1 所有权与借用:为什么没有 malloc/free#
C 的痛点:谁负责 free?#
在 C 中,每一块动态分配的内存都需要有人负责释放。当代码规模增大、模块增多时,“谁分配、谁释放”这个问题变得极其复杂:
// 模块 A:分配
msg_t *create_message(uint8_t *data, uint16_t len) {
msg_t *msg = (msg_t *)malloc(sizeof(msg_t) + len);
msg->len = len;
memcpy(msg->data, data, len);
return msg; // 调用者负责 free?还是模块 B?
}
// 模块 B:使用
void process_message(msg_t *msg) {
// 我能不能 free(msg)?
// 如果调用者还要用呢?
// 如果另一个任务也在用呢?
}
// 模块 C:也使用
void log_message(msg_t *msg) {
// 如果模块 B 已经 free 了呢?
printf("msg: %s\n", msg->data); // 💥 use-after-free
}c这个问题的本质是:C 语言没有机制来表达”这块内存归谁管”。 所有的约定都靠注释、文档和程序员的记忆力。
Rust 的所有权规则:每个值有且仅有一个所有者#
Rust 用三条规则彻底解决了这个问题:
- 每个值有且仅有一个所有者(owner)
- 当所有者离开作用域时,值被自动释放(drop)
- 值可以被移动(move)或借用(borrow),但不能同时
fn create_message(data: &[u8]) -> Vec {
let msg = data.to_vec(); // msg 拥有这块内存
msg // 所有权移动给调用者
}
fn process_message(msg: Vec) {
// msg 的所有权转移到了这个函数
// 函数结束时,msg 自动释放
println!("len: {}", msg.len());
} // ← msg 在这里被自动 drop,相当于 free()
fn main() {
let data = [1u8, 2, 3, 4];
let msg = create_message(&data);
process_message(msg);
// println!("{:?}", msg); // ❌ 编译错误!msg 的所有权已经移走了
}rust注意最后一行被注释掉的代码——如果你取消注释,编译器会报错:
error[E0382]: borrow of moved value: `msg`
--> src/main.rs:14:27
|
13 | process_message(msg);
| --- value moved here
14 | println!("{:?}", msg);
| ^^^ value borrowed here after moveplaintext这就是 Rust 的核心哲学:编译器不允许你犯 C 中”理论上可能”的错误。
借用(& 和 &mut):类比 C 的指针,但有编译期约束#
在 C 中,指针是”自由的”——你可以创建任意多个指针指向同一块内存,可以通过任何指针修改数据,编译器不会阻止你。
Rust 的借用规则:
| 规则 | 说明 | C 的对应 |
|---|---|---|
&T(不可变借用) | 可以同时存在多个 | const T *(但 C 不强制) |
&mut T(可变借用) | 同一时刻只能存在一个 | T *(但 C 允许多个) |
不能同时存在 &T 和 &mut T | 读写互斥 | C 中无此限制 |
fn main() {
let mut sensor_data = [0u16; 4];
// ✅ 多个不可变借用可以同时存在
let r1 = &sensor_data;
let r2 = &sensor_data;
println!("{:?} {:?}", r1, r2);
// ✅ 一个可变借用(此时不能有不可变借用)
let w = &mut sensor_data;
w[0] = 1024;
// ❌ 编译错误:不能同时有可变借用和不可变借用
// let r3 = &sensor_data;
// let w2 = &mut sensor_data;
}rust用 C 的思维来理解:
// C:编译器不管你
uint16_t sensor_data[4] = {0};
uint16_t *r1 = sensor_data; // 读指针 1
uint16_t *r2 = sensor_data; // 读指针 2
uint16_t *w = sensor_data; // 写指针
w[0] = 1024; // 通过写指针修改
printf("%d\n", r1[0]); // 通过读指针读取——"安全"吗?
// 如果 w 在另一个线程/中断里呢?C 编译器不会警告你。cRust 的借用规则在编译期就阻止了”一边读一边写”的可能性。这在嵌入式中尤其重要——中断和主循环之间的数据竞争,在 Rust 中根本写不出来。
生命周期:编译器如何保证引用有效#
“生命周期”(lifetime)是 Rust 中让 C 工程师最困惑的概念之一。其实它的核心思想很简单:
引用不能比它指向的数据活得更久。
fn get_reference() -> &u32 {
let value = 42u32;
&value // ❌ 编译错误!value 在函数结束时被销毁
// 返回的引用将指向已释放的内存(悬垂指针)
}rust编译器报错:
error[E0106]: missing lifetime specifier
--> src/main.rs:1:21
|
1 | fn get_reference() -> &u32 {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value,
but there is no value for it to be borrowed fromplaintext在 C 中,这段代码会编译通过,然后在运行时产生未定义行为:
uint32_t *get_reference(void) {
uint32_t value = 42;
return &value; // ⚠️ 返回局部变量的地址——未定义行为!
}cRust 的生命周期系统,本质上就是编译器自动帮你做了”这个指针还有效吗”的检查。 在 C 中,这个检查靠程序员的大脑;在 Rust 中,靠编译器。
在嵌入式中的典型场景#
// 嵌入式中的常见模式:从外设读取数据,返回引用
struct Adc {
buffer: [u16; 8],
}
impl Adc {
// ✅ 返回的引用与 self 绑定——只要 Adc 实例存在,引用就有效
fn read(&self) -> &[u16] {
&self.buffer
}
}
// ❌ 错误示例:试图返回局部变量的引用
fn read_adc_wrong() -> &[u16; 8] {
let buffer = [0u16; 8];
// 读取 ADC...
&buffer // 编译错误:buffer 在函数结束时被销毁
}rust实际代码对比:C 的链表 vs Rust 的 Vec#
C 工程师经常需要手动管理动态数组:
// C:手动管理动态数组
typedef struct {
uint16_t *data;
uint16_t len;
uint16_t capacity;
} DynArray;
DynArray *array_create(uint16_t capacity) {
DynArray *arr = (DynArray *)malloc(sizeof(DynArray));
arr->data = (uint16_t *)malloc(capacity * sizeof(uint16_t));
arr->len = 0;
arr->capacity = capacity;
return arr;
}
void array_push(DynArray *arr, uint16_t value) {
if (arr->len >= arr->capacity) {
arr->capacity *= 2;
arr->data = (uint16_t *)realloc(arr->data,
arr->capacity * sizeof(uint16_t));
}
arr->data[arr->len++] = value;
}
void array_destroy(DynArray *arr) {
free(arr->data);
free(arr);
}
// 使用
void example() {
DynArray *arr = array_create(4);
array_push(arr, 100);
array_push(arr, 200);
// 如果这里忘了 array_destroy(arr)?内存泄漏。
// 如果 array_destroy 之后又用了 arr?use-after-free。
}cRust 的等价代码:
// Rust:Vec 自动管理
fn example() {
let mut arr: Vec = Vec::with_capacity(4);
arr.push(100);
arr.push(200);
// 不需要 destroy!arr 离开作用域时自动释放
// 不可能 use-after-free:编译器不允许
// 不可能内存泄漏:除非你故意用 std::mem::forget()
}rust注意:在
no_std嵌入式环境中,Vec需要堆分配器。如果不想用堆,可以使用heapless::Vec(固定容量,栈上分配),第 15 章会详细介绍。
所有权在嵌入式中的特殊考量#
在嵌入式中,很多资源是静态分配的(没有堆),所有权模型同样适用:
// 嵌入式中的静态缓冲区
static mut UART_BUF: [u8; 256] = [0u8; 256];
// ❌ 在 Rust 中,static mut 是 unsafe 的
// 因为编译器无法保证并发安全
unsafe {
UART_BUF[0] = 0x55; // 需要 unsafe 块
}
// ✅ 更好的做法:使用安全的抽象
use core::cell::RefCell;
use critical_section::Mutex;
static UART_BUF: Mutex> =
Mutex::new(RefCell::new([0u8; 256]));
// 在临界区中安全访问
critical_section::with(|cs| {
let mut buf = UART_BUF.borrow(cs).borrow_mut();
buf[0] = 0x55; // ✅ 安全,无需 unsafe
});rust这个例子展示了 Rust 嵌入式的一个重要模式:用类型系统 + 临界区替代 volatile + 手动关中断。 后续章节会反复出现这个模式。
2.2 Trait:Rust 的”接口”与 C 的函数指针表#
C 的做法:struct + 函数指针(模拟面向对象)#
C 工程师对”接口”并不陌生——我们只是不叫它”接口”,我们叫它”函数指针表”:
// C 的"接口":函数指针表
typedef struct {
int (*init)(void *self);
int (*read)(void *self, uint8_t *buf, uint16_t len);
int (*write)(void *self, const uint8_t *buf, uint16_t len);
void (*deinit)(void *self);
} SensorOps;
typedef struct {
const SensorOps *ops; // "虚函数表"指针
void *self; // "this" 指针
} Sensor;
// 具体实现:BME280
typedef struct {
uint8_t i2c_addr;
uint8_t config;
} BME280;
int bme280_init(void *self) {
BME280 *dev = (BME280 *)self;
// 初始化 BME280...
return 0;
}
int bme280_read(void *self, uint8_t *buf, uint16_t len) {
BME280 *dev = (BME280 *)self;
// 读取 BME280 数据...
return 0;
}
// "构造"一个 Sensor
static const SensorOps bme280_ops = {
.init = bme280_init,
.read = bme280_read,
.write = NULL, // BME280 不支持写
.deinit = NULL,
};
Sensor bme280_create(uint8_t addr) {
static BME280 dev;
dev.i2c_addr = addr;
return (Sensor){ .ops = &bme280_ops, .self = &dev };
}
// 使用
void read_temperature(Sensor *sensor) {
uint8_t buf[6];
sensor->ops->read(sensor->self, buf, 6); // "虚函数调用"
}c这个模式在 C 嵌入式中非常常见:Linux 内核的 file_operations、STM32 HAL 的 UART_HandleTypeDef、Zephyr 的 device_api——本质上都是函数指针表。
Rust 的 Trait:编译期多态 vs 运行时多态#
Rust 的 trait 就是”接口”的正式语言级支持:
// Rust 的"接口":trait
trait Sensor {
fn init(&mut self) -> Result<(), SensorError>;
fn read(&mut self, buf: &mut [u8]) -> Result;
}
// 具体实现:BME280
struct Bme280 {
i2c_addr: u8,
config: u8,
}
impl Sensor for Bme280 {
fn init(&mut self) -> Result<(), SensorError> {
// 初始化 BME280...
Ok(())
}
fn read(&mut self, buf: &mut [u8]) -> Result {
// 读取 BME280 数据...
Ok(6)
}
}
// 使用:泛型约束(编译期多态,零开销)
fn read_temperature(sensor: &mut S) {
let mut buf = [0u8; 6];
sensor.read(&mut buf).unwrap();
}rust两种多态的对比#
| 特性 | C 的函数指针表 | Rust 的 Trait(泛型) | Rust 的 Trait(动态) |
|---|---|---|---|
| 语法 | ops->read(self, ...) | sensor.read(...) | sensor.read(...) |
| 分派时机 | 运行时(间接跳转) | 编译期(内联) | 运行时(间接跳转) |
| 性能开销 | 函数指针间接调用 | 零开销 | 虚函数表间接调用 |
| 类型安全 | 无(void* 强转) | 完全类型安全 | 完全类型安全 |
| 代码体积 | 一份代码 | 每个类型一份(单态化) | 一份代码 |
| 嵌入式适用性 | 通用 | 首选(性能关键路径) | 需要堆分配(Box) |
在嵌入式中,编译期多态(泛型)是首选——它生成的代码与手写 C 完全相同,没有函数指针间接调用的开销。
与 C 的回调函数表对比#
让我们看一个更贴近嵌入式的例子——中断回调:
// C:中断回调注册
typedef void (*irq_callback_t)(void *ctx);
typedef struct {
irq_callback_t callback;
void *ctx;
} IrqHandler;
IrqHandler uart_handler;
void register_uart_irq(irq_callback_t cb, void *ctx) {
uart_handler.callback = cb;
uart_handler.ctx = ctx;
}
void UART_IRQHandler(void) {
if (uart_handler.callback) {
uart_handler.callback(uart_handler.ctx);
}
}c// Rust:trait 约束的回调
trait IrqCallback {
fn on_irq(&mut self);
}
struct UartHandler {
callback: C,
}
impl UartHandler {
fn new(callback: C) -> Self {
Self { callback }
}
fn handle_irq(&mut self) {
self.callback.on_irq(); // 编译期内联,零开销
}
}
// 具体实现
struct MyHandler {
count: u32,
}
impl IrqCallback for MyHandler {
fn on_irq(&mut self) {
self.count += 1;
}
}rust关键区别:
- C 的回调通过函数指针间接调用——运行时开销
- Rust 的泛型回调在编译期单态化——直接内联,与手写代码无异
Trait 的默认实现#
Trait 可以有默认实现,类似 C++ 的虚函数默认行为:
trait Logger {
fn log(&self, msg: &str);
// 默认实现:基于 log() 构建
fn log_info(&self, msg: &str) {
self.log(&format!("[INFO] {}", msg));
}
fn log_error(&self, msg: &str) {
self.log(&format!("[ERROR] {}", msg));
}
}
// 只需实现 log(),log_info/log_error 自动获得
struct RttLogger;
impl Logger for RttLogger {
fn log(&self, msg: &str) {
// 通过 RTT 输出
defmt::info!("{}", msg);
}
}rust2.3 枚举与模式匹配:替代 union + switch#
C 的 enum:只是整数常量#
// C 的 enum:本质上就是 #define 的语法糖
typedef enum {
STATE_IDLE = 0,
STATE_RUNNING = 1,
STATE_ERROR = 2,
} State;
// 它不能携带数据!
// 如果你需要"状态 + 附加数据",只能用 struct + union:
typedef struct {
State state;
union {
struct { uint32_t error_code; } error;
struct { uint16_t speed; } running;
} data;
} MachineState;cRust 的 enum:可以携带数据(代数数据类型)#
Rust 的 enum 是 C 的 enum + union + struct 的合体:
// Rust 的 enum:每个变体可以携带不同类型的数据
enum MachineState {
Idle, // 无数据
Running { speed: u16 }, // 携带 u16
Error { code: u32, retry: bool }, // 携带多个字段
}rust这一个 enum 就替代了 C 中的 enum + union + struct 三件套,而且类型安全——你不可能在 Idle 状态下访问 speed 字段。
Option<T>:替代 NULL 指针#
C 中用 NULL 表示”没有值”,这是无数 bug 的根源:
// C:NULL 指针
uint8_t *find_in_buffer(uint8_t *buf, uint16_t len, uint8_t target) {
for (uint16_t i = 0; i < len; i++) {
if (buf[i] == target) return &buf[i];
}
return NULL; // "没找到"
}
// 调用者必须记得检查 NULL
uint8_t *result = find_in_buffer(buf, len, 0xAA);
if (result != NULL) {
*result = 0xBB; // 如果忘了检查?💥 段错误
}cRust 用 Option<T> 替代 NULL:
// Rust:Option
fn find_in_buffer(buf: &[u8], target: u8) -> Option {
for (i, &byte) in buf.iter().enumerate() {
if byte == target {
return Some(i); // "找到了,值是 i"
}
}
None // "没找到"
}
// 调用者必须处理"没找到"的情况
let result = find_in_buffer(&buf, 0xAA);
match result {
Some(index) => buf[index] = 0xBB, // 找到了
None => {}, // 没找到——你必须显式处理
}rust关键区别:在 C 中,忘记检查 NULL 是”运行时崩溃”;在 Rust 中,忘记处理 None 是”编译错误”。
match:比 switch 更强大的模式匹配#
C 的 switch 只能匹配整数常量,而且容易忘记 break:
// C:switch 的陷阱
switch (state) {
case STATE_IDLE:
start_motor();
// 忘了 break!fall-through 到下一个 case
case STATE_RUNNING:
set_speed(1000);
break;
case STATE_ERROR:
stop_motor();
break;
// 如果 state 是一个未定义的值?没有 default 就是未定义行为
}cRust 的 match 解决了所有这些问题:
// Rust:match 是穷尽的(exhaustive)
match state {
MachineState::Idle => {
start_motor();
}
MachineState::Running { speed } => {
set_speed(speed); // 直接解构,取出 speed
}
MachineState::Error { code, retry } => {
stop_motor();
if retry {
reset();
}
}
// 如果你漏掉了任何一个变体,编译器会报错!
// 不存在 fall-through
}rust编译器强制你处理所有可能的情况。如果你新增了一个状态变体,所有未更新的 match 都会编译失败——这比 C 的 switch 安全得多。
实际代码对比:C 的错误码 vs Rust 的 Result#
// C:错误码
int uart_send(uint8_t *data, uint16_t len) {
if (data == NULL) return -1; // 参数错误
if (len == 0) return -2; // 长度错误
if (!uart_ready()) return -3; // 外设未就绪
// 发送...
if (timeout()) return -4; // 超时
return 0; // 成功
}
// 调用者
int ret = uart_send(buf, len);
if (ret != 0) {
// ret 是 -1?-2?-3?-4?
// 需要查文档才知道每个错误码的含义
// 而且很容易忘记检查返回值
}c// Rust:Result
#[derive(Debug)]
enum UartError {
NullPointer,
ZeroLength,
NotReady,
Timeout,
}
fn uart_send(data: &[u8]) -> Result<(), UartError> {
if data.is_empty() {
return Err(UartError::ZeroLength);
}
if !uart_ready() {
return Err(UartError::NotReady);
}
// 发送...
if timeout() {
return Err(UartError::Timeout);
}
Ok(()) // 成功
}
// 调用者
match uart_send(&buf) {
Ok(()) => {},
Err(UartError::Timeout) => retry(),
Err(e) => log_error(e), // 其他错误
}rustResult 的优势:
- 错误是类型:编译器知道每个函数可能返回哪些错误
- 必须处理:不处理
Result会有编译警告(#[must_use]) - 自文档化:
UartError::Timeout比-4可读性强一万倍
2.4 泛型与单态化:为什么”模板”不会膨胀#
C 的”泛型”:void* 或宏#
C 没有真正的泛型,只有两种 workaround:
// 方法 1:void*(丢失类型信息)
void queue_push(Queue *q, void *item) {
memcpy(q->buf + q->tail * q->item_size, item, q->item_size);
q->tail = (q->tail + 1) % q->capacity;
}
// 方法 2:宏(代码生成,但无类型检查)
#define QUEUE_PUSH(q, item) do { \
memcpy((q)->buf + (q)->tail * sizeof(*(item)), \
(item), sizeof(*(item))); \
(q)->tail = ((q)->tail + 1) % (q)->capacity; \
} while(0)c两种方法都有问题:void* 丢失类型安全,宏没有类型检查且调试困难。
C++ 的模板:编译期代码生成#
// C++ 模板
template
class Queue {
T buf[N];
size_t head = 0, tail = 0;
public:
void push(const T& item) {
buf[tail] = item;
tail = (tail + 1) % N;
}
};
Queue adc_queue;
Queue uart_queue;
// 编译器为每个 组合生成一份代码cppC++ 模板的问题:
- 编译错误信息极其晦涩
- 可能导致代码膨胀(每个实例化都是一份完整代码)
- 在嵌入式中,
std::vector等 STL 容器隐式使用堆分配
Rust 的泛型:单态化(monomorphization)#
// Rust 泛型
struct Queue {
buf: [T; N],
head: usize,
tail: usize,
}
impl Queue {
fn push(&mut self, item: T) {
self.buf[self.tail] = item;
self.tail = (self.tail + 1) % N;
}
}
let mut adc_queue: Queue = Queue::new();
let mut uart_queue: Queue = Queue::new();rust单态化意味着:编译器在编译期为每个具体类型生成一份专用代码。Queue<u16, 16> 和 Queue<u8, 64> 会变成两个完全独立的函数,就像你手写了两份代码一样。
为什么嵌入式中泛型不会导致代码膨胀#
这是 C 工程师最常见的担忧:“每个类型都生成一份代码,Flash 不就爆了吗?”
答案是:LLVM 优化器会消除重复。
// 这两个函数,单态化后生成的机器码完全相同:
fn process_u8(val: u8) -> u8 { val.wrapping_add(1) }
fn process_u16(val: u16) -> u16 { val.wrapping_add(1) }
// 编译器会生成:
// process_u8: add r0, r0, #1; bx lr
// process_u16: add r0, r0, #1; bx lr
// 完全相同的指令!rust对于更复杂的泛型函数,LLVM 的 mergefunc pass 会识别出功能相同的函数并合并。实际测量表明,在典型的嵌入式项目中,泛型带来的额外代码体积通常小于 5%。
经验法则:如果你担心代码体积,用
cargo size测量,而不是凭直觉猜测。第 14 章会详细讨论体积优化。
2.5 错误处理:Result 替代错误码#
C 的做法:返回 -1,检查 errno#
// C 的典型错误处理模式
int sensor_read(uint8_t *buf, uint16_t len) {
int ret;
ret = i2c_write(SENSOR_ADDR, CMD_READ, 1);
if (ret < 0) {
return ret; // 传播错误码
}
ret = i2c_read(SENSOR_ADDR, buf, len);
if (ret < 0) {
return ret; // 传播错误码
}
return 0;
}
// 调用链中的每一层都要检查返回值
int application_read(void) {
uint8_t buf[6];
int ret = sensor_read(buf, 6);
if (ret < 0) {
// 处理错误...
return ret;
}
// 继续...
}c这种模式的问题:
- 容易遗漏:忘记检查返回值,编译器不会警告(除非加了
__attribute__((warn_unused_result))) - 错误信息丢失:
-1可能是 I2C 超时,也可能是 NACK,调用者无法区分 - 传播冗长:每一层都要写
if (ret < 0) return ret;
Rust 的 Result<T, E>:错误是值,不是异常#
// Rust 的等价代码
fn sensor_read(buf: &mut [u8]) -> Result<(), SensorError> {
i2c.write(SENSOR_ADDR, &[CMD_READ])?; // ? 自动传播错误
i2c.read(SENSOR_ADDR, buf)?; // ? 自动传播错误
Ok(())
}rust? 操作符:优雅的错误传播#
? 是 Rust 中最实用的语法糖之一。它的语义是:
// 这一行:
i2c.write(SENSOR_ADDR, &[CMD_READ])?;
// 等价于:
match i2c.write(SENSOR_ADDR, &[CMD_READ]) {
Ok(val) => val,
Err(e) => return Err(e.into()), // 提前返回错误
}rust对比 C 的 if (ret < 0) return ret;,? 操作符:
- 更简洁(一个字符 vs 一整行)
- 不可能遗漏(不处理
Result会有编译警告) - 支持错误类型转换(
.into())
panic!:不可恢复错误(类比 C 的 assert + abort)#
// panic! 用于"这不应该发生"的情况
fn divide(a: u32, b: u32) -> u32 {
if b == 0 {
panic!("division by zero!"); // 不可恢复,程序终止
}
a / b
}rust在嵌入式中,panic 通常意味着:
- 进入
panic_handler(你在第 3 章会配置它) - 输出错误信息(通过 RTT 或串口)
- 死循环等待看门狗复位
#[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! {
// 输出错误信息到 RTT
defmt::error!("PANIC: {}", info);
loop {
// 等待看门狗复位
cortex_m::asm::wfi();
}
}rust最佳实践:在嵌入式中,
panic应该只用于”逻辑上不可能发生”的情况(如数组越界、不可达分支)。对于”可能发生但可恢复”的错误(如通信超时),使用Result。
2.6 闭包与迭代器:嵌入式中的零成本用法#
C 的函数指针 vs Rust 的闭包#
// C:函数指针(不能捕获上下文)
typedef int (*compare_fn)(const void *a, const void *b);
int compare_u16(const void *a, const void *b) {
return *(uint16_t *)a - *(uint16_t *)b;
}
qsort(arr, len, sizeof(uint16_t), compare_u16);cC 的函数指针有一个根本限制:不能捕获局部变量。如果你需要”带上下文的回调”,就必须传 void *ctx:
// C:带上下文的回调(丑陋但常见)
typedef void (*callback_fn)(void *ctx, uint16_t value);
void for_each_sample(uint16_t *buf, uint16_t len,
callback_fn cb, void *ctx) {
for (uint16_t i = 0; i < len; i++) {
cb(ctx, buf[i]);
}
}
// 使用
struct { uint32_t sum; uint16_t count; } stats;
void accumulate(void *ctx, uint16_t value) {
struct { uint32_t sum; uint16_t count; } *s = ctx;
s->sum += value;
s->count++;
}
for_each_sample(buf, len, accumulate, &stats);cRust 的闭包可以直接捕获局部变量,无需 void *ctx:
// Rust:闭包自动捕获上下文
let mut sum = 0u32;
let mut count = 0u16;
buf.iter().for_each(|&value| {
sum += value as u32; // 直接捕获 sum
count += 1; // 直接捕获 count
});rust迭代器链:编译器会优化为与手写循环相同的代码#
// Rust 迭代器链
let result: u32 = buf.iter()
.filter(|&&x| x > 100) // 过滤大于 100 的值
.map(|&x| x as u32 * 2) // 每个值乘以 2
.sum(); // 求和rust等价的 C 代码:
// C:手写循环
uint32_t result = 0;
for (uint16_t i = 0; i < len; i++) {
if (buf[i] > 100) {
result += (uint32_t)buf[i] * 2;
}
}c关键事实:在 --release 模式下,Rust 编译器会将迭代器链优化为与手写 C 循环完全相同的机器码。没有函数调用开销,没有中间数组分配,没有虚函数分派。
你可以用 Compiler Explorer ↗ 验证这一点——将 Rust 代码和 C 代码分别编译为 ARM 汇编,对比输出。
在嵌入式中的典型用法#
// 1. ADC 多通道采样:过滤 + 转换 + 求平均
let adc_values: [u16; 8] = read_all_channels();
let average: u16 = adc_values.iter()
.filter(|&&v| v > 10) // 过滤噪声(低于 10 的无效值)
.map(|&v| v as u32) // 转为 u32 防止溢出
.sum::() as u16 / 8;
// 2. UART 协议解析:查找帧头
let frame_start = rx_buf.iter()
.position(|&b| b == 0xAA); // 查找 0xAA 的位置
// 3. GPIO 批量配置
pins.iter_mut().for_each(|pin| {
pin.set_high();
});rust2.7 模块系统与 use:替代 #include#
C 的 #include:文本替换#
// C 的 #include 是纯粹的文本替换
#include "uart.h" // 把 uart.h 的内容原封不动地粘贴到这里
#include "sensor.h" // 再粘贴 sensor.h
#include // 再粘贴标准库头文件
// 问题:
// 1. 头文件保护(#ifndef)是手动管理的
// 2. 包含顺序可能影响编译结果
// 3. 没有命名空间——所有全局符号都在同一个平面
// 4. 循环包含需要前向声明cRust 的 mod + use:命名空间与可见性#
// Rust 的模块系统
// 文件结构:
// src/
// main.rs
// uart.rs
// sensor/
// mod.rs
// bme280.rs
// main.rs
mod uart; // 声明模块(相当于告诉编译器"有这个文件")
mod sensor; // 声明模块
use uart::Uart; // 导入具体类型(相当于 C 的 #include + 命名空间)
use sensor::bme280::Bme280;
fn main() {
let mut uart = Uart::new();
let mut bme = Bme280::new(&mut uart);
}rust// uart.rs
pub struct Uart {
// ...
}
impl Uart {
pub fn new() -> Self {
// ...
}
pub fn write(&mut self, data: &[u8]) -> Result<(), UartError> {
// ...
}
// 没有 pub:模块外部不可见(相当于 C 的 static)
fn internal_helper(&mut self) {
// ...
}
}rustpub 与封装:比 static 更精细的访问控制#
| C 的做法 | Rust 的做法 | 说明 |
|---|---|---|
static 函数/变量 | 不加 pub | 仅当前模块可见 |
| 头文件中声明 | pub | 对外可见 |
| 无对应机制 | pub(crate) | 仅当前 crate 内可见 |
| 无对应机制 | pub(super) | 仅父模块可见 |
| 无对应机制 | pub(in path) | 仅指定路径可见 |
// Rust 的精细访问控制
pub struct Peripheral {
pub name: &'static str, // 外部可读可写
pub(crate) config: u32, // 仅 crate 内可访问
reg_addr: u32, // 仅本模块可访问(默认私有)
}rust这比 C 的”要么全局可见,要么 static”精细得多。在嵌入式中,这意味着你可以:
- 将寄存器地址封装在 HAL 内部,外部无法直接访问
- 将配置字段限制在 crate 内部,防止外部误修改
- 只暴露安全的 API,隐藏不安全的实现细节
嵌入式项目的典型模块结构#
my-project/
├── Cargo.toml
├── memory.x
├── build.rs
├── .cargo/
│ └── config.toml
└── src/
├── main.rs // 入口:任务定义、主逻辑
├── hal/
│ ├── mod.rs // HAL 模块声明
│ ├── gpio.rs // GPIO 驱动
│ ├── uart.rs // UART 驱动
│ └── spi.rs // SPI 驱动
├── protocol/
│ ├── mod.rs // 协议模块声明
│ ├── modbus.rs // Modbus 实现
│ └── crc.rs // CRC 计算
└── app/
├── mod.rs // 应用模块声明
├── sensor.rs // 传感器任务
└── comm.rs // 通信任务plaintext2.8 本章小结#
速查表:C → Rust 概念对照#
| C 概念 | Rust 对应 | 关键区别 |
|---|---|---|
malloc / free | 所有权系统(自动 drop) | 编译期保证,无需手动管理 |
T *(指针) | &T / &mut T(引用) | 有编译期约束(借用规则) |
const T * | &T(不可变引用) | 编译器强制不可变 |
struct + 函数指针 | trait + impl | 编译期多态,零开销 |
enum(整数常量) | enum(代数数据类型) | 可携带数据,类型安全 |
union | enum 的变体 | 无需手动管理活跃成员 |
switch | match | 穷尽性检查,无 fall-through |
NULL | Option<T> | 编译器强制处理”无值”情况 |
返回 -1 + errno | Result<T, E> | 错误是类型,必须处理 |
void* + 回调 | 闭包 / 泛型 | 类型安全,可捕获上下文 |
#include | mod + use | 命名空间,精细访问控制 |
#ifdef | Feature flag(cfg) | Cargo 管理,依赖树感知 |
static 函数 | 不加 pub | 模块级私有 |
assert() | panic!() | 输出信息后终止 |
for 循环 | 迭代器 + 闭包 | 零成本,编译器优化为相同代码 |
后续章节中会遇到的 Rust 特性预告#
| 特性 | 出现章节 | 用途 |
|---|---|---|
async / await | 第 7-10 章 | Embassy 异步任务 |
Future trait | 第 9 章 | 理解 Embassy 运行时 |
Pin | 第 9 章 | 固定 Future 的内存位置 |
const 泛型 | 第 8 章 | 固定大小的缓冲区 |
过程宏(#[...]) | 第 8 章 | #[embassy_executor::task] |
unsafe | 第 4 章 | 直接寄存器操作 |
生命周期标注 'a | 第 9 章 | 任务间共享数据 |
Send / Sync | 第 10 章 | 任务间通信的安全性 |
推荐深入学习资源#
如果你想在后续章节之外深入学习 Rust,以下资源按优先级排序:
-
《The Rust Programming Language》(官方书籍,免费在线)
- 第 4 章(所有权)、第 6 章(枚举)、第 10 章(泛型/Trait)是重点
- 不需要全部读完,按需查阅
-
Rust by Example(官方示例集,免费在线)
- 适合”看代码学语法”的工程师
-
《Rust for Rustaceans》(Jon Gjengset)
- 进阶读物,适合掌握基础后深入理解
-
Rust 官方文档(
rustup doc)- 标准库 API 参考,随用随查
建议:不要试图”先学完 Rust 再学嵌入式”。从第 3 章开始,你会在实际项目中自然习得这些概念。遇到不懂的语法,回来查这一章就够了。
下一章:第 3 章——开始上路——第一个裸机工程。我们将创建第一个 Rust 嵌入式项目,从 cargo new 到 LED 闪烁,完整走一遍编译、烧录、调试的流程。