MagicStateMachines
MagicStateMachines provides typestate wrappers for compiler-enforced state machines whose contract can live in a separate crate from the runtime implementation.
The library is nightly-only because it uses arbitrary_self_types.
This library in the default configuration does not use unsafe. If you want to use thin-pointers for dynamic dispatch of union state transitions, the feature dynZST can be used which relies on unsafe, but is very small can be easily audited.
All abstractions in this library are zero-cost. The state machines exist only at compile-time and are shown to be well optimizeable by the compiler. Only when using state-machines across boundaries that the compiler can no longer prove: e.g. behind a smart-pointer like Arc or Rc the state incurs storage cost and requires dynamic dispatch. However the dynamic dispatch only happens at boundary at which the state-space gets restricted. Without any restrictions or after a state is proven concrete, all nominally dynamic dispatches are via the compile-time type system back to static dispatch. With other words, calling a dynamically dispatched transition with a concrete state will always be a zero-cost static dispatch, that the compiler can simplify or choose to inline.
The main achievement of this crate is, that it allows using state-machines nearly without any boiler plate, and no penalty to coding ergonomics. A type that that so far did not have a state-machine can be retrofit without changing the overall implementation if it already adhered to the implied state-machine. Async, generics, &mut (SMut), & (&SRef), pin<&mut> (SPinMut), moving (SMove), smart-pointer, and runtime borrow-checking primitives, even traits are all supported. (default fn on traits are NOT supported)
As type-system enforced state-machines change the type, every function that transitions state must always return self. This requires that &mut like borrowing to be facilitated behind a guard which must specialise on the backing storage e.g. Mutex, RwLock or RefCell. For rust std types implementations are provided, for third party they can be implemented without restrictions even for foreign types, as wrappers are mostly defined by a ZST which gets around the foreign type restriction similarily to newtypes.
I generally recomment that state-machines are defined in their own crate, for sake of separation of concerns as well as speeding up compile time (which is fast either way). State machine definitions function analogous to traits, in that they define a contract that implementing types must fulfil. They are however private contracts as they do not provide an interface that could be used by consumers of implementation. Consumers very much can benefit from the compile-time enforcement tho.
In general, this crate can be relied upon for safety proofs. Be aware however as rust does not have linear types, any guard can always be mem::forget()ten.
Contract Crate
Define the stand-in type, state markers, initial states, allowed transitions, and state unions:
use ;
;
use ;
StateMachineDefinition!
The contract crate owns the stand-in and states, so downstream crates cannot add extra transitions.
Implementation Crate
Connect a runtime type to the contract and implement methods with state-typed receivers:
use ;
use ;
StateMachineImpl!
transition! is only usable in the module where StateMachineImpl! generated
the private transition token. Public callers can only transition through the
methods the implementation exposes.
You might have noticed the InOnline trait, that is not explicitly defined in this example. It is generated by the StateMachineDefinition! macro when the union was defined union Online: Connected | Authenticated;
State Storage
State<Storage, T, S> separates the runtime type T, current state marker
S, and storage backend Storage.
Common storage aliases include:
SOwned: directly owned runtime valueSBox<T, S>: boxed owned runtime valueSPinBox<T, S>: pinned boxed runtime valueSRcRefCell<T>: sharedRc<RefCell<_>>stateSArcMutex<T>: sharedArc<Mutex<_>>stateSArcRwLock<T>: sharedArc<RwLock<_>>state
Implementation methods usually constrain storage by capability:
SRef: read-only access to the runtime valueSMut: mutable access and ordinary transitionsSPinRef: pinned shared accessSPinMut: pinned mutable access and pinned transitionsSMove: storage can be moved by value
This lets one method work across owned values, boxes, shared guards, and custom storage backends.
State Unions
StateUnion! and StateMachineDefinition! { union ... } generate:
- a public union marker, for example
Online - a sealed membership trait, for example
InOnline - a generated enum, for example
OnlineEnum<Storage, T>
Use the generated In... trait in method signatures:
Use EnumExt::into_enum when runtime branching is needed (this incures the runtime cost of stack allocating the enum):
use EnumExt;
match Online.into_enum
Use In::into_discriminated when the return type should remain a
DiscriminatedState<_, _, Online>.
Pinned Transitions
Pinned effects receive Pin<&mut T> instead of &mut T:
StateMachineImpl!
Call pinned transitions with:
transition!
Pinned union transitions are also supported:
transition!;
transition!;
pin const requires every union member to share the same pinned body for the
target transition. pin dyn discriminates the current concrete state and runs
that state's pinned body.
Features
dynZST: stores dyn Trait over zero-sized types as a thin-pointer via mydynzstcrate (uses unsafe and relies to )tracing: records transition source, target, and callsite - no longer zero-cost!unique-rc-arc: enablesUniqueRcandUniqueArcowned storage backends- the decompose feature is likely not so great, it allows for a state to be temporarily disconnected from the data, which is runtime enforced by equality on a random sentinal. This likely does break garantees by this api, because an attacker could brute force a collision. I should consider instead using an atomic counter that errors on overflow. but for now it is what it is.
decompose: enablesStateOwned::decomposeandStateOwned::recomposedecompose-rand: enables the stablerandbackend for decomposition IDsnightly-random: uses nightlystd::randomfor decomposition IDs
Layout
StateOwned<T, S> is transparent over T outside of tracing. State markers are
zero-sized, and storage wrappers are designed to preserve the backend layout
where the state can be inferred from the type or from existing runtime storage.