modrun is a modular application composer for Tokio: domain Modules,
constructor injection, explicit module boundaries, and coordinated lifecycle — wired once
at the composition root. It is not a general-purpose DI container or a live runtime
with inter-module messaging.
The stable surface is intentionally small:
| Concept | API |
|---|---|
| Register constructors | provide / provide_result / provide_async / provide_result_async |
| Pre-built values | supply |
| Graph roots | invoke / invoke_async |
| Domain modules | Module + Module::builder("name") + provide_private |
| Value groups | Group<T> + provide_group* / supply_group / init_group / require_group |
| Lifecycle | Lifecycle, Hook, task / task_with |
| Run / stop | ModrunBuilder::run / start, RunningApp::stop, Shutdowner |
Hook trait methods may only gain default implementations in
minor releases. Constructor and invoker arity stays capped at eight parameters; pack
extra deps in a struct.
MSRV is 1.85 (edition 2024), tracked in Cargo.toml and CI.
Application-facing API is stable since 1.0.0.
| Release | Allowed changes |
|---|---|
| Patch | Docs, diagnostics, tracing fields, non-breaking bug fixes |
| Minor | New Error variants (Error is #[non_exhaustive]), new Hook methods with default impls only, new optional builder knobs that do not change existing call sites |
| Major | Remove or rename public items, change constructor/invoker arity cap (currently eight), expose service locator or string-keyed bindings |
Not covered by stability: modrun::__wiring, #[doc(hidden)] builder methods (provide_dyn, …),
and any pub(crate) internals.
Startup cost is dominated by your constructors (I/O, parsing), not the framework. modrun validates the graph once, constructs singletons lazily along a precomputed wave order, runs invokers, then lifecycle hooks — and drops the container. There is no runtime lookup after build.
Documented patterns that keep overhead low: Arc<T> for shared heavy deps,
Arc<Group<T>> / Group<Arc<T>> for large groups, shallow modules, no dot_graph()
on hot paths, and default-features = false when you do not need logging / signal.
See README「Performance」/「性能」.
Application code should register plain functions:
Modrun::builder()
.provide(new_config)
.provide(new_server)
.invoke(boot)Wrapper libraries that re-export modrun wiring use the hidden __wiring
module and #[doc(hidden)] builder methods. These are omitted from docs.rs navigation
and are not part of the application-facing stability promise:
use modrun::__wiring::{DynProvider, InvokeFn, ProviderFn};
let provider: DynProvider = my_ctor.into_provider();
builder.provide_dyn(provider).invoke_dyn(my_boot.into_invoke());Available in modrun::__wiring:
ProviderFn,FallibleProviderFn,AsyncProviderFn,FallibleAsyncProviderFn,ProviderMarkerInvokeFn,AsyncInvokeFnDynProvider,DynInvoker
Builder: provide_dyn, invoke_dyn, provide_group_dyn (+ _private variants on Module),
all #[doc(hidden)]. There are no *_mut builder methods — chain on self instead.
We will not add APIs for:
- Property or field injection
- Runtime string tokens / named bindings (
"primary"vs"replica") — use newtypes - Annotations, derives, or auto-scanning
- Macro-generated entire containers
- Service locator (
get<T>()after build) - Global singleton registries
fx.Populate,fx.Annotate, orfx.Replaceequivalents- First-class
decorate— use wrapper constructors instead - Module-to-module event buses (not in scope today)
Internal Container::get exists only during build (pub(crate)); it must never become public.
Before proposing new wiring APIs, ask:
- Can a plain constructor or
Modulescope express it already? - Does it push modrun toward reflection-style DI instead of Rust constructor injection?
- Does it expand the method matrix (sync/async/fallible × public/private × group × dyn)?
Prefer documentation and examples over new registration verbs. See ROADMAP.md for current priorities.
cargo test
cargo test --doc
cargo run --example basicPlease run tests before opening a PR. Commit messages use Conventional Commits in Chinese (项目惯例).
Publishing is automated by .github/workflows/release.yml
when a version tag is pushed.
One-time setup: add a crates.io API token as the repository secret
CARGO_REGISTRY_TOKEN (Settings → Secrets and variables → Actions).
Per release:
-
Update
CHANGELOG.md(move notes fromUnreleasedinto## [x.y.z] - YYYY-MM-DD). -
Bump
versioninCargo.toml. -
Commit and push to
main. -
Tag and push:
git tag v1.0.1 git push origin v1.0.1
The workflow checks that the tag matches Cargo.toml, runs fmt/clippy/tests/doc, publishes
to crates.io, and opens a GitHub Release with the matching CHANGELOG section.