Destructors, Linkage, and ABI
Purpose and Scope
This page connects three Reference-facing topics that often appear together when Rust code meets runtime and platform boundaries: destructors, linkage, and ABI. A destructor is the cleanup work run for an initialized value when it is dropped. Linkage is the compiler and linker-facing mechanism that determines how generated artifacts and symbols are connected. An ABI, or application binary interface, is the calling and representation contract used when generated code calls a function or exposes a symbol to other code. The supplied sources show these ideas at two layers: standard-library runtime support for thread-local destructors, and compiler lang-item plumbing for language concepts that code generation and runtime support rely on.
Sources: library/std/src/sys/thread_local/destructors/linux_like.rs, library/std/src/sys/thread_local/destructors/list.rs, compiler/rustc_hir/src/lang_items.rs, compiler/rustc_hir/src/weak_lang_items.rs, compiler/rustc_middle/src/middle/lang_items.rs, compiler/rustc_passes/src/lang_items.rs
The Rust Reference explains destructor behavior from the language user's perspective: initialized variables and temporaries are dropped when they go out of scope, assignment drops the initialized left-hand side, and partially initialized values only drop initialized fields. That rule is intentionally higher level than the runtime mechanism. The standard library source gives a concrete runtime example where cleanup is not just ordinary stack unwinding: thread-local values need callbacks registered so their destructors can run at thread exit. That implementation must cooperate with platform ABI details and weak dynamic symbols, so it is a useful bridge between language semantics and system linkage.
Sources: library/std/src/sys/thread_local/destructors/linux_like.rs, library/std/src/sys/thread_local/destructors/list.rs
The Linkage section of the Reference notes that linkage is described more in compiler terms than pure language terms. The sources here reinforce that framing. The compiler must know which items represent intrinsic language operations, runtime entry points, panic hooks, exception-handling personalities, callable traits, and memory intrinsics. The lang-item tables and collection pass provide that mapping. The result is not a general symbol table for all code; it is a curated set of definitions that the compiler treats specially because core language semantics or backend lowering need them.
Sources: compiler/rustc_hir/src/lang_items.rs, compiler/rustc_hir/src/weak_lang_items.rs, compiler/rustc_middle/src/middle/lang_items.rs, compiler/rustc_passes/src/lang_items.rs
Relevant Source Files
library/std/src/sys/thread_local/destructors/linux_like.rsimplements Linux-like thread-local destructor registration by weakly linking to__cxa_thread_atexit_implwhen available and falling back to the list-based implementation otherwise.library/std/src/sys/thread_local/destructors/list.rsimplements the fallback destructor registry using a thread-local vector of pointer and destructor-function pairs, enables a guard, and runs registered destructors on thread exit.compiler/rustc_hir/src/lang_items.rsdefines theLanguageItemscollection and theLangItemenum table that names compiler-recognized language concepts such as traits, operators, and compiler-called functions.compiler/rustc_hir/src/weak_lang_items.rsidentifies weak lang items and maps selected items to link names such asrust_begin_unwindandrust_eh_personality, plus weak-only memory operation items.compiler/rustc_middle/src/middle/lang_items.rsexposes typed compiler queries and helpers such asrequire_lang_item,is_lang_item, callable-trait classification, andrust-callABI-related closure trait mapping.compiler/rustc_passes/src/lang_items.rscollects#[lang = ...]attributes during compiler passes, checks targets, handles weak collection policy, and reports duplicate or invalid lang-item definitions.
Reference Concepts
For destructors, the user-visible contract is about when cleanup happens and which fields are recursively dropped. The runtime-facing contract is about arranging for cleanup code to be called at the right time even when the value is stored outside an ordinary stack frame. Thread-local storage is a good example because the variable can outlive normal lexical scopes inside a thread, but it still needs to be destroyed before the thread fully exits. The standard library represents each destructor callback as an unsafe extern "C" fn(*mut u8) paired with an opaque pointer, which makes the runtime mechanism small, ABI-explicit, and independent of the original Rust type.
Sources: library/std/src/sys/thread_local/destructors/linux_like.rs, library/std/src/sys/thread_local/destructors/list.rs
For linkage, the interesting point in these sources is not the crate-type matrix itself, but the way Rust uses symbol availability and symbol names to connect language runtime obligations to platform facilities. On Linux-like systems, __cxa_thread_atexit_impl is declared as an external weak static containing an optional function pointer. If the symbol is present, the standard library calls it. If it is absent, the implementation remains valid by registering the destructor in Rust's fallback list. That is exactly the kind of conditional boundary where Reference-level linkage concepts become concrete code.
Sources: library/std/src/sys/thread_local/destructors/linux_like.rs
For ABI, the code shows two distinct forms. The thread-local destructor path uses extern "C" because it must call into a C or C++ runtime hook and pass a C-compatible callback. The compiler lang-item helper path mentions the rust-call ABI for built-in callable traits: Fn, FnMut, FnOnce, and their async variants model inputs as tupled at the type level. These are different contracts. extern "C" is about interoperating with external runtimes, while rust-call is an internal Rust ABI convention for callable trait lowering and type modeling.
Sources: library/std/src/sys/thread_local/destructors/linux_like.rs, compiler/rustc_middle/src/middle/lang_items.rs
System-to-Code Mapping
| Topic | Source-level mechanism | Reader-facing meaning |
|---|---|---|
| Thread-local destructor registration | register(t, dtor) in linux_like.rs and list.rs | Runtime support records cleanup callbacks that must run at thread exit. |
| Weak platform linkage | #[linkage = "extern_weak"] declarations for __dso_handle and __cxa_thread_atexit_impl | The standard library can use a newer platform symbol when it exists without requiring it everywhere. |
| Fallback cleanup list | DTORS: RefCell<Vec<...>>, guard::enable(), and run() | A Rust-managed list preserves destructor execution when the platform hook is unavailable. |
| Lang items | LanguageItems, LangItem, and #[lang = ...] collection | The compiler binds intrinsic language concepts to definitions in crates. |
| Weak lang items | WEAK_LANG_ITEMS, link_name, and is_weak | Some runtime symbols may be optional or linked by name rather than treated as ordinary required definitions. |
| Callable ABI classification | is_callable_trait and closure-kind helpers | The compiler recognizes callable traits and their rust-call ABI modeling. |
The destructor implementations are intentionally narrow. They do not try to express drop order for all Rust values; that belongs to language lowering and runtime execution elsewhere. Instead, they solve one platform problem: when a thread-local value needs a destructor, remember a pointer and a callback, then arrange for that callback to execute at thread exit. On Linux-like systems the preferred route is the C++ ABI function __cxa_thread_atexit_impl. Because Rust supports older glibc configurations, the symbol is weakly linked and the code branches on whether the optional function pointer is present.
Sources: library/std/src/sys/thread_local/destructors/linux_like.rs
The fallback list implementation shows the safety shape of this runtime path. register borrows the thread-local destructor vector mutably, aborts if the borrow fails because the System allocator may not use TLS with destructors, enables the platform guard, and pushes the pointer-function pair. run is documented as safe only at thread exit, when there are no live references to TLS variables while they are destroyed. It repeatedly pops a destructor entry, releases the vector borrow, calls the destructor, and finally replaces the vector with a fresh allocation so list memory is released.
Sources: library/std/src/sys/thread_local/destructors/list.rs
The lang-item sources map compiler-known concepts to definitions. LanguageItems stores an array indexed by LangItem, a reverse map from DefId to LangItem, and a list of missing items. The set method enforces a bijection: one definition ID may not be assigned to two distinct lang items. That matters for codegen and type checking because compiler code wants to ask precise questions such as whether a definition is the FnOnce trait or whether a required runtime function exists, without re-parsing attributes throughout the compiler.
Sources: compiler/rustc_hir/src/lang_items.rs
Collection happens in a compiler pass that looks for #[lang = ...] attributes on AST items. LanguageItemCollector::check_for_lang extracts the attribute, resolves it to a known LangItem, verifies the target kind, and either collects it immediately or defers weak handling according to policy. The same file also tracks spans and diagnostics needed for duplicate definitions and incorrect targets. This is the enforcement side of the system: lang items are not just names in a table, but validated compiler inputs with crate and dependency context.
Sources: compiler/rustc_passes/src/lang_items.rs
API Components and Behavior
The most concrete public-looking runtime entry point in the supplied standard-library code is pub unsafe fn register(t: *mut u8, dtor: unsafe extern "C" fn(*mut u8)). Both implementations accept the same shape: an opaque pointer to the thread-local storage payload and an unsafe C-ABI callback that knows how to destroy that payload. The function is unsafe because the caller must supply a valid pointer, a matching destructor, and a thread-local lifecycle compatible with later execution. The Linux-like implementation may transmute the destructor callback to a libc::c_void callback before handing it to the platform hook.
Sources: library/std/src/sys/thread_local/destructors/linux_like.rs, library/std/src/sys/thread_local/destructors/list.rs
// Conceptual shape from the standard-library TLS destructor code:
pub unsafe fn register(t: *mut u8, dtor: unsafe extern "C" fn(*mut u8));The fallback runtime entry point pub unsafe fn run() is narrower than register: it is for the guard mechanism to call at thread exit. Its documentation states the key safety condition directly: it may only run on thread exit so there are no live references to TLS variables while they are destroyed. The loop also permits destructors to interact with the list indirectly, because it drops the borrow before invoking each callback. That design avoids holding a mutable borrow of the registry while user cleanup code is executing.
Sources: library/std/src/sys/thread_local/destructors/list.rs
The compiler-facing lang-item API is centered on TyCtxt. require_lang_item returns the DefId for a required item or fatally aborts compilation with a diagnostic. is_lang_item and as_lang_item let later compiler stages classify definitions. The callable helpers convert between lang items and ty::ClosureKind, including both sync and async callable traits. These helpers are small, but they encode important ABI knowledge: built-in callable traits use the rust-call ABI, and their inputs are tupled at the type level.
Sources: compiler/rustc_middle/src/middle/lang_items.rs
Weak lang items are the compiler-side analogue of optional runtime linkage. The weak_lang_items! macro defines a list containing PanicImpl and EhPersonality, adds is_weak, and maps those lang items to link names rust_begin_unwind and rust_eh_personality. The weak_only_lang_items! list contains memory operations such as MemCpy, MemMove, MemSet, MemCmp, Bcmp, and StrLen. This distinction lets compiler passes treat some intrinsic runtime hooks differently from ordinary mandatory lang items.
Sources: compiler/rustc_hir/src/weak_lang_items.rs
Execution Flow
A thread-local destructor registration on a Linux-like target starts with a value-specific pointer and destructor callback. The implementation declares two weak external symbols: __dso_handle and __cxa_thread_atexit_impl. If the platform supplies the function, Rust passes the destructor callback, the object pointer, and the DSO handle to that function. If the function pointer is absent, Rust records the callback in its own list. In both cases, the caller sees the same operation: schedule this destructor so the thread-exit path can eventually run it.
Sources: library/std/src/sys/thread_local/destructors/linux_like.rs, library/std/src/sys/thread_local/destructors/list.rs
A lang-item collection flow starts earlier in compilation. The pass constructs a LanguageItemCollector, walks relevant AST items, extracts #[lang = ...], and resolves the symbol to a known LangItem. The collector rejects mismatched targets, avoids collecting weak items when the current policy says to ignore them, and records valid items into LanguageItems. Later compiler queries use TyCtxt::lang_items() through helper methods instead of re-running collection. This separation keeps discovery, validation, and later semantic use in different phases of the compiler pipeline.
Sources: compiler/rustc_passes/src/lang_items.rs, compiler/rustc_hir/src/lang_items.rs, compiler/rustc_middle/src/middle/lang_items.rs
Practical Reading Guidance
When reading Reference material about destructors, keep the language rule and the runtime hook separate. Drop order, partial initialization, and Drop::drop describe what Rust programs are promised. Files such as linux_like.rs and list.rs show how one category of values, thread-local storage, gets connected to an actual thread-exit callback mechanism. That distinction helps avoid overgeneralizing from the TLS implementation to all destructors: most values do not pass through this registry, but TLS needs this machinery because its lifetime is tied to a thread rather than a lexical scope.
Sources: library/std/src/sys/thread_local/destructors/linux_like.rs, library/std/src/sys/thread_local/destructors/list.rs
When reading Reference material about linkage and ABI, use the lang-item files as a map of compiler special cases rather than as a complete linker manual. rustc_hir defines the names and storage model, rustc_passes collects and validates definitions, and rustc_middle exposes semantic queries that later phases can rely on. For adjacent topics, continue with the language reference overview for syntax and semantic context, the rustc overview for the driver and compilation pipeline, and the codegen backends page for how lowered programs eventually become target-specific artifacts.
Sources: compiler/rustc_hir/src/lang_items.rs, compiler/rustc_passes/src/lang_items.rs, compiler/rustc_middle/src/middle/lang_items.rs