跳转到主内容库 API 设计第一层 | 物联网民工

库 API 设计第一层

C 库 API 里,一个指针能表达很多事:

int parse_packet(const uint8_t *data, size_t len, struct Packet *out);
void normalize(uint8_t *data, size_t len);
void consume_buffer(uint8_t *data, size_t len);
uint8_t *make_packet(size_t *out_len);

这些签名看起来都和指针有关,但 API 语义完全不同:

  • 只读输入
  • 原地修改
  • 输出结果
  • 接管调用方资源
  • 创建新资源并交给调用方

Rust 库 API 的第一层设计,就是把这些意图写进类型。

只读输入:借用

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] 表示函数临时读取一段字节,不接管内存,也不能修改。

文本参数同理。不要写:

fn print_name(name: String) {
    println!("{name}");
}

如果函数只是读取,写:

fn print_name(name: &str) {
    println!("{name}");
}

这样调用方可以传字符串字面量,也可以传 String 的借用:

print_name("sensor");

let name = String::from("device");
print_name(&name);

第一条规则:

函数只是读:优先接收 &T / &[T] / &str

原地修改:可变借用

C 里修改调用方 buffer:

void xor_in_place(uint8_t *data, size_t len, uint8_t key);

Rust 写:

fn xor_in_place(data: &mut [u8], key: u8) {
    for byte in data {
        *byte ^= key;
    }
}

调用方必须明确给可变借用:

let mut data = vec![1_u8, 2, 3];
xor_in_place(&mut data, 0xAA);

如果修改的是一个结构体:

fn reset_config(config: &mut Config) {
    config.retry = 0;
}

第二条规则:

函数要原地修改调用方数据:接收 &mut T / &mut [T]

创建新数据:返回拥有类型

C 里常见“返回新分配内存 + out_len”:

uint8_t *encode_packet(const uint8_t *payload, size_t len, size_t *out_len);

Rust 普通库 API 不需要这样设计。直接返回拥有类型:

fn encode_packet(payload: &[u8]) -> Vec<u8> {
    let mut out = Vec::with_capacity(payload.len() + 2);
    out.push(0xAA);
    out.extend_from_slice(payload);
    out.push(0x55);
    out
}

如果可能失败:

fn encode_packet(payload: &[u8]) -> Result<Vec<u8>, EncodeError> {
    if payload.len() > u8::MAX as usize {
        return Err(EncodeError::TooLarge);
    }

    let mut out = Vec::with_capacity(payload.len() + 2);
    out.push(0xAA);
    out.extend_from_slice(payload);
    out.push(0x55);
    Ok(out)
}

文本同理:

fn make_topic(device: &str) -> String {
    format!("devices/{device}/status")
}

第三条规则:

函数创建新数据:返回 Vec<T> / String / Box<T> / 自定义拥有类型

输出参数:能返回就返回

C 里输出参数很常见:

int parse_header(const uint8_t *data, size_t len, struct Header *out);

Rust 可以直接返回:

fn parse_header(data: &[u8]) -> Result<Header, ParseError> {
    if data.len() < 4 {
        return Err(ParseError::TooShort);
    }

    Ok(Header {
        version: data[0],
        flags: data[1],
        len: u16::from_be_bytes([data[2], data[3]]),
    })
}

如果要返回多个值,用 tuple 或结构体:

fn parse_field(data: &[u8]) -> Result<(Field, usize), ParseError> {
    let field = parse_one_field(data)?;
    Ok((field, field.encoded_len()))
}

只有在确实要复用调用方提供的 buffer、避免分配或贴近底层接口时,再用 &mut 输出:

fn encode_into(payload: &[u8], out: &mut [u8]) -> Result<usize, EncodeError> {
    if out.len() < payload.len() + 2 {
        return Err(EncodeError::TooSmall);
    }

    out[0] = 0xAA;
    out[1..1 + payload.len()].copy_from_slice(payload);
    out[1 + payload.len()] = 0x55;
    Ok(payload.len() + 2)
}

第四条规则:

C 输出参数:Rust 优先返回值;必须复用调用方内存时才用 &mut

接管资源:按值接收

C 里有些 API 会接管调用方传来的资源:

void queue_take_packet(uint8_t *packet, size_t len);

调用后调用方不能再释放或使用这块内存。Rust 里这种 API 应该按值接收拥有类型:

fn queue_take_packet(packet: Vec<u8>) {
    println!("queued {}", packet.len());
}

调用方:

let packet = vec![1_u8, 2, 3];
queue_take_packet(packet);
// packet 不能再用

如果函数只是看一眼,不要接收 Vec<u8>:

fn inspect_packet(packet: &[u8]) {
    println!("len = {}", packet.len());
}

第五条规则:

函数要接管资源:按值接收 Vec<T> / String / File / 自定义拥有类型
函数只是使用资源:接收借用

结构体字段也要按语义选

API 不只体现在函数参数上,也体现在结构体字段上。

如果结构体要拥有配置文本:

struct Config {
    name: String,
}

如果只是临时查看外部数据,通常不该把引用存进长寿命结构体,除非你明确要引入生命周期参数:

struct View<'a> {
    data: &'a [u8],
}

第一层库 API 里,如果不想把生命周期暴露给调用方,常常让结构体拥有数据:

struct Packet {
    data: Vec<u8>,
}

这不是说引用字段不能用,而是它会把“这个结构体不能比被借用的数据活得更久”变成 API 的一部分。初学阶段不要随手把所有字段都写成引用。

一张决策表

设计 Rust 库 API 时,可以先按这个顺序问:

1. 函数只是读取吗?
   -> &T / &[T] / &str

2. 函数要原地修改吗?
   -> &mut T / &mut [T]

3. 函数要创建新数据给调用方吗?
   -> 返回 Vec<T> / String / Box<T> / 自定义类型

4. 函数要接管调用方资源吗?
   -> 按值接收 Vec<T> / String / File / 自定义拥有类型

5. 函数要复用调用方 buffer 避免分配吗?
   -> 输入 &[T],输出 &mut [T],返回写入长度或 Result<usize, E>

这比先纠结“Rust 到底要不要所有权”更实用。API 的问题不是语法,而是资源关系。

这一层先记住什么

C 到 Rust 的 API 迁移第一层:

const T *                 -> &T
const T * + len           -> &[T]
T * + len 原地修改         -> &mut [T]
返回 malloc 指针           -> 返回 Vec<T> / String / Box<T>
out 参数                   -> 优先返回值 / Result<T, E>
接管调用方资源             -> 按值接收拥有类型
只是临时使用资源           -> 接收借用

好的 Rust API 不会把所有东西都写成 Vec、String 或裸指针。它会让函数签名直接说明:我只是读、我要改、我要创建、还是我要接管。

继续阅读