Open-source project
mmastrac/linktime avatar
mmastrac/linktime

linktime: constructors and linker-managed collections for Rust

Link-time initialization, destruction, scattered data collection macros for Rust

1,094 stars82 forksRustApache-2.0

At a glance

What is it?
A Rust workspace that gives you the things Rust does not have, namely functions that run before main and after main, link sections that collect items from across a binary, and zero-allocation collections gathered by the linker. The crates are published and versioned independently, and the facade crate enables `ctor`, `dtor` and `link-section` by default.
Who is it for?
linktime suits a Rust project that needs initialization code to run before main, or that wants to register items from separate modules into one collection without a runtime registry, and that is willing to depend on the linker and the platform's constructor mechanism to do it.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 41 days ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The gap Rust leaves and this workspace fills

linktime is a set of cross-platform libraries for link-time initialization, finalization and collection in Rust.

The problem it addresses is one Rust deliberately does not solve. There is no defined order of static initialization in Rust, and there is no built-in way to run code before `main` or after it. C and C++ have `__attribute__((constructor))` and `__attribute__((destructor))`, and the README names both as the models the crates imitate.

The workspace is split into crates that are published separately, and the README's table lists them with the role of each.

The `ctor` crate provides module initialization functions for Rust, described as running code before main to initialize data, external resources or other state. The `dtor` crate is the mirror, for running code after main to clean up resources or perform other final operations. The `link-section` crate gives linker-managed typed and untyped sections, for collecting related items from an entire linked binary into a single section. The `scattered-collect` crate is for linker-managed collections, namely slices, sorted slices and maps.

And the `linktime` crate is the convenience facade for `ctor`, `dtor` and `link-section`.

The choice the README offers is pick-and-choose or import the top-level crate to get all three, and every feature on the facade is already enabled by default. That default is worth noting: taking `linktime` means taking the constructor machinery, the destructor machinery and the link sections, whether or not you use all three.

The project is by Matt Mastracci, dual licensed under the Apache License 2.0 and the MIT License, with `LICENSE-APACHE` and `LICENSE-MIT` at the repository root, and the workspace metadata sets the licence expression to `Apache-2.0 OR MIT`. The last push was on 2026-09-12.

Before main, with an unsafe attribute

The `ctor` crate is the simplest piece, and the example is short enough to read as a specification.

toml
[dependencies]
linktime = { version = "...", features = ["ctor"] }  # note: already enabled by default
# or
ctor = "..."
rust
use linktime::ctor; // or ctor::ctor
use libc_print::*;

#[ctor(unsafe)]
fn foo() {
    libc_println!("Life before main!");
}

The `unsafe` inside the attribute is the part to understand. It is not decorative. The attribute expands into code that registers a function to be run by the platform's constructor mechanism, and that registration writes to a linker-managed section, so the macro is asking the author to acknowledge that the expansion is doing something unsafe. A plain function annotated this way is not a safe function any more, in the sense that the compiler will refuse to let it be called normally, and the unsafe marker is the syntax that says so.

The `use libc_print::*` in the example is a hint about the environment. The example prints from a constructor, which is before the Rust runtime has initialised the standard library's own machinery, so it uses a crate that can print without the standard library's global state being ready.

That is the real constraint behind the whole crate. Code that runs before main runs before the Rust runtime is fully up, so anything it touches has to be safe to touch at that point. A constructor that acquires a mutex from a lazily initialised static, for example, is asking for trouble, and the crate cannot prevent that.

After main, and the dtor mirror

The `dtor` crate follows the same shape as `ctor`.

toml
[dependencies]
linktime = { version = "...", features = ["dtor"] }  # note: already enabled by default
# or
dtor = "..."
rust
use linktime::dtor; // or dtor::dtor
use libc_print::*;

#[dtor(unsafe)]
fn foo() {
    libc_println!("Life after main!");
}

So the API surface is two functions with two attributes and a dependency line, and the interesting question is not how to use it but when you should.

The `dtor` mechanism runs code after main returns, which is different from running it as part of a normal shutdown path. In Rust, the idiomatic way to clean up is to let the values drop, and a destructor attribute is for the cases where that is not possible, for example a resource that has to be released before the process image is unmapped, or a file that has to be flushed at a specific point rather than when the owning value happens to go out of scope.

The same runtime caveat applies as for constructors, in reverse. Code that runs after main may run after the standard library has already torn down some of its global state, so the same caution applies about what it touches.

