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 表达。