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:: | Type | Confers | On binding | On executable |
---|---|---|---|---|
borrow | BindBorrow | &'call T | ✓ | ✓ |
borrow_mut | BindBorrowMut | &'call mut T | ✓ | ✓ |
take_ref | BindTakeRef | &'static T | ✓ | ✓ |
take_mut | BindTakeMut | &'static mut T | ✓ | |
take | BindTake | T | ✓ | |
as_ref | BindRef | &'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.
- For
-
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) orBindTakeMut
(which mutably borrows the value indefinitely).
- 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
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
&T
to a bound function.&mut T
to a bound function.Bind
.&'static mut T
to a bound function.&'static T
to a bound function.Constants
Traits
ExecutableDefiner
. Provides a method to
attach an entry point with materialized bindings.