The `ctor` and `dtor` crates are versioned independently of the rest of the workspace. The workspace metadata pins `ctor` at version 1.0.13 and `dtor` at version 1.0.6, both on a 1.x line, while `link-section` is at 0.19.3 and `scattered-collect` at 0.22.2. That difference in version lines tells you which parts of the project are settled and which are still moving.

Link sections: collecting from across a binary

The `link-section` crate is the part with no equivalent in the standard library, and it is the most interesting of the four.

toml
[dependencies]
linktime = { version = "...", features = ["link-section"] }  # note: already enabled by default
# or
link-section = "..."
rust
use linktime::link_section::{section, in_section, TypedSection};
use linktime::ctor;
use libc_print::*;

#[section(typed)]
static FOO: TypedSection<fn()>;

#[in_section(FOO)]
fn foo() {
    libc_println!("Hello, world!");
}

#[ctor(unsafe)]
fn print_numbers() {
    for f in FOO {
        f();
    }
}

The mechanism is the linker's. A static is declared as a section, and items elsewhere in the program, possibly in other crates, are marked as belonging to it. The linker then gathers them into one place, and the section is iterated at run time.

The type parameter is the interesting part of the API. `TypedSection<fn()>` says what kind of thing the section holds, and the iteration in the example calls each entry. A typed section means the compiler can check that the items placed in it match the declared type, which is a real safety property: an untyped section is a `void*` array and a type-confused entry is a crash at run time.

The three crates compose into one pattern. The `in_section` items are functions, and the `ctor` runs a function that iterates the section and calls them. That is a registration and dispatch system with no runtime registry, no allocation and no `HashMap` of function pointers, and the items can live in entirely separate compilation units or even separate crates, since the linker resolves them.

The trade is that the collection is fixed at link time. There is no way to add to it at run time, and the order of the items is the linker's order rather than an order you choose.

Scattered collections, gathered by the linker with no allocation

The `scattered-collect` crate is the most general of the four. It is described as a crate for defining zero-allocation, linker-managed scattered collections in Rust, and the README lists six variants.

`ScatteredIterable` is a collection of items that are available only via iteration. `ScatteredSlice` is a collection of sized items collected into a slice in an arbitrary order. `ScatteredSortedSlice` is a collection of items available via slice in sorted order. `ScatteredReferencedSlice` is a collection collected into a slice in link order. `ScatteredSortedReferencedSlice` is a collection of sized items available both via sorted slice and via reference at the declaration site. And `ScatteredMap` is a collection of key-value pairs available via slice and indexed by key.

The example shows the two halves of the API:

```rust,ignore use scattered_collect::{gather, scatter, slice::ScatteredSlice};

#[gather] static SLICE_PLUGINS: ScatteredSlice<&'static str>;

#[scatter(SLICE_PLUGINS)] const _: &'static str = "json";

#[scatter(SLICE_PLUGINS)] const _: &'static str = "yaml";

fn main() { assert_eq!(SLICE_PLUGINS.len(), 2); assert!(SLICE_PLUGINS.contains(&"json")); } ```

A `gather` declares the collection and a `scatter` places an item in it. The item is an anonymous constant, so it has no name and no allocation: it is a string literal in the data section, and the linker gathers the references into the slice.

This is the plugin-registration pattern without the runtime cost. The usual way to build a registry of plugins in Rust is a `HashMap` built at run time, or a `static` of function pointers, and both require the registration code to run and the items to be named. Here, the registry is a slice the linker fills, the items are unnamed constants, and the collection is a `static` with a fixed length known after linking.

The cost is equally clear. The contents are decided at link time, so a plugin cannot be added at run time, and the sorted variants imply an ordering the linker has to produce.

UEFI, and the parts of the workspace you do not see

The UEFI section is short and it is the one place where the convenience breaks down.

UEFI is fully supported, both with `std` and with `#![no_std]`, but the constructors and destructors must be run manually via `run_constructors` and `run_destructors` respectively.

```rust,ignore #[export_name = "efi_main"] extern "efiapi" fn efi_main(_handle: *mut c_void, _st: *mut c_void) -> usize { unsafe { linktime::run_constructors() };

// ... application ...

unsafe { linktime::run_destructors() }; 0 } ```

The reason is structural. On a hosted platform the platform's startup code runs the constructor sections for you, and the runtime tears them down on the way out. A UEFI application has no such runtime, so the application entry point has to call them itself, and it has to call them in the right places.

