Module r3::bind

source ·
Expand description

Bindings (Bind), a static storage with runtime initialization and configuration-time borrow checking.

This module re-exports stable items from r3_core::bind as well as providing some additional items.

Bindings are essentially fancy global variables defined in a kernel configuration. They are defined by Bind::define and initialized by provided closures at runtime. They can be consumed or borrowed by the entry points of executable kernel objects or the initializers of another bindings, with a dependency graph explicitly defined by the passing of binders.

The configuration system tracks the usage of bindings and employs static checks to ensure that the borrowing rules are followed by the users of the bindings. It aborts the compilation if the rules may be violated.

Bindings use hunks (Hunk) as a storage for their contents. Bindings are initialized in startup hooks, where CPU Lock is active, and therefore most kernel services are unavailable.

Relation to Other Specifications: Resources in RTIC 1 serve the similar need with a quite different design. In R3, bindings are defined in modular, encapsulated configuration functions and associated to various kernel objects. The configuration system takes all definitions and figures out the correct initialization order. In RTIC, all resources are defined in one place and initialized by an application-provided #[init] function.

Binders

A binder (Binder) represents a specific borrow mode of a binding. A configuration function creates a binding by calling one of Bind’s methods and uses it in the definition of another object where the binding is intended to be consumed, i.e., borrowed or moved out by its entry point or initializer. The type a binder produces is called its materialized form.

use r3::{bind::bind, kernel::StaticTimer, prelude::*};

let count = bind((), || 0).finish(cfg);
//               ^^  ^^
//            .--'    '--------,
//       no binders  no materialized values

StaticTimer::define()
    // n.b. `(x,)` is a one-element tuple
    .start_with_bind((count.borrow_mut(),), |count: &mut i32| {
        //            ^^^^^^^^^^^^^^^^^^            ^^^^^^^^
        //                    |                         |
        //         BindBorrowMut<'_, _, i32>      &'call mut i32
        //                  gives...          for some lifetime 'call
    })
    .finish(b);

The only way for safe code to make a binding available to runtime code in a meaningful way is to include a binder in a dependency list (e.g., the binder parameter of ExecutableDefinerExt::start_with_bind) as shown above or to call Bind::as_ref to create a BindRef, which is directly consumable in runtime code. Most binders can’t escape from a configuration function.

The following table lists all provided binders:

Bind::TypeConfersOn bindingOn executable
borrowBindBorrow&'call T
borrow_mutBindBorrowMut&'call mut T
take_refBindTakeRef&'static T
take_mutBindTakeMut&'static mut T
takeBindTakeT
as_refBindRef&'static T
  • The Bind:: column shows the methods to create the binders.

  • The Type column shows the types representing the binders.

  • The Confers column shows the respective materialized forms of the binders. The lifetime 'call represents the call duration of the consuming function.

    • For &'call T and &'call mut T, the caller has the freedom of choosing an arbitrary lifetime, so the consuming function must be generic over any lifetimes. In function and closure parameters, reference types without explicit lifetimes are automatically made generic in this way owing to the lifetime elision rules.
  • The On binding column shows which types of binders can be consumed by another binding’s initializer via BindDefiner::init_with_bind.

  • The On executable column shows which types of binders can be consumed by executable objects, viz., tasks, interrupt handlers, and timers, via ExecutableDefinerExt::start_with_bind.

    • An executable object may execute its entry point for multiple times throughout its lifetime. For this reason, an executable object is not allowed to consume BindTake (which moves out the value) or BindTakeMut (which mutably borrows the value indefinitely).

Initialization Order

The configuration system determines the initialization order of the defined bindings by topological sorting with a preference toward the definition order. The specific algorithm is not a part of the stability guarantee.

Planned Features

The following features are planned and may be implemented in the future:

  • Reusing the storage of a binding whose lifetime has ended by having its contents moved out by BindTake or completing its last borrow.

  • Pruning unused bindings, unless they are marked as unpure.

  • Phantom edges to enforce ordering between bindings.

  • Pinning.

Examples

use r3::{
    kernel::{StaticTask, StaticTimer},
    bind::{bind, Bind},
    time::Duration,
    prelude::*,
};

const fn configure_app<C>(cfg: &mut Cfg<C>)
where
    C: ~const traits::CfgTask<System = System> +
       ~const traits::CfgTimer,
{
    // Create a binding and give the timer an exclusive access
    let count = bind((), || 0).finish(cfg);
    StaticTimer::define()
        // `(x,)` is a one-element tuple
        .start_with_bind((count.borrow_mut(),), |count: &mut i32| {
            // The counter persists across invocations
            assert!(*count >= 5);
            *count += 1;
        })
        .period(Duration::from_millis(50))
        .delay(Duration::ZERO)
        .active(true)
        .finish(cfg);

    // Although we gave the timer an exclusive access to `count`,
    // We can still borrow `count` temporarily before the timer
    // starts running. (The initialization of bindings happens in
    // startup hooks, which run before all tasks and timers.)
    bind(
        (count.borrow_mut(),),
        |count: &mut i32| {
            *count = 5;
        },
    ).unpure().finish(cfg);

    // Create a binding
    let num = bind((), || 42).finish(cfg);

    // Alternatively, without using `bind`:
    // let num = Bind::define().init(|| 42).finish(cfg);

    // Then create a reference to it, a reference to the reference,
    // and so on.
    let num = bind((num.take_mut(),), |x| x).finish(cfg);
    let num = bind((num.take_mut(),), |x| x).finish(cfg);
    let num = bind((num.take_mut(),), |x| x).finish(cfg);

    StaticTask::define()
        .start_with_bind(
            (num.borrow_mut(),),
            |num: &mut &'static mut &'static mut &'static mut i32| {
                assert_eq!(****num, 42);
            }
        )
        .priority(2)
        .active(true)
        .finish(cfg);
}

The configuration system enforces the borrowing rules:

const fn configure_app<C>(cfg: &mut Cfg<C>)
where
    C: ~const traits::CfgTask<System = System> +
       ~const traits::CfgTimer,
{
    let count = bind((), || 0).finish(cfg);
    StaticTimer::define()
        .start_with_bind((count.borrow_mut(),), |count: &mut i32| {})
        .period(Duration::from_millis(50))
        .delay(Duration::ZERO)
        .active(true)
        .finish(cfg);

    StaticTask::define()
        // ERROR: `count` is already mutably borrowed by the timer
        .start_with_bind((count.borrow_mut(),), |count: &mut i32| {})
        .priority(2)
        .active(true)
        .finish(cfg);
}

Structs

Constants

Traits

  • Represents a binder, which represents a specific way to access the contents of a binding from a runtime function.
  • A trait for definer objects (static builders) for kernel objects that can spawn a thread that executes after the execution of all startup hooks is complete.
  • An extension trait for ExecutableDefiner. Provides a method to attach an entry point with materialized bindings.
  • A trait for closures that can receive bindings materialized through specific binders (Binder).
  • An extension trait for destructing Bind<_, (T0, T1, ...)> into individual bindings (Bind<_, T0>, Bind<_, T1>, ...).

Functions