A .rs file is plain UTF-8 text that holds one Rust module. The
compiler rustc is always given one source file, the root of a
crate, and it follows mod declarations to load the others. This
page covers what the Rust Reference describes in its chapters “Input format” and “Crates
and source files”, and adds the file conventions of the Cargo build tool.
Crates and the module tree
A crate is the unit of compilation and linking. Each compilation reads one crate in source form and produces one crate in binary form: an executable or a library. A crate holds a tree of nested modules. The top of the tree is an anonymous module, and every item in the crate has a path that shows where it sits in the tree.
- Every source file is a module, but a module does not need its own file: a
mod name { … }block nests a module inside a file. - A file’s name and position in the tree are decided from outside it: by a
moddeclaration in another file, or by the crate name for the root file. - A source file holds zero or more items, and may begin with inner attributes that apply to its module.
Crate:
InnerAttribute*
Item*
The crate type chooses the output. It is set with --crate-type on the
command line or #![crate_type = "…"] in the root file.
| Type | Output |
|---|---|
bin | An executable; the root must have a main function |
lib | A Rust library in the compiler’s recommended form; the actual form is up to the compiler |
rlib | A Rust static library, linked by later compilations of Rust code |
dylib | A Rust dynamic library |
cdylib | A dynamic library with a C interface, for use from other languages |
staticlib | A static library with a C interface |
proc-macro | A compiler plugin holding procedural macros |
Encoding and input processing
Before tokens are read, the compiler applies these steps to the file (Reference: Input format).
- UTF-8. The file is read as UTF-8. Anything else is a compile-time error.
- Byte order mark. A leading
U+FEFFis removed if present. - Line endings. Each CR LF pair (
U+000D U+000A) is replaced by a single LF. A lone CR is left in place and counts as white space, but it is rejected inside string literals and doc comments. The replacement happens once: CR CR LF LF becomes CR LF LF, not LF LF. - Shebang removal. A shebang line is removed if present.
- Tokenisation. The remaining characters are split into the tokens described on the Lexical structure page.
The include! macro applies the byte order mark, line ending and (in
item position only) shebang steps to the file it includes.
include_str! and include_bytes! apply none of them.
Shebang line
A shebang is an optional first line that tells a Unix-like system which interpreter
runs the file. It starts with #! at the very start of the file, or
straight after a byte order mark, and runs to the first line feed or the end of the
file (Reference: Shebang).
If the #! is followed by [, ignoring any white space and
comments in between, the line is not a shebang. It is an inner attribute instead,
such as #![allow(unused)].
#!/usr/bin/env rustx
fn main() {
println!("Hello!");
}
#Inner attributes
Attributes written #![…] at the top of a file, after any shebang, apply
to the enclosing module. In the root file of a crate they apply to the crate as a
whole. Some apply only there, such as crate_name,
crate_type, no_std, no_main and
recursion_limit. The
Attributes page lists them all.
#![crate_name = "projx"] // the crate's name
#![crate_type = "lib"] // the kind of output
#![warn(non_camel_case_types)] // a lint level; valid in any module
Doc comments written //! or /*! … */ are inner attributes
too: they are shorthand for #![doc = "…"] and document the enclosing
module.
Editions
Every crate is compiled in one edition:
2015, 2018, 2021 or 2024. The edition is not recorded in the .rs file. It
is given to the compiler with --edition; without the option
rustc uses 2015. Cargo passes the value of the edition key
in Cargo.toml. The edition can change how the same text is tokenised and
parsed, for example whether async is a keyword.
How mod maps to files
Module:
unsafe? mod IDENTIFIER ;
| unsafe? mod IDENTIFIER { InnerAttribute* Item* }
A module with no body is loaded from a file. Without a path attribute
the file’s path follows the module’s path: ancestor modules are folders, and the
module’s contents are in a file named after it plus .rs (Reference:
Modules). The older form puts the module in mod.rs inside a folder named
after it. Having both for one module is an error.
| Module path | File | Older form |
|---|---|---|
crate | src/lib.rs or src/main.rs | |
crate::util | src/util.rs | src/util/mod.rs |
crate::util::config | src/util/config.rs | src/util/config/mod.rs |
Before rustc 1.30, mod.rs was the only way to give a
module child modules in their own files.
Module names read from the file system must be ASCII, unless a path
attribute gives the file. The unsafe keyword before mod is
accepted by the parser but rejected afterwards, so that macros can consume it.
The path attribute
#[path = "…"] sets the file for a module. For a module with no inline
modules around it, the path is relative to the folder of the source file.
| Source file | Declaration | File loaded | Module path |
|---|---|---|---|
src/a/b.rs | #[path = "foo.rs"] mod c; | src/a/foo.rs | crate::a::b::c |
src/a/mod.rs | #[path = "foo.rs"] mod c; | src/a/foo.rs | crate::a::c |
When the declaration sits inside inline modules, the path also goes through a
folder for each of those modules. In a root file or mod.rs, these
folders start next to the file. In any other file, the path starts with a folder
named after that file’s module:
mod inline {
#[path = "other.rs"]
mod inner;
}
src/a/b.rs this loads src/a/b/inline/other.rs; in src/a/mod.rs it loads src/a/inline/other.rsThe main function
A crate with a main function can be built as an executable. main
must take no arguments, have no trait or lifetime bounds and no where
clause, and its return type must implement std::process::Termination
(Reference: Crates and source files). The standard library implements
Termination for (), !,
Infallible, ExitCode and Result<T, E> where
T: Termination and E: Debug. The main function
may be an import, such as use foo::bar as main;.
#![no_main] stops the compiler emitting a main symbol,
which is useful when another linked object defines it.
The Cargo package layout
Cargo is the standard build tool. Its layout is a set of conventions, not part of
the language. A package has a Cargo.toml manifest, and a
Cargo.lock file that records exact dependency versions, in its root.
Cargo.toml
Cargo.lock
build.rs (optional build script)
src/
lib.rs the library crate's root
main.rs the default binary crate's root
bin/
tool.rs another binary, named tool
other/main.rs a binary split over several files
benches/large-input.rs a benchmark target
examples/simple.rs an example target
tests/integration.rs an integration test target
- Each file directly in
src/bin,benches,examplesortestsis a crate of its own. For a target spread over several files, make a folder with amain.rsin it; the target takes the folder’s name. - Target names use
kebab-caseand module names usesnake_case. Crate names may not contain hyphens, so Cargo replaces-with_in a package name when it makes the crate name. - The
editionkey and the dependencies are inCargo.toml. A.rsfile does not say which crates it may use; in edition 2018 and later, a dependency’s name can be used directly in paths.
build.rs
A build.rs file in the package root is a Rust program that Cargo
compiles and runs just before building the package. It is typically used to build
bundled C libraries, find system libraries or generate code. Its current directory
is the package root, and a non-zero exit status stops the build.
It talks to Cargo by printing lines of the form cargo::KEY=VALUE to
standard output (the cargo:: form needs Rust 1.77; older versions use
cargo:). Other lines are ignored.
| Directive | Meaning |
|---|---|
rerun-if-changed=PATH | Run the script again when this file or folder changes |
rerun-if-env-changed=VAR | Run the script again when this environment variable changes |
rustc-link-lib=LIB | Link a library (-l) |
rustc-link-search=PATH | Add a library search path (-L) |
rustc-flags=FLAGS | Pass -l and -L flags |
rustc-link-arg=FLAG | Pass a linker flag; variants exist for each target kind |
rustc-cfg=KEY[="VALUE"] | Enable a cfg setting |
rustc-check-cfg=CHECK_CFG | Declare the cfg names and values to expect (Rust 1.80) |
rustc-env=VAR=VALUE | Set an environment variable readable with env! |
error=MESSAGE | Fail the build with an error (Rust 1.84) |
warning=MESSAGE | Show a warning |
metadata=KEY=VALUE | Pass values to the direct dependents of a links package |
Generated files must go in the folder named by the OUT_DIR environment
variable, and nowhere else. With no rerun-if directive, Cargo re-runs the
script whenever any file in the package changes.
// build.rs
fn main() {
println!("cargo::rerun-if-changed=src/hello.c");
println!("cargo::rustc-link-lib=z");
}