Expand description
Pluggable, type-safe execution framework for composing runtime behavior via plugin models and families.
This module offers two complementary abstractions for extensible, type-safe runtime behaviour:
-
Read Plugin Model to understand the fundamental unit of computation - a single operation plugin, analogous to a pure function or a procedure that may mutate its input and produce an output.
-
Read Plugin Families when modelling a cohesive state-machine-like component composed of multiple related operations (methods). A family groups several operation-specific plugin models under one logical root, while the concrete implementation variant of each operation is plugged via a single family model.
Note: Plugin families are built on top of plugin models.
To correctly design or use families, one must first understand the plugin model abstraction, since families internally orchestrate multiple models as their operational building blocks.
In short:
Simple, single-step transformation? -> Plugin Model
Multi-operation logical component / state machine? -> Plugin Family (built from Plugin Models)Both approaches share the same execution infrastructure and compile-time type-safety guarantees, but differ in how behaviour is structured, composed, and ultimately resolved.
§Plugin Model
A plugin model is a type-safe, swappable unit of computation with optional context. It enables deterministic, composable, and runtime-configurable behavior.
Each model:
- Implements
PurePluginModel<Input, Context, Output>orMutablePluginModel<Input, Context, Output> - Produces an output from input (and optional context)
Execution:
compute(input, &context) -> outputcompute_mut(&mut input, &context) -> output
Context:
- Provided via
ModelContext - Defined using
plugin_context
Tooling:
- Declare via
plugin_types - Define via
plugin_model - Execute via
plugin_output - Test with
plugin_test
§Motivation
Conventional trait-based designs couple the contract and the implementation into a single resolution step:
Sub-set Contract (Trait Bounds)
|
v
Concrete Type (Implementation)The implementation is chosen first, and the contract is something it must satisfy. Any stronger bounds or richer behavior remain internal to the concrete type and cannot be independently selected or composed.
This leads to key limitations:
- Behavior is fixed once the type is chosen
- Stronger capabilities cannot be surfaced or selected explicitly
- Context-driven or configuration-based behavior becomes difficult
§Behaviour Model
Plugin models treat behavior as a compatibility problem between two independent entities:
Sub-set Contract (Pallet)
<->
Super-set Capability (Model + Bounds + Context)These are defined independently and only come together through compatibility matching.
Matching rule:
- The super-set must satisfy all requirements of the sub-set
- The sub-set must allow the super-set’s stronger bounds
Only when both conditions hold does a valid composition exist.
§Benefits
- Decoupled contract and behavior
- Multiple interchangeable implementations
- Context-driven execution
- Late selection via configuration
- Full compile-time verification
Behaviour is not implemented, it is resolved by matching a sub-set contract with a compatible super-set capability.
§Example: Sorter Plugin
This example shows how a pallet can dynamically select sorting strategies at runtime via plugin types.
// ----- Support Crate -------
/// A generic sorter plugin trait.
///
/// This trait defines a plugin point where the actual sorting logic
/// is provided by an associated plugin model and its context.
pub trait Sorter<Input> {
/// The output type produced by the sorter.
type Output;
// Declare the associated plugin model and context types.
// These will be supplied by downstream crates (e.g., pallets or runtime).
plugin_types! {
input: Input,
output: Self::Output,
model: Model,
context: Context,
}
plugin_output! {
/// Execute the sorting logic using the injected plugin model.
///
/// The actual implementation is resolved at compile time based on
/// the associated `Model` and `Context` types.
fn sort
input: values,
model: Self::Model,
context: Self::Context,
}
}
// ----- Pallet Crate -------
/// Pallet configuration exposing plugin hook points.
///
/// The runtime will decide which concrete model and context to use.
pub trait Config: frame_system::Config {
/// Input type consumed by the sorter plugin.
type InputX;
/// Output type produced by the sorter plugin.
type OutputX;
// Declare pallet-level plugin types that must satisfy the plugin contract.
plugin_types! {
input: Self::InputX,
output: Self::OutputX,
model: ModelX, // Concrete model chosen by the runtime
context: ContextX, // Concrete context provider chosen by the runtime
}
}
/// Implement the generic sorter plugin for the pallet.
///
/// The pallet simply forwards execution to the configured model.
impl<T: Config> Sorter<T::InputX> for Pallet<T> {
type Output = T::OutputX;
type Model = T::ModelX;
type Context = T::ContextX;
}
/// Helper function demonstrating how the plugin is executed generically.
fn try_sort<T: Config>(values: &T::InputX) -> T::OutputX {
<Pallet<T> as Sorter<T::InputX>>::sort(values)
}
// ----- Runtime Crate -------
/// Define a generic plugin model.
/// This model sorts the generic type `Vector` in ascending order and then
/// purges all elements greater than the runtime `until` threshold.
/// Such many models like this can live in plugin-registries
plugin_model! {
name: CappedSort,
input: Vector,
output: Vector,
context: UntilConfig<Number>,
others: [Number],
bounds: [
// Elements must be comparable and clonable
Number: Unsigned + Clone + Ord,
// Vector must be iterable and rebuildable after filtering
Vector: IntoIterator<Item = Number> + FromIterator<Number> + Clone,
],
compute: |values, ctx| {
// Clone input so original remains unchanged
let mut v: Vector = values.clone();
// Retrieve runtime threshold from context
let until = ctx.0.clone();
// Sort from small to large
let mut temp: Vec<Number> = v.into_iter().collect();
temp.sort();
// Find first element greater than `until`
// Purge that element and everything after it
let filtered = temp
.into_iter()
.take_while(|x| *x <= until)
.collect::<Vec<_>>();
// Rebuild the output vector from the filtered values
filtered.into_iter().collect()
}
}
/// Context data structure holding the threshold value.
/// Such many models's contexts like this can live in
/// plugin-registries, as models and its contexts are tightly coupled
struct UntilConfig<Number>(Number);
/// Define a concrete context provider supplying the threshold.
plugin_context! {
name: MyContext,
context: UntilConfig<u8>,
value: UntilConfig(10),
}
/// Inject the concrete model and context into the runtime configuration.
impl Config for Runtime {
type InputX = Vec<u8>;
type OutputX = Vec<u8>;
type ModelX = CappedSort; // Uses capped sorting logic
type ContextX = MyContext; // Provides the `until` threshold
}
// Example behavior:
// Input: vec![12, 3, 8, 25, 5]
// Sorted: [3, 5, 8, 12, 25]
// until = 10
// Output: [3, 5, 8] // elements > 10 are purged
The pallet only assumes a generic “sorter” transformation. The runtime injects
CappedSort, which sorts values ascending and purges elements greater than a
contextual until threshold. The pallet sees only the minimal contract,
while the runtime provides richer, context-driven logic.
§Plugin Families
A plugin family extends a single plugin model into a unified logical plugin with multiple related operations.
Unlike a model (one computation), a family represents a cohesive component (e.g., state machine or service) whose operations are selected via child markers, while a family type maps them to concrete models.
This lets callers use a single interface while deferring implementation choice to runtime configuration.
§Motivation
A single model fits simple transformations:
Model -> Implementation
Context -> ParametersBut real systems need:
- Multiple related operations
- Multiple strategy variants
- Configurable behaviour
A plugin family groups these operations under one unit, where:
- Children = operations
- Family type = model mapping
Result: structured, state-machine-like design with compile-time resolution.
§State-Machine Style Logical Plugin
A plugin family acts as a logical component exposing multiple operations, similar to a state machine or service:
Family Root (Unified Interface)
|-- Child A -> Operation A
|-- Child B -> Operation B
|-- Child C -> Operation C -> Concrete Model (via Family Type)- Root: unified special-interface
- Child: operation selector
- Family type: maps each operation to a concrete model
Calling a child is equivalent to invoking a method on the plugin. The concrete model is resolved by the family type, with context passed through to execution.
§Family Contract Consistency
All models within a family type are expected to share the same execution contract:
Input -> shared
Output -> shared
Context -> sharedThis allows the family to behave as a uniform pluggable component, where operations can be invoked without knowledge of the underlying model.
Family Type
|-- Child A -> ModelA<Input, Context, Output>
|-- Child B -> ModelB<Input, Context, Output>
|-- Child C -> ModelC<Input, Context, Output>With a consistent (Input, Output, Context) signature, callers interact
through the root interface while the compiler resolves the concrete model.
While not strictly enforced, consistent contracts are recommended for clarity and interchangeability.
§Trait Bound Consistency
Plugin models define (Input, Output, Context) generically via trait bounds.
Within a family type, the concrete types must satisfy the combined bounds of all possible models.
ModelA requires: Input: Ord
ModelB requires: Input: Clone-> Caller must provide:
Input: Ord + CloneThe family contract therefore reflects the union of required bounds:
Family Contract
Input: Ord + Clone
Output: ...This guarantees that any selected model can be resolved safely at compile time.
§Plugin Family as a Logical Plugin State Machine
+---------------------------------------+
| FAMILY ROOT |
| Unified logical plugin interface |
+--------------------+------------------+
|
Concrete Family Type
|
+-----------------------------------+-----------------------------------+
| | |
+--------------+ +--------------+ +--------------+
| Child A | | Child B | | Child C |
| Operation A | | Operation B | | Operation C |
+------+-------+ +------+-------+ +------+-------+
| | |
+------+-------+ +------+-------+ +------+-------+
| Model A | | Model B | | Model C |
| selected by | | selected by | | selected by |
| Family Type | | Family Type | | Family Type |
+------+-------+ +------+-------+ +------+-------+
| | |
+---------------------------- Context ----------------------------------+
(shared across models)In this structure:
- The family root represents the unified logical plugin interface.
- Each child marker represents one operation of that interface.
- The family type determines which concrete model implements each operation.
All models belonging to the same family share the same context type. The context is therefore represented as a single input flowing into the resolved model during execution.
§Resolution Flow
Caller invokes:
FamilyRoot + FamilyType + ChildX
Compiler resolves:
(FamilyType, ChildX) -> ConcreteModel
Execution:
ConcreteModel.compute(input, context)This allows callers to treat the family as a single logical plugin while the compiler statically resolves the concrete model for each operation.
§Declaration Model
A plugin family is constructed using three complementary macros:
Together they define the operations, models, and family implementation that make up a plugin family.
§1. Declaring the Family Interface
The declare_family macro defines the family
root trait and a set of child marker types representing operations
of the logical plugin.
Family Root
|-- ChildA
|-- ChildB
|-- ChildCThe root trait represents the unified plugin interface, while each child marker identifies one operation that the family exposes.
§2. Defining Plugin Models
Concrete behaviour is implemented using plugin_model.
Each plugin model implements a specific (Input, Context, Output)
computation and can later be attached to a family operation.
ModelA<Input, Context, Output>
ModelB<Input, Context, Output>
ModelC<Input, Context, Output>Models remain independent units of computation and can be reused across different families.
§3. Defining the Family Implementation
The define_family macro creates a concrete
family type that binds each child operation to a specific model.
FamilyType
|-- ChildA -> ModelA
|-- ChildB -> ModelB
|-- ChildC -> ModelCThis family type represents a concrete implementation of the family root and determines which models are used for each operation.
§Resolution Model
When a caller invokes an operation, it refers only to the family root and a child marker.
The compiler then resolves the concrete model using the configured family type:
(FamilyType, Child) -> ConcreteModelThe resolved model is then executed using the provided (Input, Context)
values.
This design allows callers to interact with the family as a single logical plugin while the runtime configuration determines the concrete behaviour through the selected family type.
§Immutable vs Mutable Operational Variants
A family may host immutable (PurePluginModel) and/or mutable
(MutablePluginModel) variants for its operations. However, mutability forms
part of the execution contract:
- Immutable execution resolves only to pure models.
- Mutable execution resolves only to mutable models.
Even if both coexist in the same family hierarchy, they are not interchangeable at the usage site because the caller’s expected execution semantics are part of the type-level interface.
§Example: Family-Based Model Resolution
This example demonstrates how a plugin family defines a semantic extension point on a caller trait and how concrete models attach themselves to that family. The runtime then selects the active model by supplying an appropriate context.
In this design the family is declared by the caller trait, because the trait owns the extension point. Concrete plugin models merely register themselves under that family.
§Caller Trait - Declaring the Plugin Family
The caller trait defines the plugin contract and declares the family that models may attach to.
declare_family! {
root: pub MathFamilyRoot,
child: [MaybePlusOne]
}
pub trait MathTrait {
type Input: AtLeast8BitUnsigned;
type Output: AtLeast8BitUnsigned;
plugin_types! {
input: Self::Input,
output: Self::Output,
root: MathFamilyRoot,
family: MathFamily
context: MathContext,
}
plugin_output! {
fn request,
input: Self::Input,
output: Self::Output,
root: MathFamilyRoot,
family: Self::MathFamily
child: MaybePlusOne,
context: Self::MathContext,
}
}Here:
MathFamilydefines the semantic plugin domain.MaybePlusOneacts as a child selector, representing an optional increment strategy.
The trait itself does not specify which model is used.
§Plugin Models - Registering Implementations
Plugin models implement behavior for a specific (Family, Child, Context)
combination. Multiple models may attach to the same child selector.
pub struct AddOneContext;
plugin_model! {
name: AddOne,
input: Value,
context: AddOneContext,
bounds: [Value: AtLeast8BitUnsigned],
compute: |v, _ctx| {
v.clone().saturating_add(One::one())
}
}
define_family! {
root: MathFamilyRoot,
family: OneFamily,
input: Value,
context: AddOneContext
bounds: [Value: AtLeast8BitUnsigned]
child: [
MaybePlusOne => AddOne,
],
}pub struct AddNothingContext;
plugin_model! {
name: AddNothing,
input: mut Value,
context: AddNothingContext,
bounds: [Value: Clone],
compute: |v, _ctx| {
v.clone()
}
}
define_family! {
root: MathFamilyRoot,
family: NoneFamily,
input: Value,
context: AddNothingContext
bounds: [Value: Clone]
child: [
MaybePlusOne => AddNothing,
],
}Both models attach to the same family and child selector but differ in unified family type, context and execution behavior.
§Pallet Wiring - Remaining Generic
The pallet implements the caller trait without committing to a concrete model. It simply forwards the family and context from its configuration.
struct Pallet<T: Config>(PhantomData<T>);
impl<T: Config> MathTrait for Pallet<T> {
type Input = T::XInput;
type Output = T::XOutput;
type MathFamily = T::XMathFamily;
type MathContext = T::XMathContext;
}This keeps the pallet generic and reusable across runtimes.
§Runtime Injection - Selecting the Active Model
The runtime chooses the concrete behavior by supplying a context that matches one of the registered models.
pub trait Config {
type XInput: AtLeast8BitUnsigned;
type XOutput: AtLeast8BitUnsigned;
plugin_types! {
input: Self::XInput,
output: Self::XOutput,
root: MathFamilyRoot,
family: XMathFamily,
context: XMathContext,
}
}
plugin_context! {
name: MyContext,
context: AddOneContext,
value: AddOneContext,
}
pub struct Runtime;
impl Config for Runtime {
type XInput = u8;
type XOutput = u8;
type XMathFamily = OneFamily;
type XMathContext = AddOneContext;
// Also can be plugged towards
// type XMathFamily = NoneFamily;
// type XMathContext = AddNothingContext;
}§Resolution Flow
Runtime selects:
Family = OneFamily
Child = MaybePlusOne
Context = AddOneContext
Matching Model:
AddOne<Input=u8, Context=AddOneContext, Output=u8>If the runtime instead supplied NoneFamily & AddNothingContext, the alternative model
would be selected automatically.
The caller trait never names a concrete model. Instead, the compiler resolves the correct implementation purely from the type-level contract:
(Family, Child, Context) -> ModelThis enables fully static, type-safe plugin resolution without runtime dispatch or registration tables.
Traits§
- Model
Context - Represents a source of context for models.
- Mutable
Plugin Model - Trait implemented by mutable plugin models that may transform their input in-place while still producing an output.
- Pure
Plugin Model - Core trait implemented by all immutable plugin models.