Expand description
§Life-Before-Main and Other Link-Time Hazards
The linker is a powerful tool that can be used to perform a number of tricks, but you, the developer, should be aware of the hazards that come with this low-level control.
Rust is a systems programming language that is designed to be safe, and link-time tools may bend or break some safety guarantees.
§Rust’s model vs ctor/dtor
Rust’s usual model is that nothing runs before or after main. The ctor and
dtor crates deliberately subvert that
(UCG #598).
Code inside #[ctor] and #[dtor] should favor libc-level APIs and logic
that does not depend on the Rust standard library having finished
initializing (constructors) or still being valid (destructors).
UCG #598 — Rust unsafe-code-guidelines; discussion thread on lifecycle and unsafe-code implications around execution outside main.
§Panic handling
Panic handlers may not be set up in early code, so a panic!() may not be
catchable, or may even result in undefined behavior. Generally, code that runs
before main must take great pains not to panic (🦀
#97049, 🦀
#107381, 🦀
#86030).
References
🦀 #97049 — rust-lang/rust; Miri and discussion of panicking inside #[start] before the runtime can catch unwinding.
🦀 #107381 — rust-lang/rust; unwinding and whether escaping lang_start is undefined behavior.
🦀 #86030 — rust-lang/rust; lang_start soundness when panic payloads panic during drop (failed to initiate panic).
§I/O and the Standard Library
std::io is known to be problematic after main
(🦀 #29488,
SO #35980148).
println! uses thread-local stdout machinery which may not be initialized yet, or may be
closed/broken after main exits. The
libc_print crate provides a safe
alternative for this that uses libc functions directly and will not panic if
used before or after main.
The standard library does not make any particular guarantees about the state of
the system after main exits or before it starts and code that works in one
version of Rust may or may not work in another.
References
🦀 #29488 — rust-lang/rust; println! panics from Drop / TLS (cannot access stdout during shutdown).
SO #35980148 — println! / stdout access from a libc atexit handler vs Rust runtime teardown order.
§Aggressive Linker Garbage Collection
Some linker configurations can strip the underlying registration data for
#[ctor], #[dtor] and #[in_section] registrations from the final binary,
resulting in them not being called
(rust-ctor #280,
🦀 #99721).
Building with --cfg linktime_used_linker for the ctor and dtor crates
may help — it applies used(linker) to the linker-generated items, but it
requires nightly Rust and #![feature(used_with_arg)] on the crate root.
Often a use of the module that contains the missing registration is enough
for the linker to retain the code.
References
rust-ctor #280 — mmastrac/linktime; linker / LTO stripping ctor registrations.
🦀 #99721 — rust-lang/rust; rustc / linkage behavior relevant to similar stripping (used, linker GC).
§cdylib lifecycle
A cdylib is a dynamic library that is loaded (and potentially unloaded) at
runtime, independent of the main executable.
On some platforms, unloading a shared library may not occur when you expect; behavior can be deferred until process exit. The rules are described as arcane. Thread-local storage on macOS is called out as influencing this; see this comment on 🦀 #28794.
Care should be taken to ensure that the #[dtor] functions are called before
the library is unloaded. While the #[dtor] macro supports registering
“termination” functions - which are called when the main binary process
terminates - inside of cdylibs, it is not recommended to use them as the code
that will perform the cleanup may have been unloaded and unmapped from memory,
causing random crashes.
References
🦀 #28794 (comment) — rust-lang/rust; thread-local storage and dynamic-library unload behavior on macOS.