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:
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.