跳转到主内容模块和文件组织:从 .h/.c 到 mod | 物联网民工

模块和文件组织:从 .h/.c 到 mod

C 项目常用 .h 和 .c 分工:

packet.h   -> 对外声明结构体、函数、宏
packet.c   -> 实现函数,隐藏内部 helper
main.c     -> include packet.h 后调用公开函数

例如:

/* packet.h */
int packet_parse(const uint8_t *data, size_t len, struct Packet *out);

/* packet.c */
static int parse_header(const uint8_t *data, size_t len);

int packet_parse(const uint8_t *data, size_t len, struct Packet *out)
{
    /* ... */
}

Rust 也要区分文件、内部实现和公开 API,但模型不是 #include。第一层可以这样看:

C .h/.c        -> Rust module + pub
C include      -> Rust use / module path
C static helper -> Rust 默认私有函数
C public header -> Rust pub API

Cargo.toml 描述 package

C 项目通常由 Makefile、CMake 或其他构建脚本描述源文件、头文件路径和库输出。

Rust 项目的入口通常是 Cargo.toml:

[package]
name = "iot-packet"
version = "0.1.0"
edition = "2024"

这里描述的是 package。一个 package 可以包含库 crate、二进制 crate,或者两者都有。

最常见的文件入口是:

src/main.rs  -> binary crate 入口
src/lib.rs   -> library crate 入口

写命令行工具时,通常从 src/main.rs 开始。写可复用库时,通常从 src/lib.rs 暴露 API。

crate 是编译单元,不是 C 文件

C 里每个 .c 文件通常单独编译成目标文件,再链接到一起。

Rust 的 crate 更接近一个编译单元。一个库 crate 的根通常是 src/lib.rs:

pub fn parse_packet(data: &[u8]) -> Result<Packet, ParseError> {
    // ...
}

一个 binary crate 的根通常是 src/main.rs:

fn main() {
    // ...
}

不要把一个 Rust 源文件直接类比成一个 C translation unit。Rust 的模块树从 crate root 展开,文件只是承载模块的一种方式。

mod 声明模块,不是 include

C 的 #include 是把头文件内容带进当前翻译单元:

#include "packet.h"

Rust 的 mod 不是文本包含。它声明一个模块,并让编译器去找对应代码:

mod packet;

如果在 src/main.rs 里写 mod packet;,编译器通常会找:

src/packet.rs

或者:

src/packet/mod.rs

现代项目更常用 src/packet.rs 或 src/packet/xxx.rs 搭配子模块。

例如:

src/main.rs
src/packet.rs

src/main.rs:

mod packet;

fn main() {
    let data = [1, 2, 3];
    let _ = packet::checksum(&data);
}

src/packet.rs:

pub fn checksum(data: &[u8]) -> u32 {
    data.iter().map(|b| *b as u32).sum()
}

mod packet; 的意思不是“把 packet.rs 粘贴进来”,而是“当前 crate 有一个名为 packet 的模块”。

默认私有,pub 才公开

C 里内部 helper 常用文件作用域 static:

static int parse_header(const uint8_t *data, size_t len);

Rust 模块里的项默认私有:

fn parse_header(data: &[u8]) -> Result<Header, ParseError> {
    // ...
}

需要对模块外公开时写 pub:

pub fn parse_packet(data: &[u8]) -> Result<Packet, ParseError> {
    let header = parse_header(data)?;
    Ok(Packet { header })
}

这个模型对 C 开发者很重要:

不写 pub -> 模块内部 helper
写 pub   -> 模块外可访问 API

Rust 不需要把“公开声明”和“实现定义”拆成头文件和源文件。公开边界直接写在项上。

use 是引入名字,不是加载文件

C 的 #include 既影响声明可见性,也参与文本展开。

Rust 的 use 只是在当前作用域引入路径名字:

use crate::packet::checksum;

fn main() {
    let data = [1, 2, 3];
    let sum = checksum(&data);
    println!("{sum}");
}

不用 use 也能通过完整路径调用:

let sum = crate::packet::checksum(&data);

所以第一层区别是:

mod -> 声明模块存在
pub -> 决定模块外能不能访问
use -> 把路径名字引入当前作用域

use 不会让一个文件被编译进来;模块树才决定哪些模块属于 crate。

lib.rs 像库入口,不是头文件

写库时,src/lib.rs 常作为对外 API 的组织入口:

mod packet;
mod checksum;

pub use packet::{parse_packet, Packet, ParseError};

内部文件:

src/packet.rs
src/checksum.rs

lib.rs 可以选择把内部模块里的类型和函数重新导出。这样调用方看到的是:

use iot_packet::parse_packet;

而不是:

use iot_packet::packet::parse_packet;

这有点像 C 头文件统一整理库 API,但不要把它理解成文本声明文件。lib.rs 本身就是 Rust 代码,是 crate root,也是 API 组织点。

src/main.rs 调库,src/lib.rs 放逻辑

命令行工具项目经常会先把所有代码写进 main.rs。代码变多后,更稳的结构是:

src/main.rs
src/lib.rs
src/packet.rs

src/main.rs 处理参数、打印和退出码:

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let path = std::env::args().nth(1).expect("missing path");
    let data = std::fs::read(path)?;
    let packet = iot_tool::parse_packet(&data)?;
    println!("{packet:?}");
    Ok(())
}

src/lib.rs 暴露可测试的库 API:

mod packet;

pub use packet::{parse_packet, Packet, ParseError};

这对应 C 项目里把 CLI 入口和可复用库分开的习惯。main 不应该承载所有业务逻辑。

文件路径和模块路径不是随便对应

Rust 的模块路径由 mod 声明和模块树决定,不是所有 .rs 文件自动进入项目。

例如只有这个文件:

src/helper.rs

如果没有在模块树里声明:

mod helper;

它不会自动成为 crate 的一部分。

这和 C 构建系统列出 .c 文件有点像:文件存在,不等于已经进入编译图。

Rust 的区别是:模块树写在 Rust 代码里,Cargo 从 crate root 出发构建这棵树。

最小迁移表

C project/build file       -> Rust Cargo.toml + src/
C .c translation unit      -> Rust crate 不是逐文件类比
C .h public declaration    -> Rust pub item / pub use
C #include "x.h"           -> Rust mod / use 分工
C static helper function   -> Rust private fn by default
C exported function        -> Rust pub fn
C header umbrella API      -> Rust lib.rs re-export
C main.c                   -> Rust src/main.rs
C reusable library code    -> Rust src/lib.rs + modules

Rust 的模块系统不是为了复刻 .h/.c,而是把“文件组织、命名空间、公开 API”放到同一棵模块树里。C 里靠头文件和链接约定维护的边界,在 Rust 里主要由 mod、pub 和 use 表达。

继续阅读