🧩 rust_di
— Declarative, Async-Safe Dependency Injection for Rust
✨ Highlights
- 🚀 Async-first architecture (factory-based, scoped resolution)
- 🧠 Lifetimes: Singleton, Scoped, Transient
- 📛 Named service instances
- 💡 Declarative registration via #[rust_di::registry(...)]
- 🔁 Task-local isolation (tokio::task_local!)
- 🧰 Procedural macros with zero boilerplate
- 🧪 Circular dependency detection
- 📦 Thread-safe (using Arc, RwLock, DashMap, ArcSwap, OnceCell)
⚡️ Getting Started
1. Add to Cargo.toml
[]
= "2.1.0"
2. Register Services (in a way convenient for you)
;
3. Resolve Inside Scope
🧮 Scope Bootstrapping
Before resolving any services, make sure to initialize the DI system:
async
This sets up:
- All services declared via inventory::submit!
- Global singletons & factories
- Internal caches and resolving state
You only need to call it once, typically at the beginning of main() or your test setup.
🔍 Example: Main Function with Initialization
async
🧠 Async Entrypoint — #[rust_di::main]
Use #[rust_di::main]
to simplify your async fn main
. It ensures:
- ✅ rust_di::initialize().await
- ✅ DIScope::run_with_scope(...)
- ✅ DI services available from the start
🧪 Example
async
⚠️ Must be placed above #[tokio::main] to work correctly.
🌀 Automatic DI Scope Initialization - #[with_di_scope]
⚠️ The #[rust_di::with_di_scope]
macro works only on standalone
async fn
, not on trait methods or functions wrapped with conflicting attribute macros such as #[tokio::main]
or
#[test]
.
✅ Use it for plain
async fn
entrypoints, background workers, or utility functions where full DI context is needed.
async
🧠 This macro fully replaces the manual block shown in section 3. Resolve services.
This pattern is ideal for long-running background tasks, workers, or event handlers that need access to scoped services.
✅ Why use #[with_di_scope]
?
- Eliminates boilerplate around
DIScope::run_with_scope
- Ensures
task-local
variables are properly initialized - Works seamlessly in
main
,background loops
, or anyasync entrypoint
- Encourages
clean
, scoped service resolution
🔄 Service Dependencies via DiFactory
You can declare service dependencies by implementing DiFactory
.
This allows a service to resolve other services during its creation:
use DIScope;
use DiError;
use DiFactory;
use registry;
use Arc;
;
The DiFactory
is automatically invoked if factory is enabled in #[registry(...)].
✨ Factory Benefits
- 🔧 Resolves dependencies with async precision
- 🎯 Keeps instantiation logic colocated
- 🧩 Enables complex composition across lifetimes
✋ Manual Service Registration
In some situations—like ordering guarantees, test injection, or dynamic setup—you may want to bypass macros and register manually:
use DIScope;
use DiError;
use register_singleton_name;
;
async
🧠 Manual API Available
Function Description register_singleton unnamed global instance register_singleton_name(name) named global instance register_scope_name(name) scoped factory register_transient_name(name) re-created per request
Function | Description |
---|---|
register_transient | re-created per request |
register_transient_name | named re-created per request |
register_scope | scoped factory |
register_scope_name | named scoped factory |
register_singleton | unnamed global instance |
register_singleton_name | named global instance |
All support factories and return Result.
📚 These extensions give you full control—whether bootstrapping large systems, injecting mocks in tests, or dynamically assembling modules.
🔐 Safety Model
- Services stored as
Arc<RwLock<T>>
- Global state managed via
OnceCell
&ArcSwap
- Scope-local cache via
DashMap
- Panics on usage outside active DI scope
- Circular dependency errors on recursive resolutions
🧠 Lifetimes
Lifetime | Behavior |
---|---|
Singleton | One instance per App. Global, shared across all scopes |
Scoped | Created one instance per DIScope::run_with_scope() |
Transient | New instance every timeRe-created on every .get() |
🧰 Procedural Macro
Supports:
- Singleton, Scoped, Transient
- factory — use
DiFactory
orcustom factory
- name = "..." — register named instance
🔒 Safety
- All services are stored as
Arc<RwLock<T>>
- Internally uses
DashMap
,ArcSwap
, andOnceCell
Task-local
isolation viatokio::task_local!
⚠️ Limitation: tokio::spawn
drops DI context
Because DIScope
relies on task-local
variables (tokio::task_local!
), spawning a new task with tokio::spawn
will
lose the current DI scope context.
;
spawn
✅ Workaround
If you need to spawn a task that uses DI, wrap the task in a new scope:
;
spawn
Alternatively, pass the resolved dependencies into the task before spawning.
#StandForUkraine 🇺🇦
This project aims to show support for Ukraine and its people amidst a war that has been ongoing since 2014. This war has a genocidal nature and has led to the deaths of thousands, injuries to millions, and significant property damage. We believe that the international community should focus on supporting Ukraine and ensuring security and freedom for its people.
Join us and show your support using the hashtag #StandForUkraine. Together, we can help bring attention to the issues faced by Ukraine and provide aid.