C 里的 T * 太常用了。它可以表示很多不同语义:
void print_config(const struct Config *cfg);
void reset_config(struct Config *cfg);
int parse_packet(const uint8_t *data, size_t len, struct Packet *out);
void driver_write(void *addr, uint32_t value);
这些在 C 里都长得像指针,但语义完全不同:
- 只读借用一个对象
- 修改调用方对象
- 指针加长度表示一段 buffer
- 输出参数
- 可能为空的指针
- 硬件地址或 FFI 传来的地址
Rust 不会把这些都塞进一个“指针”概念里。第一层可以先用这张表:
const T * -> &T / Option<&T>
T * -> &mut T / Option<&mut T>
const T * + len -> &[T]
T * + capacity -> &mut [T]
out 参数 -> 返回值 / &mut T / &mut [T]
void * -> 泛型、trait、裸指针或 FFI 上下文
真实地址 / FFI 指针 -> *const T / *mut T
第一层先把分工弄清楚:普通库 API 优先用引用和 slice,裸指针靠近 FFI、硬件地址和底层封装。
只读输入用 &T
C 里只读结构体参数常写成 const T *:
void print_config(const struct Config *cfg)
{
printf("%u\n", cfg->port);
}
Rust 里通常用共享引用:
fn print_config(cfg: &Config) {
println!("{}", cfg.port);
}
&Config 表示函数可以读取 Config,但不能修改它。
调用时:
let cfg = Config { port: 1883 };
print_config(&cfg);
C 里还要约定 cfg 能不能是 NULL。Rust 的 &Config 不表示空引用。只要函数拿到 &Config,调用方就必须传入一个有效对象。
如果“可能没有配置”是合法状态,用 Option<&Config>:
fn print_config(cfg: Option<&Config>) {
if let Some(cfg) = cfg {
println!("{}", cfg.port);
}
}
不要把 nullable 指针习惯直接带进普通 Rust API。
可写对象用 &mut T
C 里修改调用方对象:
void reset_config(struct Config *cfg)
{
cfg->port = 0;
}
Rust 里用可变引用:
fn reset_config(cfg: &mut Config) {
cfg.port = 0;
}
调用方必须明确给出可变引用:
let mut cfg = Config { port: 1883 };
reset_config(&mut cfg);
这里有两层信息:
let mut cfg表示调用方这个绑定可以被改&mut cfg表示把可写访问权交给函数
C 的 T * 只靠函数名和注释暗示“会不会改”。Rust 把它放进参数类型里。
指针加长度用 slice
C 里 buffer 参数最常见:
uint32_t checksum(const uint8_t *data, size_t len);
Rust 里用 slice:
fn checksum(data: &[u8]) -> u32 {
data.iter().map(|b| *b as u32).sum()
}
&[u8] 同时表达两件事:
- 这是一段连续的
u8 - 函数只能读取,不能修改
调用方可以传数组、Vec 或 slice:
let data = [1_u8, 2, 3];
let sum = checksum(&data);
let data = vec![1_u8, 2, 3];
let sum = checksum(&data);
C 里指针和长度是两个独立参数,可能不匹配。Rust 的 slice 把指针和长度绑定在一起,减少了一类错误。
可写 buffer 用 &mut [T]
C 里写输出 buffer:
int fill_packet(uint8_t *out, size_t capacity, size_t *written);
Rust 里第一层对应是可变 slice:
fn fill_packet(out: &mut [u8]) -> Result<usize, EncodeError> {
if out.len() < 4 {
return Err(EncodeError::TooSmall);
}
out[0] = 1;
out[1] = 2;
out[2] = 3;
out[3] = 4;
Ok(4)
}
&mut [u8] 表示函数可以写这段 buffer,但不能越过 slice 的长度。
调用方:
let mut buf = [0_u8; 16];
let written = fill_packet(&mut buf)?;
这比 uint8_t *out, size_t capacity 更直接:buffer 的起始位置和长度在一个值里。
输出参数优先考虑返回值
C 用输出参数返回结果:
int parse_u16(const uint8_t *data, size_t len, uint16_t *out);
Rust 不一定要保留 out 参数。更常见的是返回值:
fn parse_u16(data: &[u8]) -> Result<u16, ParseError> {
if data.len() < 2 {
return Err(ParseError::TooShort);
}
Ok(u16::from_be_bytes([data[0], data[1]]))
}
如果函数要同时返回多个值,可以返回结构体或 tuple:
fn parse_field(data: &[u8]) -> Result<(u16, usize), ParseError> {
let value = parse_u16(data)?;
Ok((value, 2))
}
只有在必须复用调用方 buffer、避免分配、或匹配底层 API 时,才优先使用 &mut 输出参数。
裸指针留给边界
Rust 也有裸指针:
*const u8
*mut u8
它们更接近 C 指针,可以为空,可以来自任意地址,也不带借用检查保护。
例如 FFI 声明:
unsafe extern "C" {
fn sensor_read(out: *mut u8, capacity: usize) -> i32;
}
或者 C 调 Rust 的边界:
#[unsafe(no_mangle)]
pub extern "C" fn packet_checksum(ptr: *const u8, len: usize) -> u32 {
if ptr.is_null() {
return 0;
}
let data = unsafe { std::slice::from_raw_parts(ptr, len) };
checksum(data)
}
策略是:
边界用裸指针 -> 检查空指针和长度 -> 转成 slice / 引用 -> 内部用安全 Rust
不要把 *const T、*mut T 当成普通 Rust API 的默认参数类型。它们适合 FFI、系统调用、硬件地址、特殊底层封装。
void * 不要急着翻译成裸指针
C 里 void * 常用来表达“任意上下文”:
typedef void (*callback_t)(void *ctx, int event);
void register_callback(callback_t cb, void *ctx);
Rust 普通代码里,不一定要用 *mut c_void。根据场景可能有不同写法:
fn handle_event<F>(mut callback: F)
where
F: FnMut(Event),
{
callback(Event::Ready);
}
或者用 trait:
trait Handler {
fn handle(&mut self, event: Event);
}
只有跨 C ABI 时,才常见 *mut c_void:
use std::ffi::c_void;
type Callback = extern "C" fn(ctx: *mut c_void, event: i32);
第一层先记住:void * 在 C 里承担了很多抽象职责。Rust 里可能对应泛型、trait、闭包,也可能在 FFI 边界保留成裸指针。
最小迁移表
C const T * -> Rust &T
C nullable const T * -> Rust Option<&T> / raw pointer at FFI boundary
C T * for mutation -> Rust &mut T
C const T * + len -> Rust &[T]
C T * + capacity -> Rust &mut [T]
C out parameter -> Rust return value / Result<T, E> / &mut T
C void * context -> Rust generic / trait / closure / *mut c_void at FFI
C raw address -> Rust *const T / *mut T
C pointer API internally -> Rust boundary conversion to reference or slice
Rust 不是没有指针,而是把 C 里一个 T * 承担的多种含义拆开了。普通库 API 优先用引用和 slice;真正不受 Rust 检查保护的裸指针,尽量留在边界。