Module plugins

Source
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:

Execution:

  • compute(input, &context) -> output
  • compute_mut(&mut input, &context) -> output

Context:

Tooling:

§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 -> Parameters

But 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 -> shared

This 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 + Clone

The 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
  |-- ChildC

The 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 -> ModelC

This 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) -> ConcreteModel

The 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:

  • MathFamily defines the semantic plugin domain.
  • MaybePlusOne acts 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) -> Model

This enables fully static, type-safe plugin resolution without runtime dispatch or registration tables.

Traits§

ModelContext
Represents a source of context for models.
MutablePluginModel
Trait implemented by mutable plugin models that may transform their input in-place while still producing an output.
PurePluginModel
Core trait implemented by all immutable plugin models.