Expand description
rust-dix — Rust Dependency Injection Framework
A dependency injection container for Rust inspired by Microsoft.Extensions.DependencyInjection (MEDI).
§Quick start
use rust_dix::*;
use std::sync::Arc;
// Trait-based interface (recommended)
trait ILogger: Send + Sync {
fn log(&self, msg: &str);
}
struct ConsoleLogger;
impl ILogger for ConsoleLogger {
fn log(&self, msg: &str) { println!("[LOG] {}", msg); }
}
// Register as trait interface
let provider = ServiceCollection::new()
.singleton::<dyn ILogger>(|_| Arc::new(ConsoleLogger))
.build()
.unwrap();
// Resolve as trait
let logger: Arc<dyn ILogger> = provider.get().unwrap();
logger.log("Hello");§Keyed services: typed keys and AnyKey fallbacks
Keys can be any Any + Send + Sync + Eq + Hash + Debug value
(ServiceKey::new); string keys ("fast") remain supported and are stored
as String keys. Key equality includes the value type, so
ServiceKey::new(1u32) is not equal to "1"; use
ServiceKey::from("name") / "name".into() for string-keyed registrations.
The injection macros accept any expression convertible to
ServiceKey — string literals keep working:
#[inject(key = "fast")], #[inject(key = ServiceKey::new(Kind::Fast))], or a
downstream key value implementing From<LocalKey> for ServiceKey (the
official extension path for frameworks; do not duplicate the injection
macros). See the repository reference chapter for details:
https://github.com/Rust-Framework/rust-dix/blob/main/docs/rust-dix/09-reference/extension-points.md.
A registration with KeyedService::any_key (or
ServiceKey::any_key) acts as a fallback for keys that have no exact
registration. Singleton/scoped fallback instances are cached per
(registration, requested key); get_all_keyed never enumerates them.
use rust_dix::*;
use std::sync::Arc;
#[derive(Debug, PartialEq, Eq, Hash)]
enum Kind { Fast, Safe }
trait Strategy: Send + Sync { fn name(&self) -> &'static str; }
struct Fast;
impl Strategy for Fast { fn name(&self) -> &'static str { "fast" } }
struct Fallback;
impl Strategy for Fallback { fn name(&self) -> &'static str { "fallback" } }
let provider = ServiceCollection::new()
.keyed_singleton::<dyn Strategy>(ServiceKey::new(Kind::Fast), |_| Arc::new(Fast))
.keyed_any_singleton::<dyn Strategy>(|_, _| Arc::new(Fallback))
.build()
.unwrap();
let fast = provider.get_keyed::<dyn Strategy>(ServiceKey::new(Kind::Fast)).unwrap();
assert_eq!(fast.name(), "fast");
// No exact registration for "unknown": the AnyKey fallback is used.
let fallback = provider.get_keyed::<dyn Strategy>("unknown").unwrap();
assert_eq!(fallback.name(), "fallback");For typed-key constructor injection:
use rust_dix::*;
use std::sync::Arc;
#[derive(Debug, PartialEq, Eq, Hash)]
enum Channel { Primary }
struct Sink;
#[derive(Inject)]
struct Consumer {
#[inject(key = ServiceKey::new(Channel::Primary))]
sink: Arc<Sink>,
}
let provider = ServiceCollection::new()
.keyed_singleton(ServiceKey::new(Channel::Primary), |_| Arc::new(Sink))
.transient(|r| __rdi_construct_Consumer(r))
.build()
.unwrap();
let consumer: Arc<Consumer> = provider.get().unwrap();
let _ = &consumer.sink;§Build-time validation
rust-dix validates dependencies during build() to catch errors early:
// Circular dependency detection — factories resolve deps via `get_any`
// (the `get::<T>()` method requires `Self: Sized` and cannot be called
// on `&dyn IServiceResolver`).
let result = ServiceCollection::new()
.singleton(|r| {
let _b = r.get_any(std::any::type_name::<B>()); // A depends on B
Arc::new(A)
})
.singleton(|r| {
let _a = r.get_any(std::any::type_name::<A>()); // B depends on A → CIRCULAR!
Arc::new(B)
})
.build();
assert!(result.is_err());
if let Err(RdiError::CircularDependency(cycle)) = result {
println!("Circular dependency detected: {}", cycle);
}§Async services
For services that require async initialization (e.g., database
connections, remote config loading), use build_async() and the async_*
registration methods. Async singletons are initialized dependency-first
(registration order does not matter), so a factory may await another async
singleton registered later. build_async() returns Arc<ServiceProvider>.
Async resolution is chain-based: each get_async() future captures the
current resolution chain when constructed, so self/cross dependency cycles
are reported as RdiError::CircularDependency instead of dead-locking.
To detect cycles across detached tasks, propagate
AsyncFactoryContext (see ServiceProvider::async_context) into the
spawned task; without it, a fully detached task cannot inherit the chain.
use rust_dix::*;
use std::sync::Arc;
struct DbPool { url: String }
async fn connect() -> DbPool { DbPool { url: "sqlite::memory:".into() } }
let provider = futures::executor::block_on(
ServiceCollection::new()
.async_singleton(|_| Box::pin(async { Arc::new(connect().await) }))
.build_async(),
)
.unwrap();
let pool: Arc<DbPool> = futures::executor::block_on(provider.get_async()).unwrap();
assert_eq!(pool.url, "sqlite::memory:");§Features
- Three lifetimes: Singleton, Scoped, Transient
- Keyed services: multiple implementations by key
- string keys (
"fast") and typed keys (ServiceKey::new, e.g. enums) KeyedService::any_key/ServiceKey::any_keyfallback registrations (keyed_any_singleton/keyed_any_scoped/keyed_any_transient)- per-
(registration, requested key)caching for AnyKey fallbacks; exact registrations take precedence;get_all_keyedexcludes fallbacks
- string keys (
- Constructor injection:
#[derive(Inject)]on structs - Attribute-based auto-registration:
#[rust_dix::inject]on structs or trait impls (#[inject(key = <expr>)],#[inject(owned)],#[inject(owned, key = <expr>)]; key expressions accept string literals, typedServiceKeyvalues, or downstream key types viaFrom<LocalKey> for ServiceKey) - Compile-time module scanning:
#[rust_dix::module]collectsregister!()declarations - Cross-DLL support: named service registry for cdylib plugins
- Layered containers: child-first resolution via
ServiceProviderWrapper - Build-time validation: sync/async dependency checks (cycles, captive dependencies, sync singleton depending on async services) during build
- Async resolution with cycle detection:
ServiceProvider::get_async/ServiceProvider::get_keyed_async; propagateAsyncFactoryContextinto detached tasks so cross-task cycles also returnRdiError::CircularDependency - Scope validation: scoped services cannot be resolved from the root
provider by default (MEDI
ValidateScopesparity; opt out withServiceProviderOptions::lenient);get_owned/get_keyed_ownedare the documented exception - Deterministic lifecycle: eager singletons are created in registration
order (async singletons dependency-first);
ServiceProvider::dispose/Scope::disposerelease cached instances in reverse creation order - MEDI registration semantics: the last registration wins for a single
service;
get_allreturns unkeyed implementations in registration order (get_all_keyedfor keyed ones) - Typed diagnostics:
RdiErrorexposes dedicated variants such asSyncSingletonDependsOnAsyncServiceandAsyncServiceRequiresGetAsync, plusRdiError::is_circular(heuristic for factory-panic-wrapped cycles) - Engineering baseline: MSRV 1.95,
#![forbid(unsafe_code)], fmt/clippy/doc-D warnings+ tests + RustSeccargo auditin CI
§Diagnostics
Prefer matching the typed RdiError variants over strings:
use rust_dix::*;
use std::sync::Arc;
struct A;
struct B;
let result = ServiceCollection::new()
.singleton(|r| {
let _ = r.get_any(std::any::type_name::<B>());
Arc::new(A)
})
.singleton(|r| {
let _ = r.get_any(std::any::type_name::<A>());
Arc::new(B)
})
.build();
let err = result.err().expect("cycle must be rejected");
assert!(err.is_circular());
if let RdiError::CircularDependency(cycle) = err {
assert!(cycle.contains("->"));
}Sync resolution of an async service returns
RdiError::AsyncServiceRequiresGetAsync (use get_async / get_keyed_async);
registering async_* services and calling ServiceCollection::build
returns RdiError::AsyncServiceRequiresBuildAsync.
§Crate layout
| Module | Description |
|---|---|
ServiceCollection | Register services with a builder API |
ServiceProvider | The root DI container |
Scope / ServiceScope | Scoped container (one per scope) |
IServiceResolver | Resolution trait (implemented by ServiceProvider & Scope) |
ServiceProviderWrapper | Child-first layered container |
ServiceLifetime | Enum: Singleton, Scoped, Transient |
ServiceKey / KeyedService | Typed keys and the AnyKey sentinel |
AsyncFactoryContext | Propagate the async resolution chain into tasks |
ServiceProviderOptions | Scope-validation options (lenient()) |
IProvider | Unified trait for resolution + named registry |
RdiError | Typed errors (cycles, disposed, async diagnostics, …) |
§MSRV and security
- MSRV: Rust 1.95 (
rust-versioninCargo.toml, verified in CI). #![forbid(unsafe_code)]in bothrust-dixandrust-dix-macros.- No network/filesystem access; containers only manage in-memory services.
- Release/security policy: https://github.com/Rust-Framework/rust-dix/blob/main/SECURITY.md
§Proc-macros
#[derive(Inject)]— auto-generates constructor injection code#[rust_dix::inject]— auto-registration on structs or trait impls#[rust_dix::module]— compile-time module scanningrust_dix::register!(...)— declare services inside#[rust_dix::module]
Re-exports§
pub use collection::ServiceCollection;pub use provider::ServiceProvider;pub use key::KeyedService;pub use key::ServiceKey;pub use provider::AsyncFactoryContext;pub use provider::ServiceProviderOptions;pub use entry::ServiceDescriptor;pub use entry::ServiceLifetime;pub use entry::IServiceResolver;pub use scope::Scope;pub use scope::Scope as ServiceScope;pub use scope::ScopeFactory;pub use entry::ServiceEntry;pub use entry::ServiceFactory;pub use wrapper::ServiceProviderWrapper;pub use bridge::IProvider;pub use error::RdiError;pub use registration::ServiceRegistration;
Modules§
- bridge
- Unified provider trait for service resolution and named registry access.
- collection
- entry
- error
- key
- Service keys for keyed registrations.
- provider
- registration
- scope
- wrapper