Qubit SPI
Typed service provider registry infrastructure for Rust.
Overview
qubit-spi provides a small, explicit SPI layer for crates that define a trait
in one package and allow other packages to provide optional implementations. It
is designed for statically linked Rust crates, where the application decides
which extension crates are linked and when providers are registered.
The public surface is organized around three core types:
ServiceSpec: binds the configuration type and service contract.ServiceProvider: creates one implementation of a service specification.ProviderRegistry: stores providers and resolves them by name, fallback chain, or priority-based automatic selection.
Design Goals
- Explicit Discovery: applications decide which providers are linked and registered.
- Readable Types: registries use one
ServiceSpectype parameter instead of separate service, config, and error parameters. - Type Safety: service and configuration types are fixed by the spec at compile time.
- Deterministic Selection: automatic selection uses stable priority and name ordering.
- Fallback Transparency: failed candidates are preserved for diagnostics.
- Small Runtime Surface: the crate depends only on
logandthiserror.
Features
- One-parameter registries based on a
ServiceSpectype. - Stable
ProviderNamevalidation and normalized provider descriptors. - Runtime availability checks for optional backends.
- Priority-based automatic provider selection.
- Explicit named provider plus fallback-chain selection.
- Shared provider registration through
Arc. - Separated provider creation errors and registry errors.
- Provider creation errors can preserve lower-level source errors.
- Low-noise diagnostics through the
logfacade.
Installation
Add the crate to Cargo.toml:
[]
= "0.2.3"
Quick Start
use Debug;
use ;
;
;
;
let mut registry = new;
registry
.register
.expect;
let greeter = registry
.create_box
.expect;
assert_eq!;
Core Concepts
ServiceSpec
ServiceSpec binds the configuration type and service contract for one service
family. The contract can be a trait object such as dyn MyService; callers then
choose whether a registry returns Box<dyn MyService>, Arc<dyn MyService>,
or Rc<dyn MyService>.
ServiceProvider
ServiceProvider<Spec> is the factory contract implemented by each backend. A
provider supplies:
| Method | Purpose |
|---|---|
descriptor() |
Stable provider id, aliases, and priority |
availability(config) |
Runtime check for optional dependencies |
create_box(config) |
Creates a boxed service value |
create_arc(config) |
Creates an atomically shared service value |
create_rc(config) |
Creates a locally shared service value |
ProviderRegistry
ProviderRegistry<Spec> stores providers for one service specification.
Provider descriptors are captured at registration time. Provider ids and aliases
are normalized into ProviderName values and indexed, so lookup is stable even
if a provider instance has mutable internal state.
ProviderSelection
ProviderSelection is an enum:
Auto: try registered providers by descending priority, then provider id.Named: try a primary provider, then explicit fallbacks in order.
Selection stops at the first provider that is available and successfully creates a service.
Fallback Example
use Debug;
use ;
;
;
;
let mut registry = new;
registry
.register
.expect;
registry
.register
.expect;
let selection = from_names
.expect;
let greeter = registry
.create_selected_box
.expect;
assert_eq!;
Error Model
Provider errors and registry errors are separate:
ProviderCreateErroris returned by a provider factory.ProviderRegistryErroris returned by registration, lookup, and selection.ProviderFailurerecords each failed fallback candidate.
ProviderRegistryError variants:
| Variant | Meaning |
|---|---|
EmptyProviderName |
A provider id, alias, or selector was empty |
InvalidProviderName |
A provider id, alias, or selector used unsupported characters |
DuplicateProviderName |
A provider id or alias conflicts with another name |
UnknownProvider |
No provider matched the requested selector |
ProviderUnavailable |
The selected provider reported unavailable |
ProviderCreate |
The selected provider failed during creation |
NoAvailableProvider |
Every candidate in a fallback chain failed |
EmptyRegistry |
Automatic/selected creation was requested from an empty registry |
NoAvailableProvider keeps ordered ProviderFailure values so callers can
explain the whole fallback chain.
ProviderCreateError::failed_with_source() and
ProviderCreateError::unavailable_with_source() preserve lower-level error
causes through direct registry creation and fallback failure reporting.
Diagnostics
qubit-spi emits low-noise diagnostics through the log facade. Applications
remain responsible for installing the logger implementation they prefer. The
crate uses debug for successful registration and selection outcomes, and
trace for name resolution, candidate ordering, and fallback failures. It does
not log service configuration values or service instances.
Lifetime Model
ProviderRegistry stores providers behind shared trait objects. Registered
providers and service specifications are therefore required to be 'static.
This is intentional: the crate targets long-lived provider registries assembled
from application crates and extension crates, not short-lived registries that
borrow stack-local provider state.
Relationship to Java ServiceLoader
Rust does not have a standard-library equivalent of Java ServiceLoader.
qubit-spi intentionally keeps discovery explicit: extension crates expose a
provider type or registration function, and applications register the providers
they want to make visible. This avoids linker magic and keeps tests isolated.
If a future crate needs linker-time discovery, it can build that layer on top of
ProviderRegistry with crates such as inventory or linkme.
API Overview
| API | Purpose |
|---|---|
ServiceSpec |
Binds provider configuration and service contract |
ServiceProvider |
Provider trait implemented by each backend |
ProviderDescriptor |
Captured provider id, aliases, and priority |
ProviderName |
Validated and normalized provider name |
ProviderRegistry::new() |
Creates an empty registry |
ProviderRegistry::register(provider) |
Registers an owned provider |
ProviderRegistry::register_shared(provider) |
Registers a shared provider |
ProviderRegistry::resolve_provider(name) |
Resolves a provider or returns a precise error |
ProviderRegistry::find_provider(name) |
Option-returning provider lookup convenience |
ProviderRegistry::iter_provider_names() |
Iterates provider ids without allocation |
ProviderRegistry::iter_provider_descriptors() |
Iterates descriptors without allocation |
ProviderRegistry::create_box(name, config) |
Creates a boxed service by provider name |
ProviderRegistry::create_arc(name, config) |
Creates an atomically shared service by provider name |
ProviderRegistry::create_rc(name, config) |
Creates a locally shared service by provider name |
ProviderRegistry::create_auto_box(config) |
Creates a boxed service by automatic priority |
ProviderRegistry::create_auto_arc(config) |
Creates an atomically shared service by automatic priority |
ProviderRegistry::create_auto_rc(config) |
Creates a locally shared service by automatic priority |
ProviderRegistry::create_selected_box(selection, config) |
Creates a boxed service from selection |
ProviderRegistry::create_selected_arc(selection, config) |
Creates an atomically shared service from selection |
ProviderRegistry::create_selected_rc(selection, config) |
Creates a locally shared service from selection |
ProviderSelection |
Automatic or named fallback candidate selection |
ProviderAvailability |
Provider availability state |
ProviderCreateError |
Provider-level creation error |
ProviderFailure |
One failed candidate in a fallback chain |
ProviderRegistryError |
Registry-level error type |
Rust Version
This crate uses Rust 2024 edition and requires Rust 1.94 or newer.
Testing & Code Coverage
This project keeps tests under tests/ and validates provider name handling,
descriptor normalization, registration, lookup, provider selection, fallback
failure reporting, and error formatting.
Running Tests
# Run all tests
# Generate a coverage report
# Generate a text format coverage report
# Align formatting with CI
# Run CI checks (format, clippy, tests, docs, coverage, audit)
Dependencies
Runtime dependencies are intentionally small:
logprovides low-noise diagnostics through the standard logging facade.thiserrorprovides concrete error implementations.
License
Copyright (c) 2026. Haixing Hu.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
See LICENSE for the full license text.
Contributing
Contributions are welcome. Please keep changes aligned with the existing Rust
project structure and run ./ci-check.sh before opening a pull request.
Author
Haixing Hu
Related Projects
More Rust libraries from Qubit are published under the qubit-ltd GitHub organization.
Repository: https://github.com/qubit-ltd/rs-spi