ridl_loopback/lib.rs
1//! `ridl-loopback` — the in-process reference runtime.
2//!
3//! This crate implements the twelve port traits of
4//! [`ridl_rt::port`](https://docs.rs/ridl-rt) over one in-memory store. It is
5//! the first runtime in this workspace (ADR-0020 decision 6, which names the
6//! crate and fixes `ridl-rt` as its only dependency), and it is what the
7//! generated interaction face runs against in a test, an example or a
8//! single-process application.
9//!
10//! What it is not:
11//!
12//! - **Not a transport.** It carries no frame, opens no socket and has no wire
13//! format. A value published here is read here, in the same process.
14//! - **Not the engine.** The store below is a map and a queue, not the seqlock
15//! store, the sans-IO session, the platform traits and the scheduler, which
16//! are outside this repository.
17//! - **Not a checker.** Payload bytes are opaque to it: it carries them and
18//! never verifies one. `Payload::verify` is the generated face's, on both
19//! sides.
20//!
21//! # The handles
22//!
23//! A runtime presents one handle type per port role rather than one type
24//! implementing them all, and may also offer an aggregate handle covering the
25//! port set one interface's face needs (ADR-0021 decision 12). This crate
26//! offers both. Its six handles group eleven roles the way that decision
27//! derives the threading split: a handle each for the five roles with a
28//! `&mut self` method, and one handle for the six whose methods all take
29//! `&self`, which are exactly the roles several threads may hold at once. The
30//! twelfth port trait, `Wakeable`, is implemented on every handle, because
31//! each handle wakes its own waiters (see "Waking" below).
32//! [`Loopback`] is the aggregate: it implements all twelve port traits by
33//! delegating to the six role handles it holds, and it is what a generated
34//! `Client`, `Publisher` or `dispatch` is normally built over, and it states
35//! the encoding the face encodes with. Every handle carries that encoding as
36//! its type parameter `E` and implements [`Encoded`] with it; the default is
37//! FlatBuffers, and [`Loopback::with_encoding`] names another.
38//!
39//! ```
40//! use ridl_loopback::Loopback;
41//! use ridl_rt::contract::{CatalogHash, CatalogRef, InterfaceNo, Ordinal};
42//! use ridl_rt::port::{SignalReader, SignalWriter};
43//!
44//! let catalog = CatalogRef { name: "face.demo", hash: CatalogHash([0u8; 32]) };
45//! let mut rt = Loopback::new(catalog);
46//!
47//! rt.set(InterfaceNo(1), Ordinal(1), &[42]).expect("staged");
48//! rt.commit();
49//!
50//! let mut out = [0u8; 8];
51//! let sample = rt.read(InterfaceNo(1), Ordinal(1), &mut out).expect("read");
52//! assert_eq!(&out[..sample.len], &[42]);
53//! ```
54//!
55//! [`Loopback::split`] hands out the six role handles for a program that wants
56//! them apart — one thread reading while another publishes, or two callers on
57//! one provider. [`ReaderHandle`] is `Send + Sync`; the other five are `Send`
58//! and driven by one thread each. Nothing here declares either: both follow
59//! from the fields, and the assertions at the bottom of this file pin them.
60//! [`Loopback::attach`] makes a second aggregate on the same store, for a
61//! program that holds several faces over one runtime, each owning its own.
62//!
63//! # What it reports, and what it cannot
64//!
65//! The loopback holds no catalog descriptor — driftsys/ridl#381
66//! writes the descriptor file, and giving the loopback one is not yet
67//! assigned — so it has no member table, and there is no ordinal
68//! it can call unknown, no member it can call unowned, and no timing
69//! annotation it can measure a value's freshness or a call's remaining time
70//! against. What it therefore never returns:
71//! `WriteError::NotOwner`, `RaiseError::NotOwner`, `ServeError::NotOwner`, any
72//! port error's `Contract` variant except
73//! [`FixedReader::read_fixed`]'s, and
74//! `Freshness::Fresh` or `Freshness::Stale`. Nothing detaches, because every
75//! handle holds the store alive, so `Detached` never appears either. The one
76//! bound is the call table's [`Loopback::SLOTS`]: a send with every slot
77//! taken answers `SendError::Busy`. Nothing else is bounded, so `TooLarge`
78//! appears only from [`Loopback::fail_next_settle`], and `Transport::Busy`
79//! reaches a caller only when a provider settles a call with it.
80//!
81//! # Waking
82//!
83//! Every handle implements [`Wakeable`], and a handle stores a waker only
84//! under a kind of key one of its roles observes: a caller handle under
85//! `Interest::Outcome`, kept with each call, and one waker under
86//! `Interest::Slot`, a source handle one waker under `Interest::Event`, and a
87//! handler handle one waker under `Interest::Claim`.
88//! For `Event` and `Claim` the rule is one waker per kind, and a change to
89//! any key of the kind wakes the stored waker, whatever interface it was
90//! registered under (ADR-0021 decision 13); an `Outcome` waker is per call,
91//! and no change but that call's settlement, its `forget`, or the drop of the
92//! caller handle that sent it wakes it (a displacement by another task does,
93//! as for every kind). A settlement wakes the
94//! call's waiter, a raise wakes each source it queues the occurrence for, and
95//! a send wakes each handler that serves the member, and a reclaimed slot of
96//! the call table wakes every caller's `Slot` waiter. A registration whose key
97//! already holds — the outcome is known, an occurrence or a call is waiting, a
98//! slot is free — is woken at once, and so is one under a kind the handle does
99//! not observe. A registration of the waker already stored refreshes it
100//! without waking it. No waker is woken while the store is locked.
101//!
102//! [`Attached::catalog`] returns the
103//! `CatalogRef` the runtime was built with, unexamined. ADR-0021 decision 3
104//! places a check of it against the interface's own `CATALOG` in a generated
105//! face's constructor, once, when the face is built; the constructor the Rust
106//! backend emits makes that check and panics on a mismatch (ADR-0023 decision
107//! 8). It is the face's check and not the runtime's: the loopback carries the
108//! value and compares nothing.
109//!
110//! The crate's as-built design record, with the reasoning behind each of these
111//! choices, is `docs/design/ridl-loopback.md` in this repository.
112
113use std::sync::{Arc, Mutex};
114use std::task::Waker;
115
116use ridl_rt::contract::{CatalogRef, InterfaceNo, Ordinal};
117use ridl_rt::encoding::{Encoding, FlatBuffers};
118use ridl_rt::error::CallError;
119use ridl_rt::port::{
120 Attached, Caller, Changed, Claim, ClaimId, Clock, CoherentSignals, Correlation, Encoded,
121 EventSink, EventSource, FixedReader, Handler, Interest, RaiseError, RawOccurrence, RawSample,
122 ReadError, ScannableSignals, SendError, ServeError, SettleError, SignalReader, SignalWriter,
123 SubscribeError, Wakeable, Watermark, WriteError,
124};
125use ridl_rt::sample::{Duration, Timestamp};
126use ridl_rt::trace::TraceContext;
127
128mod handle;
129mod store;
130
131pub use handle::{
132 CallerHandle, HandlerHandle, ReaderHandle, SinkHandle, SourceHandle, WriterHandle,
133};
134
135use handle::{lock, Shared};
136use store::Store;
137
138/// The six role handles of one runtime, as [`Loopback::split`] hands them out.
139/// Each carries the runtime's encoding `E`.
140pub struct Handles<E: Encoding = FlatBuffers> {
141 /// `Attached`, `Clock`, `SignalReader`, `FixedReader`, `ScannableSignals`
142 /// and `CoherentSignals`.
143 pub reader: ReaderHandle<E>,
144 /// `SignalWriter`.
145 pub writer: WriterHandle<E>,
146 /// `EventSource`.
147 pub source: SourceHandle<E>,
148 /// `EventSink`.
149 pub sink: SinkHandle<E>,
150 /// `Caller`.
151 pub caller: CallerHandle<E>,
152 /// `Handler`.
153 pub handler: HandlerHandle<E>,
154}
155
156/// The aggregate handle: one value implementing all twelve port traits by
157/// delegating to the six role handles it holds.
158///
159/// A face is built over one value implementing at least the port traits its
160/// interface needs, and a generated `Client` is commonly bound over
161/// `SignalReader + EventSource + Caller` at once, which no single role handle
162/// satisfies. That is what an aggregate is for (ADR-0021 decision 12). Pass it
163/// by value, or as `&mut` under the forwarding impls of ADR-0021 decision 11.
164///
165/// The type parameter `E` is the encoding its ports carry, stated for a face
166/// over it to read through [`Encoded`]. The default is [`FlatBuffers`], because
167/// both ends of a loopback are one process and FlatBuffers is the codec that
168/// reads a payload in place; [`new`](Loopback::new) builds that default, and
169/// [`with_encoding`](Loopback::with_encoding) names another. The encoding is
170/// carried, not applied: the loopback moves bytes and decodes none of them.
171pub struct Loopback<E: Encoding = FlatBuffers> {
172 shared: Shared,
173 catalog: CatalogRef,
174 handles: Handles<E>,
175}
176
177impl Loopback<FlatBuffers> {
178 /// The number of calls the runtime holds at once. A call holds its slot
179 /// from its send until one of these reclaims it:
180 ///
181 /// - [`Caller::forget`], or the drop of the caller handle that sent it,
182 /// while it is settled or still waiting for a handler and offered to
183 /// none;
184 /// - its settlement, when it was forgotten while a handler held it —
185 /// taken, or offered through `ReadError::ShortClaim` and still waiting
186 /// (driftsys/ridl#569);
187 /// - the drop of the handler that held it unsettled, when it was
188 /// forgotten.
189 ///
190 /// So a settled call keeps its slot until it is forgotten, and a claimed
191 /// call keeps its slot after its `forget` until it is settled or its
192 /// handler is dropped. With every slot taken, [`Caller::command`] and
193 /// [`Caller::query`] answer [`SendError::Busy`], on every caller handle,
194 /// because the table is the runtime's.
195 ///
196 /// Sixteen is a small bound, chosen so that a test reaches it in a few
197 /// sends and a program that never forgets a call finds out at once rather
198 /// than after its memory grows. The loopback has no catalog descriptor to
199 /// size a byte budget from: the descriptor file is written by
200 /// `--emit catalog` (driftsys/ridl#381), and nothing yet wires
201 /// the loopback to read one. So the slot count is its only bound (note
202 /// F-9 of the async face design).
203 ///
204 /// The generated async client's future forgets its call
205 /// when it leaves the waiting phase: in the poll that takes the outcome,
206 /// at the call's deadline, and on drop while the call is still waiting.
207 /// So a program that calls through the generated face holds one slot per
208 /// call in flight. A program that sends through the `Caller` port
209 /// directly must forget each correlation itself once it has read the
210 /// outcome, or drop the caller handle, which forgets that handle's calls;
211 /// otherwise it gets `SendError::Busy` from its seventeenth call on.
212 ///
213 /// The count is the store's and the same under every encoding, so it is
214 /// defined once, here, where `Loopback::SLOTS` resolves with no type
215 /// argument.
216 pub const SLOTS: usize = store::SLOTS;
217
218 /// A runtime attached to `catalog`, with an empty store and its clock at
219 /// [`Timestamp`] 0, whose ports carry FlatBuffers, the loopback's
220 /// default encoding.
221 ///
222 /// The catalog is carried, not checked: see the crate documentation.
223 #[must_use]
224 pub fn new(catalog: CatalogRef) -> Self {
225 Loopback::<FlatBuffers>::with_encoding(catalog)
226 }
227}
228
229impl<E: Encoding> Loopback<E> {
230 /// A runtime attached to `catalog`, with an empty store and its clock at
231 /// [`Timestamp`] 0, whose ports carry `E`.
232 ///
233 /// `Loopback::<Proto3>::with_encoding(catalog)` is a runtime whose ports
234 /// carry proto3, which a face over them reads through [`Encoded`]. The
235 /// catalog is carried, not checked: see the crate documentation.
236 #[must_use]
237 pub fn with_encoding(catalog: CatalogRef) -> Self {
238 Loopback::over(Arc::new(Mutex::new(Store::new())), catalog)
239 }
240
241 /// An aggregate of six new role handles on `shared`: what
242 /// `with_encoding` and `attach` both build, over a new store and over
243 /// this one.
244 fn over(shared: Shared, catalog: CatalogRef) -> Self {
245 let handles = Handles {
246 reader: ReaderHandle::new(Arc::clone(&shared), catalog),
247 writer: WriterHandle::new(Arc::clone(&shared), catalog),
248 source: SourceHandle::new(Arc::clone(&shared), catalog),
249 sink: SinkHandle::new(Arc::clone(&shared), catalog),
250 caller: CallerHandle::new(Arc::clone(&shared), catalog),
251 handler: HandlerHandle::new(Arc::clone(&shared), catalog),
252 };
253 Loopback {
254 shared,
255 catalog,
256 handles,
257 }
258 }
259
260 /// Hands out the six role handles this aggregate holds. Every one of them
261 /// keeps the same store, so a value published through `writer` is read
262 /// through `reader`.
263 #[must_use]
264 pub fn split(self) -> Handles<E> {
265 self.handles
266 }
267
268 /// An additional aggregate on the same store: six new role handles, made
269 /// the way [`reader`](Loopback::reader) to [`handler`](Loopback::handler)
270 /// make one each. This is how an application holds several faces over one
271 /// runtime — a `Client`, a `Publisher`, a second `Client` with a call of
272 /// its own in flight — each owning its own aggregate (driftsys/ridl#488).
273 ///
274 /// What is in the store is shared: the published signals, the `fixed`
275 /// values, the clock and the call table. What is on a handle is not
276 /// carried over: the attached aggregate starts subscribed to nothing, with
277 /// no value staged, no call sent and nothing served. Dropping it closes
278 /// only its own handles, and the store lives until the last handle of
279 /// every aggregate is dropped.
280 ///
281 /// Not `Clone`: `docs/design/ridl-loopback.md` records why.
282 #[must_use]
283 pub fn attach(&self) -> Loopback<E> {
284 Loopback::over(Arc::clone(&self.shared), self.catalog)
285 }
286
287 /// An additional reader handle on the same store.
288 #[must_use]
289 pub fn reader(&self) -> ReaderHandle<E> {
290 ReaderHandle::new(Arc::clone(&self.shared), self.catalog)
291 }
292
293 /// An additional writer handle on the same store, with its own staging
294 /// area and its own per channel sequence counters.
295 #[must_use]
296 pub fn writer(&self) -> WriterHandle<E> {
297 WriterHandle::new(Arc::clone(&self.shared), self.catalog)
298 }
299
300 /// An additional event source on the same store, with its own
301 /// subscription set and its own queue.
302 #[must_use]
303 pub fn source(&self) -> SourceHandle<E> {
304 SourceHandle::new(Arc::clone(&self.shared), self.catalog)
305 }
306
307 /// An additional event sink on the same store, with its own sequence
308 /// counter.
309 #[must_use]
310 pub fn sink(&self) -> SinkHandle<E> {
311 SinkHandle::new(Arc::clone(&self.shared), self.catalog)
312 }
313
314 /// An additional caller on the same store, with its own sequence counter.
315 /// Two callers on one provider therefore never collide on a sequence
316 /// number (driftsys/ridl#308).
317 #[must_use]
318 pub fn caller(&self) -> CallerHandle<E> {
319 CallerHandle::new(Arc::clone(&self.shared), self.catalog)
320 }
321
322 /// An additional handler on the same store.
323 ///
324 /// Every handler draws from the one queue of waiting calls, filtered by
325 /// what it has served: a handler that has served nothing is presented
326 /// every waiting call, and one that has served members is presented only
327 /// those. A claim belongs to the handler it was presented to.
328 #[must_use]
329 pub fn handler(&self) -> HandlerHandle<E> {
330 HandlerHandle::new(Arc::clone(&self.shared), self.catalog)
331 }
332
333 /// Advances the clock by `by`.
334 ///
335 /// The clock is a counter this method moves and nothing else moves. It
336 /// never reads wall-clock time, so a round trip over this runtime produces
337 /// the same timestamps on every run and on every machine. It saturates
338 /// rather than overflowing.
339 ///
340 /// # Panics
341 ///
342 /// When `by` is negative. A clock that ran backwards would stamp an
343 /// envelope before one already stamped.
344 pub fn advance(&mut self, by: Duration) {
345 lock(&self.shared).advance(by);
346 }
347
348 /// Supplies the value of a `fixed` (ridl §8), which is provisioned into a
349 /// runtime rather than published by application code.
350 ///
351 /// Until a `fixed` is provisioned, reading it answers
352 /// `ReadError::Contract(Contract::UnknownInteraction)`: the loopback holds
353 /// no value and, having no member table, cannot say anything narrower.
354 pub fn provision_fixed(&mut self, iface: InterfaceNo, ord: Ordinal, bytes: &[u8]) {
355 lock(&self.shared).provision_fixed(iface, ord, bytes);
356 }
357
358 /// Makes the next `Handler::settle` on this runtime fail with
359 /// `SettleError::TooLarge` and record no outcome.
360 ///
361 /// This is the one fault this runtime injects, and the one place a
362 /// `SettleError` other than `UnknownClaim` comes from. It exists because
363 /// the generated `dispatch` counts a claim only once the handler has
364 /// accepted its settlement, and nothing else in an in-process runtime can
365 /// make that path fail.
366 pub fn fail_next_settle(&mut self) {
367 lock(&self.shared).fail_next_settle();
368 }
369}
370
371// ---------------------------------------------------------------------------
372// The aggregate's twelve port implementations, each one a delegation.
373// ---------------------------------------------------------------------------
374
375impl<E: Encoding> Attached for Loopback<E> {
376 fn catalog(&self) -> &CatalogRef {
377 self.handles.reader.catalog()
378 }
379}
380
381/// One session has one encoding: every port of this runtime carries `E`, and
382/// an aggregate from [`Loopback::attach`] carries the same `E`.
383impl<E: Encoding> Encoded for Loopback<E> {
384 type Encoding = E;
385}
386
387impl<E: Encoding> Clock for Loopback<E> {
388 fn now(&self) -> Timestamp {
389 self.handles.reader.now()
390 }
391}
392
393impl<E: Encoding> SignalReader for Loopback<E> {
394 fn read(
395 &self,
396 iface: InterfaceNo,
397 ord: Ordinal,
398 out: &mut [u8],
399 ) -> Result<RawSample, ReadError> {
400 self.handles.reader.read(iface, ord, out)
401 }
402}
403
404impl<E: Encoding> FixedReader for Loopback<E> {
405 fn read_fixed(
406 &self,
407 iface: InterfaceNo,
408 ord: Ordinal,
409 out: &mut [u8],
410 ) -> Result<usize, ReadError> {
411 self.handles.reader.read_fixed(iface, ord, out)
412 }
413}
414
415impl<E: Encoding> ScannableSignals for Loopback<E> {
416 fn generation(&self, iface: InterfaceNo) -> u64 {
417 self.handles.reader.generation(iface)
418 }
419
420 fn scan(&self, marks: &mut [Watermark], out: &mut [Changed]) -> usize {
421 self.handles.reader.scan(marks, out)
422 }
423}
424
425impl<E: Encoding> CoherentSignals for Loopback<E> {
426 fn read_coherent(
427 &self,
428 iface: InterfaceNo,
429 ords: &[Ordinal],
430 out: &mut [u8],
431 samples: &mut [RawSample],
432 ) -> Result<usize, ReadError> {
433 self.handles.reader.read_coherent(iface, ords, out, samples)
434 }
435}
436
437impl<E: Encoding> SignalWriter for Loopback<E> {
438 fn set(&mut self, iface: InterfaceNo, ord: Ordinal, bytes: &[u8]) -> Result<(), WriteError> {
439 self.handles.writer.set(iface, ord, bytes)
440 }
441
442 fn invalidate(&mut self, iface: InterfaceNo, ord: Ordinal) -> Result<(), WriteError> {
443 self.handles.writer.invalidate(iface, ord)
444 }
445
446 fn touch(&mut self, iface: InterfaceNo, ord: Ordinal) -> Result<(), WriteError> {
447 self.handles.writer.touch(iface, ord)
448 }
449
450 fn commit(&mut self) {
451 self.handles.writer.commit();
452 }
453}
454
455impl<E: Encoding> EventSource for Loopback<E> {
456 fn subscribe(&mut self, iface: InterfaceNo, ords: &[Ordinal]) -> Result<(), SubscribeError> {
457 self.handles.source.subscribe(iface, ords)
458 }
459
460 fn unsubscribe(&mut self, iface: InterfaceNo, ords: &[Ordinal]) {
461 self.handles.source.unsubscribe(iface, ords);
462 }
463
464 fn next(&mut self, out: &mut [u8]) -> Result<Option<RawOccurrence>, ReadError> {
465 self.handles.source.next(out)
466 }
467}
468
469impl<E: Encoding> EventSink for Loopback<E> {
470 fn raise(
471 &mut self,
472 iface: InterfaceNo,
473 ord: Ordinal,
474 bytes: &[u8],
475 trace: Option<TraceContext>,
476 ) -> Result<(), RaiseError> {
477 self.handles.sink.raise(iface, ord, bytes, trace)
478 }
479}
480
481impl<E: Encoding> Caller for Loopback<E> {
482 fn command(
483 &mut self,
484 iface: InterfaceNo,
485 ord: Ordinal,
486 args: &[u8],
487 trace: Option<TraceContext>,
488 ) -> Result<Correlation, SendError> {
489 self.handles.caller.command(iface, ord, args, trace)
490 }
491
492 fn query(
493 &mut self,
494 iface: InterfaceNo,
495 ord: Ordinal,
496 args: &[u8],
497 trace: Option<TraceContext>,
498 ) -> Result<Correlation, SendError> {
499 self.handles.caller.query(iface, ord, args, trace)
500 }
501
502 fn ack(&mut self, c: Correlation) -> Option<Result<(), CallError>> {
503 self.handles.caller.ack(c)
504 }
505
506 fn reply(
507 &mut self,
508 c: Correlation,
509 out: &mut [u8],
510 ) -> Result<Option<Result<usize, CallError>>, ReadError> {
511 self.handles.caller.reply(c, out)
512 }
513
514 fn forget(&mut self, c: Correlation) {
515 self.handles.caller.forget(c);
516 }
517}
518
519impl<E: Encoding> Handler for Loopback<E> {
520 fn serve(&mut self, iface: InterfaceNo, ords: &[Ordinal]) -> Result<(), ServeError> {
521 self.handles.handler.serve(iface, ords)
522 }
523
524 fn next_claim(&mut self, out: &mut [u8]) -> Result<Option<Claim>, ReadError> {
525 self.handles.handler.next_claim(out)
526 }
527
528 fn settle(
529 &mut self,
530 claim: ClaimId,
531 outcome: Result<&[u8], CallError>,
532 ) -> Result<(), SettleError> {
533 self.handles.handler.settle(claim, outcome)
534 }
535}
536
537/// Each key goes to the handle that observes it: `Outcome` and `Slot` to the
538/// caller, `Event` to the source, and `Claim` to the handler.
539impl<E: Encoding> Wakeable for Loopback<E> {
540 fn wake_on(&self, what: Interest, waker: &Waker) {
541 match what {
542 Interest::Outcome(_) | Interest::Slot => self.handles.caller.wake_on(what, waker),
543 Interest::Event(_) => self.handles.source.wake_on(what, waker),
544 Interest::Claim(_) => self.handles.handler.wake_on(what, waker),
545 }
546 }
547}
548
549// ---------------------------------------------------------------------------
550// The threading model, asserted rather than documented (ADR-0021 decision 12).
551// ---------------------------------------------------------------------------
552
553const _: () = {
554 const fn assert_sync<T: Sync>() {}
555 const fn assert_send<T: Send>() {}
556
557 // Several threads may read one store at once.
558 assert_sync::<ReaderHandle>();
559 assert_send::<ReaderHandle>();
560
561 // One thread drives each of the rest.
562 assert_send::<WriterHandle>();
563 assert_send::<SourceHandle>();
564 assert_send::<SinkHandle>();
565 assert_send::<CallerHandle>();
566 assert_send::<HandlerHandle>();
567
568 // The aggregate is as `Send` as the handles it holds, whatever its
569 // encoding: `PhantomData` of a unit marker type adds no restriction.
570 assert_send::<Loopback>();
571 assert_send::<Loopback<ridl_rt::encoding::Proto3>>();
572};