The repository layout shows how the workspace is built, and there are two directories that the crate table does not mention: `macro-magic` and `linktime-proc-macro`. The proc macro crate is the obvious one, since `#[ctor]`, `#[dtor]`, `#[section]`, `#[gather]` and `#[scatter]` are all procedural attributes. `macro-magic` is a separate workspace member that the metadata does not describe, and the workspace lists it in both the members and the default-members, so it is a first-class part of the build even though the README does not explain it.

There is also a `tests/` crate, listed as a workspace member, and a `tools/` directory, a `docs/` directory, a `.cargo/` directory for cargo configuration, a `CHANGELOG.md` and a `Cargo.lock`.

The release history in the repository metadata shows the per-crate lines in action: `scattered-collect-0.22.1` from 2026-08-14, `scattered-collect-0.22.0` from 2026-08-09 and `link-section-0.19.3` from 2026-08-09, with the latter published in the same minute as the former, which is what a coordinated multi-crate release looks like.

Where linktime is the wrong choice

The first wrong case is a project that needs a defined global initialization order. These crates do not provide one. The constructor functions run before main, but in what order relative to each other is decided by the platform and the linker, not by you. If your design has two subsystems that must initialise in a specific sequence, a constructor is the wrong tool, because it makes the order an implementation detail you cannot see.

The second is a project that has to work identically everywhere with no platform-specific code. The mechanism underneath is the platform's, so on a hosted target the constructors are run by the C runtime equivalent and on UEFI you have to run them yourself. The README documents the UEFI case, and the difference is real work.

The third is a codebase that has decided runtime registration is acceptable. A `static` of function pointers with an atomic counter, or a `OnceLock` around a `Vec`, is a few lines of Rust that does the same job with no macro, no `unsafe` attribute and no dependency, and it works on every platform including UEFI without a manual run step.

The fourth is a project that needs to add entries at run time. Linker-managed collections are fixed when the binary is linked, and so are the section contents. A plugin system that loads a shared object at run time cannot be built on it.

The fifth is a team that wants a small dependency tree. The facade crate enables every feature by default, and the `ctor` and `dtor` attributes carry an `unsafe` marker, so adopting the crate is adopting a code generation model that your reviewers have to understand.

What the workspace gives you, when the problem is the right one, is the thing Rust will not give you for free: code that runs before main, code that runs after it, and collections whose contents the linker decides, all without a runtime registry.

Editorial conclusion

linktime suits a Rust project that needs initialization code to run before main, or that wants to register items from separate modules into one collection without a runtime registry, and that is willing to depend on the linker and the platform's constructor mechanism to do it. It does not suit a project that needs portable behaviour across operating systems without thinking about the platform, since the constructor and destructor mechanism is platform-specific underneath, and it does not suit a codebase that treats the global initialization order as a solved problem, because these crates exist precisely because Rust does not define one. Before adopting it, read the UEFI section, where the constructors have to be run by hand, and decide whether you want the facade crate with its default features or the individual crates, since each has its own version and its own release cadence.

Frequently asked questions

What are the crates in the linktime workspace?

The `ctor` crate for functions running before main, `dtor` for functions running after it, `link-section` for linker-managed typed and untyped sections, `scattered-collect` for zero-allocation linker-managed collections, and the `linktime` facade crate that aggregates `ctor`, `dtor` and `link-section`.

Why do the ctor and dtor attributes take unsafe?

The attribute expands into code that registers the function with the platform's constructor mechanism, which writes to a linker-managed section, so the author has to acknowledge that the expansion is unsafe. A function marked this way is not a callable safe function.

How do I use linktime on UEFI?

UEFI is fully supported with both `std` and `#![no_std]`, but the constructors and destructors must be run by hand with `run_constructors` and `run_destructors` inside the `efi_main` entry point, because there is no platform startup code to do it for you.

What is the difference between a ScatteredSlice and a ScatteredSortedSlice?

A ScatteredSlice holds sized items collected into a slice in an arbitrary order, while a ScatteredSortedSlice holds items available via slice in sorted order. The workspace also offers a ReferencedSlice variant collected in link order and a Map variant indexed by key.

What licence is linktime released under?

Dual licensed under the Apache License, Version 2.0 and the MIT License, with `LICENSE-APACHE` and `LICENSE-MIT` at the repository root and the workspace metadata setting `Apache-2.0 OR MIT`.

Official sources

  1. Issues
  2. License: Apache-2.0
  3. mmastrac/linktime on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/mmastrac-linktime.svg)](https://hysenlabs.com/projects/mmastrac-linktime)

Was this useful?