Skip to content

Why backends register themselves at link time

There is no rtb_forge::init(). You never enumerate the backends, never call a register_github(), and never assemble a HashMap<&str, Factory> in your main. You ask for a source_type string and get a factory back:

let factory = rtb_forge::lookup("github").expect("github backend not compiled in");

That works because each backend contributes an entry to a linkme distributed slice, and the linker collects them.

What a distributed slice is

RELEASE_PROVIDERS is declared in release.rs as an empty slice. Each backend module has a small function annotated with #[distributed_slice(RELEASE_PROVIDERS)]. The linkme crate places each such function's pointer into a dedicated linker section; at link time the section is contiguous, and the slice is a view over it.

The result is a plugin registry with no runtime registration step and no initialisation ordering to get wrong.

Why this and not a plain registry

Three properties fall out of it, and they are the reason it was chosen:

Cargo features become the plugin manifest. A backend registers only if its module is compiled, and its module compiles only if its feature is on. So registered_types() tells you what this specific binary can do — which makes it the correct thing to print in an "unknown source_type" error message, rather than a hard-coded list that lies on trimmed builds.

Downstream tools extend the registry without a fork. A tool with an internal artefact server writes its own type implementing ProviderRegistration, annotates a fn() -> Box<dyn ProviderRegistration> with the same attribute, and its backend is discoverable through the same lookup call as the built-ins. ReleaseSourceConfig::Custom exists to carry its configuration. Nothing in rtb-forge needs to know it exists.

There is no ordering problem. A lazy_static registry populated by constructors has to answer "what if someone looks it up before it is populated". A linker section does not have that state.

What it costs

The slice element is a boxed trait object, not a struct. You would expect RELEASE_PROVIDERS to be [RegisteredProvider]. It is [fn() -> Box<dyn ProviderRegistration>]. The indirection exists because linkme emits #[link_section] attributes at each registration site, and from Rust 1.95 that attribute is attributed to the unsafe_code lint. A struct-valued distributed slice trips a crate-level #![forbid(unsafe_code)]; a slice of function pointers returning boxed trait objects does not. It also matches the convention the wider toolkit already uses for command registration.

One allocation per lookup, per entry. lookup and registered_types walk the slice and call each function, which boxes a registration. With six built-in backends that is six small allocations per call — irrelevant next to the HTTP round trip that follows, and worth avoiding in a loop.

unsafe_code is denied, not forbidden. The crate uses #![deny(unsafe_code)] rather than #![forbid(...)] precisely so the six backend modules can carry a targeted #![allow(unsafe_code)] for their registration sites. No hand-written unsafe block exists anywhere in the crate; the allowance covers a lint triggered by an attribute, not by real unsafe code. The [lints.rust] unsafe_code = "deny" entry in Cargo.toml is what keeps that guarantee visible to anyone auditing the manifest.

A missing backend is a runtime None, not a compile error. Trim default-features too aggressively and lookup("gitlab") returns None at the moment a user's config asks for GitLab. The type system will not catch it. This is the real trade for the flexibility, and the mitigation is to always render registered_types() in the error you produce:

let factory = rtb_forge::lookup(cfg.source_type()).ok_or_else(|| {
    format!(
        "no backend for source_type `{}`; this build has: {:?}",
        cfg.source_type(),
        rtb_forge::registered_types()
    )
})?;

Why the config enum is typed rather than a map

ReleaseSourceConfig could have been a source_type string plus a HashMap<String, String> for everything else. It is a tagged enum with a typed struct per backend instead.

The reason is where errors surface. A typed enum moves a misspelled owner: or a missing repo: to deserialise time, where the error names the field and the line. A string map defers it to the first network call, where it presents as a 404 against a URL with an empty path segment.

The Custom variant is the deliberate hole in that: a downstream backend cannot have its parameter struct known to this crate, so it gets the freeform BTreeMap<String, String> and validates at factory time instead. That is a worse experience, and it is confined to the case where nothing better is possible.