C projects commonly split .h and .c files like this:
packet.h -> public structs, functions, macros
packet.c -> function implementations and private helpers
main.c -> includes packet.h and calls public functions
For example:
/* 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 also separates files, internal implementation, and public APIs, but the model is not #include. The first mapping is:
C .h/.c -> Rust module + pub
C include -> Rust use / module path
C static helper -> Rust private function by default
C public header -> Rust pub API
Cargo.toml describes the package
C projects usually rely on Makefiles, CMake, or another build system to describe source files, include paths, and library outputs.
Rust projects usually start with Cargo.toml:
[package]
name = "iot-packet"
version = "0.1.0"
edition = "2024"
This describes a package. A package may contain a library crate, binary crates, or both.
Common entry files are:
src/main.rs -> binary crate entry
src/lib.rs -> library crate entry
For a command-line tool, start with src/main.rs. For a reusable library, expose APIs from src/lib.rs.
A crate is a compilation unit, not a C file
In C, each .c file is usually compiled into an object file and then linked.
In Rust, a crate is closer to a compilation unit. A library crate root is usually src/lib.rs:
pub fn parse_packet(data: &[u8]) -> Result<Packet, ParseError> {
// ...
}
A binary crate root is usually src/main.rs:
fn main() {
// ...
}
Do not map one Rust source file directly to one C translation unit. Rust’s module tree grows from the crate root; files are one way to store modules.
mod declares a module, not an include
C #include brings header text into the current translation unit:
#include "packet.h"
Rust mod is not textual inclusion. It declares a module and tells the compiler to look for its code:
mod packet;
If src/main.rs contains mod packet;, the compiler usually looks for:
src/packet.rs
or:
src/packet/mod.rs
Modern projects commonly use src/packet.rs or src/packet/xxx.rs with submodules.
Example:
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; does not mean “paste packet.rs here.” It means “this crate has a module named packet.”
Private by default, public with pub
C often uses file-scope static for internal helpers:
static int parse_header(const uint8_t *data, size_t len);
Rust items are private to their module by default:
fn parse_header(data: &[u8]) -> Result<Header, ParseError> {
// ...
}
Use pub when something should be accessible from outside the module:
pub fn parse_packet(data: &[u8]) -> Result<Packet, ParseError> {
let header = parse_header(data)?;
Ok(Packet { header })
}
For C developers, the important mapping is:
no pub -> internal helper
pub -> accessible API
Rust does not need public declarations in a separate header file. The public boundary is written directly on the item.
use imports names, not files
C #include affects declaration visibility and performs textual expansion.
Rust use only brings a path into the current scope:
use crate::packet::checksum;
fn main() {
let data = [1, 2, 3];
let sum = checksum(&data);
println!("{sum}");
}
Without use, you can still call through the full path:
let sum = crate::packet::checksum(&data);
The first distinction is:
mod -> declares that a module exists
pub -> controls access from outside the module
use -> brings a path name into the current scope
use does not cause a file to be compiled. The module tree determines which modules belong to the crate.
lib.rs is a library entry, not a header
For libraries, src/lib.rs often organizes the public API:
mod packet;
mod checksum;
pub use packet::{parse_packet, Packet, ParseError};
Internal files:
src/packet.rs
src/checksum.rs
lib.rs can re-export types and functions from internal modules. Then callers see:
use iot_packet::parse_packet;
instead of:
use iot_packet::packet::parse_packet;
This is somewhat like a C header collecting the public API, but it is not a text declaration file. lib.rs is Rust code, the crate root, and an API organization point.
main.rs calls the library, lib.rs holds reusable logic
Command-line tools often start with everything in main.rs. As code grows, a cleaner shape is:
src/main.rs
src/lib.rs
src/packet.rs
src/main.rs handles arguments, printing, and exit behavior:
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 exposes testable library APIs:
mod packet;
pub use packet::{parse_packet, Packet, ParseError};
This matches the C habit of separating CLI entry code from reusable library code. main should not carry all business logic.
File paths and module paths are not automatic
Rust module paths are determined by mod declarations and the module tree. Not every .rs file automatically enters the project.
If this file exists:
src/helper.rs
but nothing declares:
mod helper;
then it does not automatically become part of the crate.
This is somewhat like listing .c files in a C build system: a file existing on disk does not mean it has entered the compilation graph.
The difference is that Rust’s module tree is written in Rust code, and Cargo builds it starting from the crate root.
Minimal migration table
C project/build file -> Rust Cargo.toml + src/
C .c translation unit -> Rust crate is not a per-file equivalent
C .h public declaration -> Rust pub item / pub use
C #include "x.h" -> Rust mod / use split the jobs
C static helper function -> Rust private fn by default
C exported function -> Rust pub fn
C umbrella public header -> Rust lib.rs re-export
C main.c -> Rust src/main.rs
C reusable library code -> Rust src/lib.rs + modules
Rust’s module system is not a copy of .h/.c. It puts file organization, namespaces, and public APIs into one module tree. Boundaries that C maintains through headers and linker conventions are mostly expressed with mod, pub, and use in Rust.