rust-dix-macros provides procedural macros for the rust-dix DI
framework. These macros shift work from runtime to compile time, eliminating
boilerplate and letting you declare DI configuration as close to your types
as possible.
[]
= "0.8" # re-exports all macros — you usually don't need rust-dix-macros directly
Note: You rarely need to add
rust-dix-macrosas a direct dependency. Therust-dixcrate re-exports all macros (Inject,module,inject,register). Justuse rust_dix::*and you're set.
Macros
#[rust_dix::inject] — Attribute-based auto-registration (recommended)
What it does: Combines constructor generation and service registration in
one attribute. The attribute can be placed on either a struct or a trait
impl block, with a single optional lifetime argument that defaults to
singleton.
ServiceCollection::from_injected() collects all registrations at startup.
Placement 1 — on a struct (registers as concrete type):
use *;
// singleton by default
;
Placement 2 — on a trait impl (registers as dyn Trait, auto-detected):
// on the struct first
// then on the impl — registers as `dyn UserRepo`
The trait type is read from impl Trait for Type, so there is no need to
spell out as = dyn Trait — the macro infers it. The impl-block form reuses
the constructor generated by the struct-level annotation (or by an explicit
#[derive(Inject)]), so you can register the same type under as many trait
interfaces as you like:
;
Supported syntax:
| Attribute | Placement | Registers as |
|---|---|---|
#[rust_dix::inject] |
struct | Concrete type, Singleton (default) |
#[rust_dix::inject(singleton)] |
struct | Concrete type, Singleton |
#[rust_dix::inject(scoped)] |
struct | Concrete type, Scoped |
#[rust_dix::inject(transient)] |
struct | Concrete type, Transient |
#[rust_dix::inject] |
impl Trait for T |
dyn Trait, Singleton (default) |
#[rust_dix::inject(scoped)] |
impl Trait for T |
dyn Trait, Scoped |
#[rust_dix::inject(transient)] |
impl Trait for T |
dyn Trait, Transient |
Supports both named-field structs and unit structs (zero fields):
; // unit struct — works!
This macro subsumes #[derive(Inject)] — when placed on a struct, it
internally generates the same constructor function
(__rdi_construct_{Type}) using the same field attribute syntax
(#[inject], #[inject(owned)], #[inject(key = "...")], etc.). You do
NOT need both. The impl-block placement does not generate a constructor —
it reuses the one already produced by the struct placement.
Migration note: The old verbose syntax
#[rust_dix::inject_attr(singleton, as = dyn Trait<...>)]has been removed. Use the impl-block placement instead.
#[derive(Inject)] — Auto-generated constructor
What it does: Reads the struct fields and their #[inject(...)] attributes,
then generates a factory function __rdi_construct_{TypeName}() that resolves
each marked field from the container. Unmarked fields use Default::default().
The generated function is
#[doc(hidden)]internal surface: the macros use it to wire#[rust_dix::inject]/from_injected()auto-registration. Call it directly only when registering a custom factory closure — prefer#[rust_dix::inject]+from_injected()in application code.
Note: This derive macro is subsumed by
#[rust_dix::inject]on a struct. You only need#[derive(Inject)]if you want to generate the constructor without automatic registration (e.g., to register manually with a custom factory closure).
Without it, you hand-write the factory closure and resolve every dependency
yourself. Note that the closure receives &dyn IServiceResolver, where the
generic get::<T>() is not callable (Self: Sized); manual resolution needs
get_any + downcast, or capturing already-resolved Arcs in the closure.
With it, the macro generates the exact same code:
// Generates: __rdi_construct_MyService(resolver) -> Arc<MyService>
The generated function can be used anywhere you'd write a factory closure:
let provider = new
.singleton
.build.unwrap;
Field attributes (#[inject(...)]):
Explicit injection: only marked fields consult the resolver. Unmarked fields
use Default::default() and are treated as internal state. Optionality is
driven by the field type (Option<...>), not by a marker.
| Attribute | Field type | Behavior |
|---|---|---|
| (none) | any (Default) |
Internal field — uses Default::default() |
#[inject] |
Arc<T> |
Required shared — Err(ServiceNotFound) (+ field context via try-construct) |
#[inject] |
Option<Arc<T>> |
Optional shared — None if T not registered |
#[inject(key = "k")] |
Arc<T> |
Keyed shared — Err(KeyedServiceNotFound) if key missing |
#[inject(key = "k")] |
Option<Arc<T>> |
Optional keyed shared |
#[inject(owned)] |
bare T |
Required owned — Err if not registered or Singleton |
#[inject(owned)] |
Option<T> |
Optional owned — None if not registered or Singleton |
#[inject(owned, key = "k")] |
bare T / Option<T> |
Keyed owned |
#[inject] |
Vec<Arc<T>> |
Polymorphic — collects all non-keyed registrations in registration order (keyed implementations need get_all_keyed on the provider); no key attribute |
#[inject(provider)] |
Arc<ServiceProvider> |
Injects the provider for on-demand resolution |
Type checking is strict:
#[inject]acceptsArc<T>,Option<Arc<T>>, orVec<Arc<T>>,#[inject(owned)]only accepts bareT/Option<T>. Mismatched markers produce a compile-time error.
Built-in container services need no registration and also resolve in eagerly
built singletons (0.7.1+): #[inject] scope_factory: Arc<dyn ScopeFactory>,
#[inject] provider: Arc<ServiceProvider>, and the dedicated
#[inject(provider)]. The built-in ScopeFactory is weak-backed and cached as a
singleton, so holding it in a singleton does not create an Arc cycle; the
container must outlive the services created from it. Prefer ScopeFactory over
provider in singletons (holding Arc<ServiceProvider> creates a strong cycle),
and resolve lazily in methods — resolving through the injected provider inside a
build-time constructor is rejected.
#[rust_dix::module] — Compile-time module scanning
What it does: Scans a mod block for rust_dix::register!() declarations at
compile time, then generates a __rdi_build_provider_{module_name}() function
that constructs a fully configured ServiceProvider.
Without it, you register services one by one in a central setup function:
With it, you declare registrations right inside the module:
// Generates: services::__rdi_build_provider_services() -> Result<Arc<ServiceProvider>>
let provider = __rdi_build_provider_services.unwrap;
Supported declarations:
| Syntax | Meaning |
|---|---|
register!(singleton: T) |
Register T as singleton using Default |
register!(scoped: T) |
Register T as scoped using Default |
register!(transient: T) |
Register T as transient using Default |
register!(singleton: dyn Trait => Impl) |
Register Impl as singleton implementing Trait |
register!(keyed "k": singleton: T) |
Register T as keyed singleton |
register!(keyed "k": scoped: T) |
Register T as keyed scoped |
register!(factory singleton: T => expr) |
Register with a custom factory expression |
When to use it:
- You want service registrations physically close to the module that owns them
- You are building a library or plugin and want to ship a pre-configured provider builder
- You prefer a declarative DSL over method chaining
rust_dix::register! — The declaration macro
This is the macro used inside #[rust_dix::module] blocks to declare service
registrations. It expands to nothing at the item level — its only effect is
being collected by the enclosing #[rust_dix::module] attribute.
You can also use register! standalone as a macro_export:
register!;
Migration note: The function-like macro was renamed from
inject!()toregister!()to avoid the proc-macro namespace conflict (Rust does not allow an attribute macro and a function-like macro to share the same name in the same crate). This freed up the nameinjectfor the attribute macro described above.
Behavior notes (0.8.0)
The macros themselves are unchanged in 0.8.0, but the container semantics they feed into are:
- Single-service resolution is "last registration wins" (MEDI parity). If the
same type is registered twice — e.g. an
#[rust_dix::inject]auto-registration plus a manualsingletonoverride — the last registration is whatget()returns. Usetry_add*when you want "only add if absent". Vec<Arc<T>>injection only collects non-keyed registrations (registration order). Keyed implementations are resolved explicitly withget_all_keyed::<T>().- Strict scope validation is the default: resolving a Scoped service from the
root provider returns
Err(RdiError::ScopedFromRootProvider). Annotate request-scoped services with#[inject(scoped)]and resolve them insideprovider.scope(). - Dispose is explicit and ordered:
Scope::dispose()/ Drop releases scoped instances in reverse creation order;ServiceProvider::dispose()releases singletons and root-scoped instances. Constructor-time resolution through an injected provider is rejected with a typedFactoryPanic.
Relationship with rust-dix
rust-dix-macros is a companion to rust-dix. You can use rust-dix
without any macros — just write factory closures manually — and everything
works. The macros exist to eliminate repetition when you have many services
or want to declare DI configuration declaratively.
| Approach | Pros | Cons |
|---|---|---|
#[rust_dix::inject] (recommended) |
Zero boilerplate, auto-registration, impl-block trait inference | Requires inventory; unmarked fields need Default |
| Manual factories | Full control, explicit | Boilerplate for many services |
#[derive(Inject)] |
Zero boilerplate for constructors | Manual registration still needed |
#[rust_dix::module] |
Declarative, centralized | Less flexible for dynamic registration |
All approaches can be mixed freely in the same project.
License
MIT.