跳转到主内容指针第一层:引用和裸指针怎么分工 | 物联网民工

指针第一层:引用和裸指针怎么分工

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 检查保护的裸指针,尽量留在边界。

继续阅读