Disclosures š§ This post is 100% human-written. Claude was used for feedback and to assist with the linker symbol diagram. Cursor was used for feedback and to ensure examples were compilable. The author of this post is deeply interested in the topic of life-before-main: he is the author of the ctor crate, and the creator of the linktime project that weāll be using in the examples below.
Every Rust binary has one thing in common: fn main() . If you come from the C world, that might be more familiar as int main(argc, argv) . Some platforms might obfuscate it a bit more, but under the hood, every binary has an entrypoint.
Weāre going to discuss what happens before main and what interesting things we can do there. In addition, weāll be showing some novel techniques for mutable data that arenāt in common use in the Rust ecosystem today.
This post is a deep dive into some technical details of how Rust source becomes a Rust binary. Some background knowledge may be helpful to the reader, including:
Before main
What might not be familiar to most developers is how you get into the main function. You see, under the hood for every language is the runtime. C has one: the C runtime that you might recognize as libc . Rust also has its own runtime: the Rust standard library. And because C is the lingua franca of runtimes for most executable code , Rust builds its own runtime atop of Cās, effectively building its own higher-level abstraction encapsulating Cās.
A runtime is a bit fuzzy to define. Itās both the executable code that lives on disk and compilable headers and libraries used at compile time. But the purpose of a runtime is always the same: integrating developer code with the platformās operating system.
Thereās an entire ecosystem of processing that happens before the function you declared as main starts up. C uses this to configure allocation, file access, thread-local storage and other C runtime services. Rust uses this time to configure parts of its own language and runtime. Specifically, Rust has infrastructure to handle panics and unwinding. Rust also needs to translate the C-style program arguments into its own std::env::args interface. The machinery for all this is visible in the Rust compiler project.
Runtimes make use of this pre-main phase because it guarantees (1) running before user code, and (2) a single-threaded, highly-consistent and predictably-ordered environment, which allow for reliable and deterministic initialization.
By not taking advantage of this environment, you are missing out on a very useful bootstrapping phase. Weāll see later on in this post how we can build some useful primitives making use of life before main.
... continue reading