Expand description
ridl-loopback — the in-process reference runtime.
This crate implements the twelve port traits of
ridl_rt::port over one in-memory store. It is
the first runtime in this workspace (ADR-0020 decision 6, which names the
crate and fixes ridl-rt as its only dependency), and it is what the
generated interaction face runs against in a test, an example or a
single-process application.
What it is not:
- Not a transport. It carries no frame, opens no socket and has no wire format. A value published here is read here, in the same process.
- Not the engine. The store below is a map and a queue, not the seqlock store, the sans-IO session, the platform traits and the scheduler, which are outside this repository.
- Not a checker. Payload bytes are opaque to it: it carries them and
never verifies one.
Payload::verifyis the generated face’s, on both sides.
§The handles
A runtime presents one handle type per port role rather than one type
implementing them all, and may also offer an aggregate handle covering the
port set one interface’s face needs (ADR-0021 decision 12). This crate
offers both. Its six handles group eleven roles the way that decision
derives the threading split: a handle each for the five roles with a
&mut self method, and one handle for the six whose methods all take
&self, which are exactly the roles several threads may hold at once. The
twelfth port trait, Wakeable, is implemented on every handle, because
each handle wakes its own waiters (see “Waking” below).
Loopback is the aggregate: it implements all twelve port traits by
delegating to the six role handles it holds, and it is what a generated
Client, Publisher or dispatch is normally built over.
use ridl_loopback::Loopback;
use ridl_rt::contract::{CatalogHash, CatalogRef, InterfaceNo, Ordinal};
use ridl_rt::port::{SignalReader, SignalWriter};
let catalog = CatalogRef { name: "face.demo", hash: CatalogHash([0u8; 32]) };
let mut rt = Loopback::new(catalog);
rt.set(InterfaceNo(1), Ordinal(1), &[42]).expect("staged");
rt.commit();
let mut out = [0u8; 8];
let sample = rt.read(InterfaceNo(1), Ordinal(1), &mut out).expect("read");
assert_eq!(&out[..sample.len], &[42]);Loopback::split hands out the six role handles for a program that wants
them apart — one thread reading while another publishes, or two callers on
one provider. ReaderHandle is Send + Sync; the other five are Send
and driven by one thread each. Nothing here declares either: both follow
from the fields, and the assertions at the bottom of this file pin them.
Loopback::attach makes a second aggregate on the same store, for a
program that holds several faces over one runtime, each owning its own.
§What it reports, and what it cannot
The loopback holds no catalog descriptor — driftsys/ridl#381
writes the descriptor file, and giving the loopback one is not yet
assigned — so it has no member table, and there is no ordinal
it can call unknown, no member it can call unowned, and no timing
annotation it can measure a value’s freshness or a call’s remaining time
against. What it therefore never returns:
WriteError::NotOwner, RaiseError::NotOwner, ServeError::NotOwner, any
port error’s Contract variant except
FixedReader::read_fixed’s, and
Freshness::Fresh or Freshness::Stale. Nothing detaches, because every
handle holds the store alive, so Detached never appears either. The one
bound is the call table’s Loopback::SLOTS: a send with every slot
taken answers SendError::Busy. Nothing else is bounded, so TooLarge
appears only from Loopback::fail_next_settle, and Transport::Busy
reaches a caller only when a provider settles a call with it.
§Waking
Every handle implements Wakeable, and a handle stores a waker only
under a kind of key one of its roles observes: a caller handle under
Interest::Outcome, kept with each call, and one waker under
Interest::Slot, a source handle one waker under Interest::Event, and a
handler handle one waker under Interest::Claim.
For Event and Claim the rule is one waker per kind, and a change to
any key of the kind wakes the stored waker, whatever interface it was
registered under (ADR-0021 decision 13); an Outcome waker is per call,
and no change but that call’s settlement, its forget, or the drop of the
caller handle that sent it wakes it (a displacement by another task does,
as for every kind). A settlement wakes the
call’s waiter, a raise wakes each source it queues the occurrence for, and
a send wakes each handler that serves the member, and a reclaimed slot of
the call table wakes every caller’s Slot waiter. A registration whose key
already holds — the outcome is known, an occurrence or a call is waiting, a
slot is free — is woken at once, and so is one under a kind the handle does
not observe. A registration of the waker already stored refreshes it
without waking it. No waker is woken while the store is locked.
Attached::catalog returns the
CatalogRef the runtime was built with, unexamined. ADR-0021 decision 3
places a check of it against the interface’s own CATALOG in a generated
face’s constructor, once, when the face is built; the constructor the Rust
backend emits makes that check and panics on a mismatch (ADR-0023 decision
8). It is the face’s check and not the runtime’s: the loopback carries the
value and compares nothing.
The crate’s as-built design record, with the reasoning behind each of these
choices, is docs/design/ridl-loopback.md in this repository.
Structs§
- Caller
Handle - The
Callerport role. - Handler
Handle - The
Handlerport role. - Handles
- The six role handles of one runtime, as
Loopback::splithands them out. - Loopback
- The aggregate handle: one value implementing all twelve port traits by delegating to the six role handles it holds.
- Reader
Handle - The
&selfport roles:Attached,Clock,SignalReader,FixedReader, and the two signal extensions. - Sink
Handle - The
EventSinkport role. - Source
Handle - The
EventSourceport role: one subscription set and one queue, both in the store under this handle’s identity. - Writer
Handle - The
SignalWriterport role.