{"slug": "there-is-life-before-main-in-rust", "title": "There Is Life Before Main in Rust", "summary": "A Rust developer explores what happens before the main function executes, detailing the runtime initialization phase and introducing novel techniques for mutable data. The post highlights how the Rust runtime builds atop C's runtime to configure panics, unwinding, and program arguments, and demonstrates the use of the linktime project for pre-main bootstrapping.", "body_md": "## There Is Life Before Main in Rust\n\n[permalink](/blog/2026/06/11/life-before-main/)\n\n**Disclosures**\n\n🧠 This post is [100% human-written](/assets/2026/06/remarkable_life_after_main.pdf). Claude was used for feedback and to assist with the linker symbol diagram. Cursor was used for feedback and to ensure examples were compilable.\n\nThe author of this post is deeply interested in the topic of life-before-main: he is the author of the [ ctor](https://crates.io/crates/ctor) crate, and the creator of the\n\n[project that we’ll be using in the examples below.](https://github.com/mmastrac/linktime)\n\n`linktime`\n\nEvery Rust binary has one thing in common: `fn main()`\n\n. If you come from the C world, that might be more familiar as `int main(argc, argv)`\n\n. Some platforms might obfuscate it a bit more, but under the hood, every binary has an entrypoint.\n\nWe’re going to discuss what happens *before* `main`\n\nand 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.\n\nThis 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:\n\n## Before main\n\nWhat 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](https://en.wikipedia.org/wiki/C_standard_library). Rust also has its own runtime: the Rust standard library. And because C is the lingua franca of runtimes for most executable code\n\n, Rust builds its own runtime atop of C’s, effectively building its own higher-level abstraction encapsulating C’s.\n\n[1](#fn:0)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.\n\nThere’s an entire ecosystem of processing that happens before the function you declared as `main`\n\nstarts 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 2 into its own\n\n[interface. The machinery for all this is](https://doc.rust-lang.org/beta/std/env/fn.args.html)\n\n`std::env::args`\n\n[visible in the Rust compiler project](https://github.com/rust-lang/rust/blob/main/compiler/rustc_codegen_ssa/src/base.rs#L501).\n\nRuntimes 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.\n\nBy 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.\n\n## Entry Points\n\nA binary starts when the operating system’s loader 3 - the part of the OS that loads the binary into memory and sets up the environment - hands off control. The runtime is responsible for accepting the hand-off from the loader. There’s a platform-specific hook on every OS that accepts the hand-off - to some extent this is the\n\n*real*main. On Linux, the entry point\n\n[is stored in the](https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#:~:text=e_entry)of the ELF header, and by default, the linker places the address of a symbol named\n\n`e_entry`\n\nfield`_start`\n\nthere. A similar hook [exists on Windows](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#:~:text=AddressOfEntryPoint), and boots the executable in\n\n[a function named](https://stackoverflow.com/questions/1583193/what-functions-does-winmaincrtstartup-perform). At this point the C runtime has a chance to configure itself, and the way that all runtimes do this is via initialization functions.\n\n`_WinMainCRTStartup`\n\nIn early iterations of runtimes, bootstrapping was a static tree of function calls: initialize file I/O, initialize the allocator, etc. As runtimes became more complex, this tree of function calls became more complex, and binary sizes increased to absorb more C runtime functionality that they may or may not need.\n\nOver time, linkers developed the ability to discard unused code before even writing the binary to disk (including unused parts of the C runtime), and with that came a need for a replacement for the static init call trees.\n\nThe most popular method 4 of declaring init code came from GCC:\n\n`__attribute__((constructor))`\n\n. The way this worked was to place a list of init functions into a contiguous chunk of the binary on disk. When the C runtime started, it could walk through each of these functions and call them, allowing various bits of the C runtime to request initialization without strongly coupling subsystems, and allowing the linker to jettison unused subsystems, init code and all.Eventually the need for constructor ordering became important enough that constructors could be given a priority and run in a specific order, allowing the runtime to initialize subsystems before and after each other. E.g., the memory allocation (`malloc`\n\n) subsystem might be needed for buffered file I/O.\n\nOn most platforms 5, the linker was called in to do the priority work: each platform ended up with a way to prioritize the order in which data gets written to sections, which allowed for the C runtime to end up with a well-ordered list of function pointers\n\n.\n\n[6](#fn:4)We can even build an example of this by hand in Rust using the `#[unsafe(link_section = \"...\")]`\n\nattribute ([try it in the Rust Playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=61a54d3cde75e732db52558f4ef9381c)):\n\n```\n/// Linux example: the modern glibc runtime uses `.init_array` to hold function\n/// pointers, and a numeric suffix allows them to be ordered. Note that priorities\n/// less than or equal to 100 are reserved for the runtime itself, so any code that\n/// wants to use the C runtime must use a priority of 101 or higher.\n\n// On Linux, `.init_array` holds _function pointers_, not functions.\n// We can convert a function to a function pointer with one of the below\n// blocks which is equivalent to this:\n//\n// #[used] // <-- without this, Rust might decide the init function is unused and remove it\n// #[unsafe(link_section = \".init_array.NNNNN\")] // <-- the section where we place the function pointer\n// static INIT_ARRAY_FN_PTR: extern \"C\" fn() \n//     = function; // <-- the function pointer data: we assign the function to it\n//\n// extern \"C\" fn function() { ... } // <-- the function itself\n\n#[used]\n#[unsafe(link_section = \".init_array.101\")]\nstatic INIT_FN_FIRST: extern \"C\" fn() = const {\n    extern \"C\" fn init() {\n        println!(\"Initializing (first!)\");\n    }\n    init\n};\n\n#[used]\n#[unsafe(link_section = \".init_array.201\")]\nstatic INIT_FN_SECOND: extern \"C\" fn() = const {\n    extern \"C\" fn init() {\n        println!(\"Initializing (second!)\");\n    }\n    init\n};\n\nfn main() {\n    println!(\"Main!\")\n}\n```\n\n## linktime: ctor, link-section and more\n\nThe examples in this post will work on Linux and various BSDs, but are not designed to be cross-platform examples. For example, macOS has `start`\n\nand `stop`\n\nsymbols, but they are named differently 7. Windows does not support\n\n`start`\n\nand `stop`\n\nsymbols, but has a set of rules for sorting sections that is effectively equivalent.Because platforms are so widely variable, we’ll be introducing the [ ctor](https://crates.io/crates/ctor) and\n\n[crates (from the](https://crates.io/crates/link-section)\n\n`link-section`\n\n[) as a way to abstract away platform-specific differences and hide the general complexity of linker work.](https://github.com/mmastrac/linktime)\n\n`linktime`\n\nprojectThe excellent [ inventory](https://crates.io/crates/inventory) and\n\n[are two other very popular crates built on the same principles, but have limitations](https://crates.io/crates/linkme)\n\n`linkme`\n\nthat make them less suitable for the examples in this post.\n\n[8](#fn:c1)If you’d like to learn more, the `link-section`\n\ncrate contains a [detailed report on platform-specific behaviour](https://crates.io/crates/link-section#:~:text=Platform%20Support).\n\nThe [ ctor](https://crates.io/crates/ctor) crate is designed to handle all of the boilerplate of registering constructors in a cross-platform way. This allows us to simplify our examples above to:\n\n```\nuse ctor::ctor;\n\n#[ctor(unsafe, priority = 101)]\nfn init1() {\n    println!(\"Initializing (first)!\");\n}\n\n#[ctor(unsafe, priority = 201)]\nfn init2() {\n    println!(\"Initializing (second)!\");\n}\n\nfn main() {\n    println!(\"Main!\")\n}\n```\n\nNote that neither example explicitly calls the init functions. The linker organized them in a way that the C runtime called them for us!\n\n## Sections and Linker Scripts\n\nThe process in which constructors are linked isn’t mysterious, though. In fact, compilers allow you to name the location in the binary (on most platforms called a “section”) you want to place any of your data and/or code. And by extension, and as we saw above, Rust allows this as well. The challenge, as we will see, is making use of this organizational feature.\n\nLinkers have been the key to C’s ability to target any form of binary for some time. Most linkers allow for developers to provide **linker scripts** - text files that live alongside your source code (which is compiled to object files) and instruct the linker on how those object files are assembled. Using a linker script, a single C file might become a Linux executable, or a block of raw assembly that lives in the boot sector of a hard drive.\n\nLinker scripts also allow for defining virtual symbols - that is, symbols that don’t exist in any source file but can be used by C code to access pointers to the underlying data in the loaded binary.\n\nLinker scripts are a complex topic and beyond the scope of this post, but we can [easily find examples](https://wiki.osdev.org/Linker_Scripts) of them in the wild:\n\n```\n// Adapted from https://wiki.osdev.org/Linker_Scripts\nSECTIONS\n{\n  .text.start (_KERNEL_BASE_) : {\n    startup.o( .text )\n  }\n\n  .text : ALIGN(CONSTANT(MAXPAGESIZE)) {\n_TEXT_START_ = .;\n    *(.text)\n_TEXT_END_ = .;\n  }\n\n  .data : ALIGN(CONSTANT(MAXPAGESIZE)) {\n_DATA_START_ = .;\n    *(.data)\n_DATA_END_ = .;\n  }\n}\n```\n\nIn the above example, the virtual symbols `_TEXT_START_`\n\nand `_TEXT_END_`\n\nare explicitly defined to point to the beginning and end of the `.text`\n\nsection, respectively. The period in `_TEXT_START_ = .;`\n\nis a special syntax that refers to [a location counter](https://sourceware.org/binutils/docs/ld/Location-Counter.html) that resolves roughly to the current output address in the binary.\n\n## Linker Symbols\n\nThis trips up most developers that encounter it for the first time, but the linker is *setting the address of the start and end symbols*, and therefore where the `static`\n\nwith the same name is placed, and *not* setting the value of symbols that are pointers. That is to say: the start and stop symbols aren’t a `*const Type`\n\n. The start and stop symbols carry no data themselves and are used for their addresses only! The section consists of the range of data *between* the start (inclusive) and stop (exclusive) symbols.\n\n| Section | Static | Value | Linker symbol(s) | |\n|---|---|---|---|---|\n`my_numbers` |\n`_DATA_1` |\n`11` |\n⎫ ⎬ ⎭ |\n`_DATA_1, _start_my_numbers` |\n`_DATA_2` |\n`22` |\n`_DATA_2` |\n||\n`_DATA_3` |\n`33` |\n`_DATA_3` |\n||\n`_DATA_4` |\n`44` |\n`_DATA_4` |\n||\n`(past the end)` |\n↤ | `_stop_my_numbers` |\n\nSpecifying start and end symbols for every section can be complex and tedious in linker scripts, so many linkers 9 eventually gained a feature where they could automatically define symbols bounding all sections in the executable. E.g., for GNU toolchains, a section named\n\n`MY_SECTION`\n\nwill automatically have symbols `__start_MY_SECTION`\n\nand `__stop_MY_SECTION`\n\ndefined. macOS has [a similar pattern](https://discourse.llvm.org/t/lld-support-for-ld64-mach-o-linker-synthesised-symbols/45145)where it synthesizes a\n\n`section$start`\n\nand `section$end`\n\nsymbol for each section.In the GNU linker, those sections not explicitly defined in the linker script are called “orphan sections” 10. One important thing to note: if (and only if!) a section’s name is compatible with a C symbol name, the linker will automatically define a\n\n`_start`\n\n- and `_stop`\n\n-prefixed symbol for the section. In the example you’ll see below, the section name `our_strings`\n\nthat we used works, but if we had chosen `our.strings`\n\nor `.our_strings`\n\nit would not have!You’ll see in the example below that the start and stop symbols are `MaybeUninit<()>`\n\n. The boundary symbols contain no data, and only their address is significant.\n\nThe ideal Rust type for these would be an “opaque external type” (this would be implemented by the [ extern_types feature](https://doc.rust-lang.org/beta/unstable-book/language-features/extern-types.html)). As these are not currently implemented in Stable Rust,\n\n`MaybeUninit`\n\nis a stand-in. It signifies to the compiler that the data is uninitialized, and generally not safe to read via reference. Since taking a [to a](https://blog.rust-lang.org/2024/10/17/Rust-1.82.0/#native-syntax-for-creating-a-raw-pointer)\n\n`&raw const`\n\npointer`static`\n\nitem is always valid, however, we can still safely capture its address without ever reading its value.[Try it in the Rust Playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=1696bdc67f02992cfde9de752e117e0e):\n\n```\nuse std::mem::MaybeUninit;\n\n#[used]\n#[unsafe(link_section = \"our_strings\")]\nstatic FIRST_STRING: &'static str = \"Hello, \";\n\n#[used]\n#[unsafe(link_section = \"our_strings\")]\nstatic SECOND_STRING: &'static str = \"world!\";\n\n// Note: these are not pointers. Instead, the linker has placed\n// the boundary symbols STATIC_STRING_START and STATIC_STRING_END at\n// the start and end of the section!\nunsafe extern \"C\" {\n    #[link_name = \"__start_our_strings\"]\n    static STATIC_STRING_START: MaybeUninit<()>;\n    #[link_name = \"__stop_our_strings\"]\n    static STATIC_STRING_END: MaybeUninit<()>;\n}\n\nfn main() {\n    let strings: &'static [&'static str] = unsafe {\n        // SAFETY: get the addresses of the start and end symbols without\n        // reading them.\n        let start = &raw const STATIC_STRING_START as *const &'static str;\n        let end = &raw const STATIC_STRING_END as *const &'static str;\n        std::slice::from_raw_parts(start, end.offset_from(start) as usize)\n    };\n    \n    // \"Hello, world!\"\n    println!(\"String: {}\", strings.join(\"\"));\n}\n```\n\nThe [ link-section](https://crates.io/crates/link-section) crate is designed to abstract away the details of these linker sections and convert them into traditional Rust slices with all standard slice operations available. We can use it to simplify the example above to:\n\n```\nuse link_section::{in_section, section};\n\n#[section(typed)]\nstatic OUR_STRINGS: link_section::TypedSection<&'static str>;\n\n#[in_section(OUR_STRINGS)]\nstatic FIRST_STRING: &'static str = \"Hello, \";\n\n#[in_section(OUR_STRINGS)]\nstatic SECOND_STRING: &'static str = \"world!\";\n\nfn main() {\n    println!(\"String: {}\", OUR_STRINGS.join(\"\"));\n}\n```\n\nIn these examples we’re submitting items to the link section in a single module within a single crate, but that’s not a requirement. In fact, the power of link sections is that you can submit items to a link section from *any* crate that contributes code to a binary - the linker will gather them all together just before writing the final binary.\n\n## Dependency Injection\n\nThe registration pattern we’re about to build is [Dependency Injection](https://en.wikipedia.org/wiki/Dependency_injection) by another name. This is a well-known pattern: frameworks like [Dagger](https://dagger.dev/) and [Spring](https://spring.io/) are built on the same principle that *consumers* of registration data should not be coupled to the *providers* of that data. A *provider* registers data at its definition site, a *consumer* simply reads the registry.\n\nWhat’s somewhat different with linker sections versus traditional DI is that in DI the framework often needs to walk the module graph or scan loaded classes at startup to discover both providers and consumer sites. With linker sections, this magic is handled when the binary is written. The linker is the one that gathers all of the provider data and makes it trivially available to the consumer.\n\nThe example below uses a `link_section::section`\n\nto register CLI subcommands and is an instance of this pattern. More complex projects like [Turbopack](https://github.com/vercel/next.js/blob/canary/turbopack/) use this pattern to register string-pool constants, and the registration machinery used for serialization/deserialization and [turbotask incremental compilation functions](https://web.archive.org/web/20250222021941/https://turbo.build/pack/docs/incremental-computation). A hypothetical webserver could make use of this pattern to register routes and middleware that are automatically collected at build time. The core mechanism is the same: the contributors place data into a shared registration system from any crate in the dependency tree, and the consumer reads the collected data without having to know where it was provided from.\n\n## Using Sections for Registration\n\nOne advantage we have in doing work before main is that it is well-behaved. No threads are running unless we start them. This means we are able to avoid the complexity of locks and other synchronization primitives in many cases, and that we can explicitly split our writable and immutable phase of our data’s lifecycle clearly: before and after main. And because of that, accessing data in the running program can become both simpler and more efficient by avoiding the need to acquire and release locks.\n\nFirst, we’ll define our subcommand, a `const`\n\nconstructor function, and a `#[section]`\n\nto collect them:\n\n```\nuse std::collections::VecDeque;\nuse std::path::Path;\n\nuse link_section::{in_section, section};\n\nstruct CliSubcommand {\n    is_default: bool,\n    name: &'static str,\n    description: &'static str,\n    f: fn(&Path, &[String]),\n}\n\nimpl CliSubcommand {\n    const fn new(name: &'static str,\n                 description: &'static str, \n                 f: fn(&Path, &[String])) -> Self {\n        Self { is_default: false, name, description, f }\n    }\n\n    const fn new_default(name: &'static str,\n                        description: &'static str, \n                        f: fn(&Path, &[String])) -> Self {\n        Self { is_default: true, name, description, f }\n    }\n}\n\n#[section(typed)]\nstatic CLI_SUBCOMMANDS: link_section::TypedSection<CliSubcommand>;\n```\n\nThen we’ll register subcommands - these can live *anywhere* in your code:\n\n```\nmod list {\n    #[in_section(CLI_SUBCOMMANDS)]\n    static CLI_SUBCOMMAND_LIST: CliSubcommand = \n        CliSubcommand::new(\"list\", \"List all items\", |_exe, _args| {\n            println!(\"Listing all items\");\n        });\n}\n\nmod add {\n    #[in_section(CLI_SUBCOMMANDS)]\n    static CLI_SUBCOMMAND_ADD: CliSubcommand = \n        CliSubcommand::new(\"add\", \"Add a new item\", |_exe, _args| {\n            println!(\"Adding a new item\");\n        });\n}\n\nmod help {\n    #[in_section(CLI_SUBCOMMANDS)]\n    static CLI_SUBCOMMAND_HELP: CliSubcommand = \n        CliSubcommand::new_default(\"help\", \"Show help\", |exe, _args| {\n            println!(\"Usage: {} <subcommand> [options]\", exe.display());\n            println!();\n            println!(\"Subcommands:\");\n            for subcommand in CLI_SUBCOMMANDS {\n                println!(\"  {}: {}\", subcommand.name, subcommand.description);\n            }\n        });\n}\n```\n\nAnd then in our `main`\n\nfunction we can dynamically dispatch to any registered subcommand without ever having to know what they are or where they live. It only needs to be able to see the `CLI_SUBCOMMANDS`\n\nsection definition:\n\n``` js\nfn main() {\n    let mut args: VecDeque<String> = std::env::args().collect();\n    let exe = args.pop_front().expect(\"No executable name provided\");\n    let exe = Path::new(&exe);\n    let subcommand_name = args.pop_front().unwrap_or_default();\n    let rest: Vec<String> = args.into();\n\n    // Try to find the subcommand by name\n    for cmd in CLI_SUBCOMMANDS {\n        if cmd.name == subcommand_name {\n            (cmd.f)(exe, &rest);\n            return;\n        }\n    }\n    // If no subcommand was found, fall back to the default subcommand\n    for cmd in CLI_SUBCOMMANDS {\n        if cmd.is_default {\n            (cmd.f)(exe, &rest);\n            return;\n        }\n    }\n}\n```\n\nRunning the code above works as you’d expect:\n\n```\n$ ./cli\nUsage: ./cli <subcommand> [options]\n\nSubcommands:\n  list: List all items\n  add: Add a new item\n  help: Show help\n\n$ ./cli list\nListing all items\n```\n\n## Beyond Immutable Data\n\nThis section deals with some more advanced topics. Familiarity with [Rust Atomics and Locks](https://marabos.nl/atomics/), or at least reading the first chapter on the basics of Rust concurrency, is recommended!\n\nThe example above assumes that the linked data is immutable. But that’s only half the power of using linker organization for data. Mutability in global static data is a [common problem with well-known solutions](https://doc.rust-lang.org/reference/interior-mutability.html) in standard Rust. We could potentially use Rust’s built-in tools for interior mutability like mutexes, or atomic types, for example. Each of those comes with *some* runtime cost. If they are “uncontended” they aren’t expensive, but they are not necessarily free. [11](#fn:7)\n\nBut what if we want to minimize the overhead of runtime data access? Immutable data is trivial: Rust allows safe concurrent access to immutable data by default 12. Rust has strict requirements for mutable data, however. There are two requirements to safely mutate data: (1) the modifications must be done in a thread-safe manner, and (2), there must never be more than one reference to the data if a mutable reference exists.\n\nAt the beginning of this post, we mentioned that life-before-main is a useful place to bootstrap because no threads are running unless we start them. And the solution to (1) is trivial if the data is currently accessible to a single thread only! We don’t need to do anything atomically; we only need to ensure that all of the changes we make to that data [“happen before”](https://marabos.nl/atomics/) any reads to the data. In a single-threaded environment, “happens before” is automatic 13. This means that we can mutate data in a link-section before main and it will be safe to access, lock free, from any thread after main.\n\nThe resolution for (2) is similar: as long as we only ever take a mutable reference (and only a mutable reference!) *before main*, there will never be more than one reference to the data when a mutable reference exists.\n\nThe pre-main environment satisfies *both* (1) and (2), without needing to reach for locks or other synchronization primitives.\n\nThere’s also one additional gotcha with linker sections we need to be very careful with: the slice that contains all of the items in a section is an *alias* to the static item that lives in the section. The rules about aliasing apply to both the slice and the static item, and you *must* ensure that static items are placed in `UnsafeCell`\n\nto safely mutate them from the slice 14. Rust does not allow a static item to be modified through other means. With static items that aren’t wrapped in an\n\n`UnsafeCell`\n\n, LLVM may consider itself free to cache, reorder or otherwise make assumptions about the data itself. `UnsafeCell`\n\nitself is not `Sync`\n\n, so you’ll need to add your own wrapper types on top of this!Note that in the example below, we’re now using `MaybeUninit<SyncUnsafeCell<...>>`\n\nfor the boundary symbols and `SyncUnsafeCell<...>`\n\nfor the items.\n\nBecause we’re planning on sorting the slice, we need to tell Rust that the slice items are not immutable so the data doesn’t end up in read-only memory. By using a type that includes `UnsafeCell`\n\n- a semantic signal that Rust uses to indicate [interior mutability](https://doc.rust-lang.org/reference/interior-mutability.html) - the Rust compiler will then know to place that data in a part of the binary that can be written.\n\nOn some platforms (Windows in particular), omitting this from the data items will cause segmentation faults when trying to sort the slice. On other platforms (AIX for example), section mutability is part of a section’s identifier, so the boundary symbols’ mutability needs to match the section’s mutability!\n\nLet’s walk through an example of how we might otherwise do something like this. We’re going to build a string interning pool defined entirely at link-time, and add a wrinkle: we want to be able to sort the slice of interned strings at runtime so we can quickly intern a string by value if needed via binary search ([try it in the Rust Playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=cc4df8dc5aba46357a5f6c64ce11e587)):\n\n```\nuse std::cell::UnsafeCell;\nuse std::mem::MaybeUninit;\n#[cfg(debug_assertions)]\nuse std::sync::atomic::{AtomicBool, Ordering};\n\n/// Nightly Rust offers a built-in `SyncUnsafeCell`. This is a minimal\n/// reimplementation of that: \n/// <https://doc.rust-lang.org/std/cell/struct.SyncUnsafeCell.html>\n#[repr(transparent)]\nstruct SyncUnsafeCell<T: ?Sized>(UnsafeCell<T>);\n// SAFETY: safety burden of UnsafeCell is placed entirely on the user\nunsafe impl<T: ?Sized + Sync> Sync for SyncUnsafeCell<T> {}\n\nmacro_rules! intern_string {\n    ($name:ident, $string:literal) => {\n        #[allow(unused)]\n        const $name: &'static str = const {\n            // This is not a common pattern, but it's entirely valid\n            // to nest static items inside of const blocks. \n            // You can think of this as a way to hide the symbols\n            // in a completely anonymous namespace.\n            const VALUE: &str = $string;\n            // Safety note: this static must _never_ be used. This is\n            // purely a submission to the linker and _any_ access to it\n            // may be UB.\n            #[used]\n            #[unsafe(link_section = \"our_strings\")]\n            static ITEM: SyncUnsafeCell<&'static str> = \n                SyncUnsafeCell(UnsafeCell::new(VALUE));\n            VALUE\n        };\n    };\n}\n\nintern_string!(WORLD, \"world\");\nintern_string!(EXCLAMATION, \"!\");\nintern_string!(HELLO, \"hello\");\nintern_string!(FROM, \"from\");\nintern_string!(RUST, \"Rust\");\n\nunsafe extern \"C\" {\n    #[link_name = \"__start_our_strings\"]\n    static STATIC_STRING_START: MaybeUninit<SyncUnsafeCell<()>>;\n    #[link_name = \"__stop_our_strings\"]\n    static STATIC_STRING_END: MaybeUninit<SyncUnsafeCell<()>>;\n}\n\n/// Debug check to make sure the slice is sorted once and only once. This _could_\n/// be enabled in release mode without any major performance impact, but we have enough\n/// guarantees in place. Note that atomic access _does_ establish some memory ordering\n/// guarantees, but the soundness guarantees are upheld with or without this atomic check.\n#[cfg(debug_assertions)]\nstatic SLICE_IS_SORTED: AtomicBool = AtomicBool::new(false);\n\n// Implementation note: this function must not be called before `SORT_STRINGS_CTOR` has\n// run.\nfn interned_strings() -> &'static [&'static str] {\n    // We use Acquire/Release pairing as a double-initialization check\n    #[cfg(debug_assertions)]\n    debug_assert!(SLICE_IS_SORTED.load(Ordering::Acquire), \"Oh no! Slice was not sorted!\");\n\n    // SAFETY: we are calling this after main and we can guarantee that no\n    // mutable reference is still alive. Since we know that no other code\n    // is running before main, and that `SORT_STRINGS_CTOR` will run before main,\n    // we can guarantee creating these slices is safe as 1) the sort \"happens-before\"\n    // any access and 2) the mutable reference has been closed before any read-reference\n    // access (satisfying aliasing XOR mutability requirement).\n    let strings: &'static [&'static str] = unsafe {\n        let start = &raw const STATIC_STRING_START as *const &'static str;\n        let end = &raw const STATIC_STRING_END as *const &'static str;\n        std::slice::from_raw_parts(start, end.offset_from(start) as usize)\n    };\n    strings\n}\n\n// Implementation note: this function assumes the slice has been sorted. See\n// the guarantee above on `interned_strings` for reasoning.\nfn maybe_intern_string(s: impl AsRef<str>) -> Option<&'static str> {\n    let s = s.as_ref();\n    let strings = interned_strings();\n    strings.binary_search(&s).ok().map(|index| strings[index])\n}\n\n// SAFETY: We use the reserved `.init_array.0` priority because we do not\n// access any C runtime functions (sort_unstable does not allocate) and we\n// want to run before all other code. `.init_array.101` would work in our\n// case, but this prevents other early-init code from accidentally running\n// in the wrong order. `SLICE_IS_SORTED` is a debug check to make sure that\n// doesn't happen. Note that all early-init code is tagged with `unsafe` so\n// it always needs to be aware of safety guarantees of all APIs it touches.\n#[used]\n#[unsafe(link_section = \".init_array.0\")]\nstatic SORT_STRINGS_CTOR: extern \"C\" fn() = const {\n    extern \"C\" fn sort_strings() {\n        // We use Acquire/Release pairing as a double-initialization check\n        #[cfg(debug_assertions)]\n        debug_assert!(!SLICE_IS_SORTED.load(Ordering::Acquire), \"Oh no! Sorted twice?!?\");\n        // SAFETY: we are calling this before main and we can guarantee that no\n        // reference from `interned_strings` exists yet because we know no other\n        // threads will be running, and we're not calling `interned_strings` yet.\n        let strings: &mut [&'static str] = unsafe {\n            // SAFETY: the bounds markers are not mutable, but we can safely\n            // cast them to mutable pointers because we know the data behind\n            // them is stored within `UnsafeCell` which is Rust's way of \n            // giving us interior mutability.\n            let start = &raw const STATIC_STRING_START as *mut &'static str;\n            let end = &raw const STATIC_STRING_END as *mut &'static str;\n            std::slice::from_raw_parts_mut(start, end.offset_from(start) as usize)\n        };\n        strings.sort_unstable();\n        #[cfg(debug_assertions)]\n        SLICE_IS_SORTED.store(true, Ordering::Release);\n    }\n    sort_strings\n};\n\nfn main() {\n    for (i, s) in interned_strings().iter().enumerate() {\n        println!(\"[{i}]: {s}\");\n    }\n\n    println!(\"{}, {}{}\", HELLO, WORLD, EXCLAMATION);\n    println!(\n        \"{}, {}{}\",\n        maybe_intern_string(\"hello\").unwrap(),\n        maybe_intern_string(\"world\").unwrap(),\n        maybe_intern_string(\"!\").unwrap()\n    );\n}\n```\n\nThe above example is pretty heavy (and a bit thicker thanks to the generous commentary), but it’s a good example of how much boilerplate crates like `ctor`\n\nand `link-section`\n\ncan save you.\n\nThe equivalent using those crates can make use of the `TypedMutableSection`\n\nand a `ctor`\n\nto ensure the items are sorted before main. Note that the requirements for `TypedMutableSection`\n\nare that the items must be `const`\n\n- the reason is that the mutable section uses a similar style of code to the manually-implemented example above.\n\n```\n//! String interning pool using `ctor` and `link-section`.\nuse ctor::ctor;\nuse link_section::{in_section, section};\n\n#[section(mutable)]\nstatic INTERNED_STRINGS: link_section::TypedMutableSection<&'static str>;\n\n#[in_section(INTERNED_STRINGS)]\nconst WORLD: &'static str = \"world\";\n\n#[in_section(INTERNED_STRINGS)]\nconst EXCLAMATION: &'static str = \"!\";\n\n#[in_section(INTERNED_STRINGS)]\nconst HELLO: &'static str = \"hello\";\n\n#[in_section(INTERNED_STRINGS)]\nconst FROM: &'static str = \"from\";\n\n#[in_section(INTERNED_STRINGS)]\nconst RUST: &'static str = \"Rust\";\n\n#[ctor(unsafe)]\nfn sort_strings() {\n    let strings: &mut [&'static str] = unsafe { INTERNED_STRINGS.as_mut_slice() };\n    strings.sort_unstable();\n}\n\nfn maybe_intern_string(s: impl AsRef<str>) -> Option<&'static str> {\n    let s = s.as_ref();\n    let strings = INTERNED_STRINGS.as_slice();\n    strings.binary_search(&s).ok().map(|index| strings[index])\n}\n\nfn main() {\n    for (i, s) in INTERNED_STRINGS.iter().enumerate() {\n        println!(\"[{i}]: {s}\");\n    }\n\n    println!(\"{}, {}{}\", HELLO, WORLD, EXCLAMATION);\n    println!(\n        \"{}, {}{}\",\n        maybe_intern_string(\"hello\").unwrap(),\n        maybe_intern_string(\"world\").unwrap(),\n        maybe_intern_string(\"!\").unwrap()\n    );\n}\n```\n\nThis particular example isn’t impossible without link sections, of course. What we get from the patterns we’ve discussed in this post are three things: (1) the guaranteed aggregation of tagged items, with all data pre-allocated and contiguous in memory; (2) the ability to distribute registrations *anywhere* in the code; and (3) a guaranteed count of the items in the section.\n\nOne major benefit that falls out of the three advantages above is that link sections require *no* allocations. If we were to rewrite this without link sections we’d be allocating a `HashMap`\n\n, `Vec`\n\nor other data structure, and potentially resizing it a number of times as we gather items (because we don’t actually know how many items we’ll have until runtime!).\n\nThe second major benefit that falls out is the *Inversion of Control*. The dependency graph for a traditional “collection” approach looks like below, with shared types deeply nested in the dependency graph, modules depending on that shared types module, and then a collector module that depends on all those modules to collect their types:\n\nThe change doesn’t seem large, but there *is* a large impact: the collector can now live *anywhere* and no longer needs to care what modules are contributing data:\n\nAnd of course, we aren’t just limited to slices: you’ll find that there are analogues to many data-structures with link-time support in the [ scattered-collect crate](https://docs.rs/scattered-collect/latest/scattered_collect/index.html):\n\n`Scattered*Slice`\n\n: Various`Vec`\n\n-like structures that provide slices (and optionally sorting).`ScatteredMap`\n\n/`ScatteredSet`\n\n: An analogue to`HashMap`\n\n/`HashSet`\n\nthat provides hashed key-to-value lookup with some minimal pre-main initialization.\n\n## But Seriously: When Not to Use This\n\nLink-time computation is fun and powerful, and it’s not always the right tool for the job. There is often a non-link-time equivalent: manually collecting data in crates that have visibility into each crate that wishes to contribute data. This can be inconvenient at times - instead of the contributors seeing a single contribution point “upstream” in a core crate, a “collector” crate with lots of crate references is required to collect them all.\n\nDead-code elimination becomes challenging: the link-section crate (and the `linkme`\n\nequivalent) both decorate all items using `#[used]`\n\n, so the linker is disallowed from pruning unused data. Figuring out how to make link-time collection and dead-code elimination work well together is a complex problem beyond the scope of this post. For smaller bits of data like interned string atoms this might not be a problem, but if a program wants to intern larger chunks of data like chunks of raw JSON/JavaScript, or extensive data structures, this may add up to a lot of dead-code that may be difficult to identify.\n\nPre-main constructor functions [have limitations](https://docs.rs/ctor/latest/ctor/life_before_main/index.html): they cannot panic 15, Rust does not guarantee that all stdlib functions are available, and the order that the initialization functions are called within a given priority level is not guaranteed and highly platform-dependent. With careful planning, these limitations may be worked around but life-before-main may not be correct for subtle and difficult-to-debug reasons.\n\nAt this time, [Miri](https://github.com/rust-lang/miri/) is not fully compatible with all pre-main constructors and link-section constructions: it has a very basic view of pre-main execution, and does not model link sections at all. This may improve over time, but as of the time of writing, LLVM sanitizers ([ASan](https://clang.llvm.org/docs/AddressSanitizer.html), [TSan](https://clang.llvm.org/docs/ThreadSanitizer.html), and others) are the recommended way to test for undefined behaviour.\n\nThe *Inversion of Control* pattern also has a cost: it makes it potentially harder to audit all the places that contribute data to a link section.\n\nIn reality, many widely-deployed and heavily-used Rust programs already rely on pre-main functionality: the `ctor`\n\n, `link-section`\n\n, `inventory`\n\nand `linkme`\n\ncrates are used by many downstream crates today.\n\n**Briefly, on WASM**\n\nThe examples above omitted a fairly important platform, though for good reason. WASM does not currently support linker sections natively because of an inconvenient choice many years back ([51088](https://github.com/rust-lang/rust/issues/51088) and [52353](https://github.com/rust-lang/rust/pull/52353) for more details). Instead of allowing the `#[link_section]`\n\nannotation to place items in a true *code* section, the items are placed in a WASM *custom section* which is inaccessible to the WASM code itself!\n\nThe `linktime`\n\ncrates *do* support WASM and have [an emulation workaround](https://docs.rs/crate/link-section/latest#:~:text=On%20WASM%20platforms) that makes the approaches work for WASM binaries, but the author of this post hopes to make a suggestion in the near future on how proper WASM support could be added!\n\n## Conclusions\n\nYou can do a lot before main, and the benefits of doing so are pretty significant for certain cases. It’s a highly-ordered, highly-controllable environment that lets you more confidently do a lot of work without locks, atomics and other synchronization primitives. Link sections give you arbitrary aggregation and co-location of related data across your whole binary without awkward crate dependency order. In a lot of cases you can even avoid allocations completely which helps keep you away from one of the worst allocator sins: churn of allocations leading to fragmentation.\n\nFor further reading, check out the various crates discussed in this post:\n\n: Module initialization functions that run before`ctor`\n\n`main`\n\n: Not discussed in this post, but the shutdown analogue to`dtor`\n\n`ctor`\n\n: Linker-managed typed (slices) and untyped sections, with mutability support.`link-section`\n\n: Linker-managed higher-level collections: slices, sorted slices, maps`scattered-collect`\n\n## Thanks\n\nThanks to my lovely wife [Mia](https://github.com/ojeda-e), [Benjamin Woodruff](https://x.com/_bgwoodruff) and [Luke Sandberg](https://x.com/lukeisandberg), as well as [@ssokolow](https://lobste.rs/s/rs1t8s/there_is_life_before_main_rust#c_qoljqy) for their feedback and review. This post would not have been possible without their help.\n\n-\nGo is a notable exception in that it avoids the C runtime on some platforms, only using libc as an ABI stability boundary on platforms that require it e.g., Apple’s\n\n`libSystem.dylib`\n\nand OpenBSD’s`libc`\n\n.[↩](#fnref:0) -\nOn Windows, these are DOS-style (which were in turn derived from CP/M-style) arguments.\n\n[↩](#fnref:0b) -\nBefore the loader runs, the program is just some bytes on a disk and the loader (which can be the kernel itself or a user-space system component like\n\n`ld.so`\n\non Linux) maps those bytes into memory and hands off control.[↩](#fnref:1) -\nThe most popular method… in the humble author’s opinion.\n\n[↩](#fnref:2) -\nmacOS does not support this. The C runtime does its own initialization and then just runs every user constructor function in the order the linker saw them.\n\n[↩](#fnref:3) -\nAIX has a special symbol naming convention for constructor functions: the\n\n`sinit`\n\nprefix, followed by a hexadecimal priority value.[↩](#fnref:4) -\nThis will be discussed later in the post, but macOS synthesizes a\n\n`section$start`\n\nand`section$end`\n\nsymbol for each section instead of a`__start_`\n\nand`__stop_`\n\nsymbol.[↩](#fnref:c0) -\n`linkme`\n\ncreates distributed slices, but does not currently support WASM, and does not support mutable section data required to sort a section.`inventory`\n\nsupports WASM, but requires a`ctor`\n\n-like function per item in the section.[↩](#fnref:c1) -\nThe Windows linker does not support this feature, but instead\n\n[defines an overall sort order for](https://devblogs.microsoft.com/oldnewthing/20181107-00/?p=100155)that is effectively equivalent.*symbols*[↩](#fnref:5) -\nOrphan sections have\n\n[a complicated algorithm for placement](https://maskray.me/blog/2024-06-02-understanding-orphan-sections).[↩](#fnref:6) -\nFor example, an atomic value must always be re-read, and that may incur use of CPU cache which is pretty numerous these days, but definitely not infinite.\n\n[↩](#fnref:7) -\n[As long as it’s](https://doc.rust-lang.org/book/ch16-04-extensible-concurrency-sync-and-send.html).`Sync`\n\n`Sync`\n\nmeans that it’s safe to share a reference to the data between threads.[↩](#fnref:8) -\nThis is a complex topic, and\n\n[Rust Atomics and Locks](https://marabos.nl/atomics/)is the best resource for learning more. Starting a new thread means that all the previous writes “happen before” anything on the new thread, but we’ll leave the proof of this for the reader (or possibly a future post).[↩](#fnref:9) -\nEven taking a reference to a mutable static\n\n[is disallowed by default in Rust 2024!](https://doc.rust-lang.org/edition-guide/rust-2024/static-mut-references.html)[↩](#fnref:10) -\nWell, more accurately they\n\n*can*panic but they*shouldn’t*panic. In fact you’ll get a*double*panic:`thread '<unnamed>' panicked at ...: pre-main panic!`\n\nand then immediately`thread '<unnamed>' panicked at ...: panic in a function that cannot unwind`\n\n.[↩](#fnref:11)", "url": "https://wpnews.pro/news/there-is-life-before-main-in-rust", "canonical_source": "https://grack.com/blog/2026/06/11/life-before-main/", "published_at": "2026-06-11 17:32:42+00:00", "updated_at": "2026-06-13 02:31:51.350982+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Rust", "linktime", "ctor", "Claude", "Cursor", "Linux", "Windows"], "alternates": {"html": "https://wpnews.pro/news/there-is-life-before-main-in-rust", "markdown": "https://wpnews.pro/news/there-is-life-before-main-in-rust.md", "text": "https://wpnews.pro/news/there-is-life-before-main-in-rust.txt", "jsonld": "https://wpnews.pro/news/there-is-life-before-main-in-rust.jsonld"}}