给 C 开发者的 Rust

43 文章

Drop 深入

4 分钟

前面讲过 Drop 的第一层作用:值离开作用域时自动释放资源。

这已经能替代很多 C 里的 goto cleanup。但真正在库代码里包装文件句柄、连接、锁、临时目录或 FFI handle 时,只知道“离开作用域会释放”还不够。

阅读更多

可变借用

4 分钟

C 里,拿到 T * 通常意味着“可以改”。问题是,C 不阻止你同时留下很多入口:

uint8_t *a = buf;
uint8_t *b = buf;

a[0] = 1;
printf("%u\n", b[0]);

这段代码本身没错,但在真实项目里,多个指针同时读写同一块数据,会让状态变化很难追踪。谁刚刚改过?谁还拿着旧视图?某个函数会不会在你读之前顺手改了?

阅读更多

移动、复制和 clone

4 分钟

C 程序员看赋值和传参,第一反应通常是“复制一份值”:

uint32_t a = 10;
uint32_t b = a;

结构体也可以按值复制:

struct Point p1 = {1, 2};
struct Point p2 = p1;

但一旦结构体里有指针,事情就变复杂:

struct Buffer {
    uint8_t *data;
    size_t len;
};

struct Buffer b2 = b1; // 只是复制指针和长度

这不是深拷贝。b1.data 和 b2.data 指向同一块内存。谁释放?能不能同时改?释放一个以后另一个还能不能用?C 里这些问题靠约定处理。

阅读更多

测试

3 分钟

C 项目里测试常从一个额外的 main 开始:

#include <assert.h>

int main(void)
{
    assert(checksum((uint8_t[]){1, 2, 3}, 3) == 6);
    return 0;
}

或者写几个小程序、shell 脚本、Makefile 目标:

test:
	./test_checksum
	./test_parser

Rust 把这个入口直接放进 Cargo:

cargo test

第一层可以先记住:Rust 的测试不是另外一套工程系统,而是 crate 的一部分。

阅读更多

库 API 设计第一层

4 分钟

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 的第一层设计,就是把这些意图写进类型。

阅读更多

动态分配和容器

4 分钟

C 里动态分配通常从这几个函数开始:

void *p = malloc(size);
p = realloc(p, new_size);
free(p);

Rust 普通代码很少直接调用分配器。更常见的是用拥有类型表达“这块内存归谁管”:

Box<T>     -> 堆上的一个 T
Vec<T>     -> 堆上的连续可增长数组
String     -> 堆上的可增长 UTF-8 文本

这些类型内部当然会分配和释放内存,但调用方通常不写 free。值离开作用域时,释放由类型的 Drop 完成。

阅读更多

文件、目录和进程

3 分钟

C 程序员写工具或基础库,很快会碰到这些接口:

FILE *fp = fopen(path, "rb");
fread(buf, 1, sizeof(buf), fp);
fclose(fp);

DIR *dir = opendir(path);
system("gzip file.log");
getenv("HOME");

Rust 标准库里对应的入口主要是:

std::fs       -> 文件和目录的便捷操作
std::io       -> Read / Write / BufRead 等 I/O trait
std::env      -> 参数、环境变量、当前目录
std::process  -> 启动子进程、取退出状态和输出

这一篇不展开所有权和 Drop 的细节,只看 C 常用库函数在 Rust 里的第一层替代方式。

阅读更多

位标志和位域

3 分钟

C 里位标志很常见:

#define FLAG_READ   (1u << 0)
#define FLAG_WRITE  (1u << 1)
#define FLAG_ERROR  (1u << 2)

uint32_t flags = FLAG_READ | FLAG_WRITE;

if (flags & FLAG_ERROR) {
    handle_error();
}

Rust 当然也能做位运算。但有两个习惯要先改:

C:    if (flags & FLAG_ERROR)
Rust: if flags & FLAG_ERROR != 0

Rust 的 if 条件必须是 bool。整数不会自动当作真假值。

阅读更多