Using the generated Rust code

ridl build --emit rust turns a package into a Rust crate. This chapter shows how an application uses that crate: the types it holds, the two ways to call an interface — async and blocking — the Cargo features that select what the crate contains, and the failures a call can report. The flag itself is described in the CLI reference.

Most code snippets in this chapter are taken from examples/cabin/consumer/src/main.rs, a program that the repository's just demo gate builds and runs against the crate generated from examples/cabin/cabin.ridl. A snippet that is taken neither from that program nor from that schema is marked as an illustration in the sentence before it, and is not compiled. The interface the snippets call, and the types it uses, from that schema:

package veh.cabin

/// Cabin air temperature, in degrees.
type Temperature: integer [-40..85]

/// A percentage of full scale.
type Level: integer [0..100]

/// An averaging window, in milliseconds.
type Window: integer [0..100000]

/// The average a query replies with. Its range is wider than `Level`'s,
/// so a reply is not constrained to the scale the command takes.
type Average: integer [0..1000]

/// The health of a cabin subsystem.
enum Health {
  /// The subsystem works normally.
  OK = 0
  /// The subsystem works, with a fault that needs attention.
  WARN = 1
  /// The subsystem does not work.
  FAIL = 2
}

/// Raised when a subsystem leaves `OK`.
struct Warning {
  /// The warning code, as a percentage of full scale.
  code: Level
  /// The health the subsystem moved to.
  health: Health
}

/// The cabin's own interface: a signal, an event, a command and a query, so
/// the proof covers every interaction kind that carries a payload.
interface Cabin {
  /// The current cabin air temperature.
  signal temperature: Temperature @10ms
  /// Sent when a subsystem leaves `OK`.
  event warning: Warning @[100ms..1s]
  /// Sets the cabin level, below 100.
  command setLevel(level: Level) @[..50ms] [
    require level < 100
  ]
  /// The average level over the window, in milliseconds.
  query average(window: Window): Average @[..200ms] [
    require window > 0
    ensure result >= 0
  ]
}

What the crate contains

The build writes one <package>.rs file per package, a lib.rs that declares the module tree, and a Cargo.toml. A package veh.cabin is the module veh::cabin. The crate is named after the [package] name of the ridl.toml manifest, with each . replaced by _: examples/cabin declares the package veh.cabin, so its crate is veh_cabin. A [workspace] manifest names no package, and a build from one names the crate ridl_generated.

The cabin program imports the package module as api, the face of the interface Cabin as cabin, and the face's prelude:

use api::cabin;
use api::cabin::prelude::*;
use ridl_loopback::Loopback;
use ridl_rt::contract::{CatalogRef, Interface};
use ridl_rt::encoding::Podbuf;
use ridl_rt::payload::{Payload, Ref};
use ridl_rt::sample::Provenance;
use veh_cabin::veh::cabin as api;

prelude brings into scope the traits that carry the fixed methods of the face: ridl_rt::face::Bind (new), Events (next_event), Publish (commit), and, under the crate's std feature, Timeout (with_timeout and set_timeout); and, without a name, the interface's own Subscribe (subscribe_<event>) and Invalidate (invalidate_<signal>) traits. Each interface's prelude holds only the traits its face implements. These methods are trait methods rather than inherent methods so that a member of the interface can have one of these names. Without the use of the prelude, cabin::Client::new does not compile. The other imports are for the snippets below: Loopback is the runtime, CatalogRef and Interface name the catalog the face was generated from, and Provenance is read from a signal sample. A Loopback carries FlatBuffers, which is its default encoding; the face takes the encoding from the port it is bound to and names none itself.

The package module holds:

  • The domain types. A struct, an enum, an enum set, a union and a named scalar each become a Rust type. A constrained type is built with new, which checks the typl constraints and returns an error when the value breaks one; new_unchecked skips the check and is for a value already known to satisfy them. get returns the value of a named scalar.
  • The codecs of each payload type, one per encoding the manifest names under [backend.rust] encodings: flatbuffers, proto3, or both. Each codec is encode, verify, decode and a MAX_SIZE bound; with no list the module holds the types only. Each payload type also has a <T as Payload<FlatBuffers>>::Buffer type and a zeroed BUFFER value, sized by that type's own MAX_SIZE, and the same in Proto3. Every MAX_SIZE is finite: a declared type whose encoding has no finite bound, such as a string with no length bound, is refused in either encoding, and ridl build fails with an error that names the type and the cause; the proto3 codec record's known gaps give the limit this sets and its tracking issue. The face calls the codec; an application calls it only when it moves payloads by itself.
  • The interaction descriptors: one unit type per interface (here api::Cabin) that implements ridl_rt::contract::Interface and carries the interface's CATALOG, NUMBER and NAME. Each interface also implements ridl_rt::contract::Sizes<E> for each encoding the package carries, the size table of that codec for the interface: its reservations, its largest buffer (MAX_BUFFER_SIZE), and the claim and event buffers the face holds. Like the codecs, the descriptors are present only when the manifest names an encoding under [backend.rust] encodings.
  • The face: one module per interface that has one, named after it (here api::cabin), present under the same condition. The face is what the application calls.

The face

Each interface module holds the parts its own interactions need:

ItemPresent when the interface carriesUsed by
Clientany interactionthe consumer: reads, subscriptions, calls
Publishera signal or an eventthe producer: publishes values, raises events
Provider (trait)a command or a querythe provider: the application implements it
servea command or a querythe provider: settles the calls waiting on a port
Event (enum)an eventthe consumer: one variant per event
blocking (module)an event, a command or a querya thread that waits: blocking Client and serve

An interface that carries only fixed declarations gets no face module, and neither does an internal interface, whose descriptors are crate-visible (the design record gives the reason). A signal-only interface gets a Client and a Publisher and no blocking module, because a signal read returns at once.

An interface the face cannot carry gets none of these items and no interaction descriptor. In their place, the package module holds a __RIDL_NO_FACE_<NAME> constant whose documentation names the interface and the reason; the CLI reference lists the cases.

A call always sends one named type. A command or query with exactly one parameter of a named type sends that type. Any other parameter list gets an argument struct that the compiler names <Interface><Call>Params, with one field per parameter in declaration order, each carrying the parameter's documentation. A call with no parameter sends the same kind of struct with no field. A query whose reply is a tuple, or that declares no return type, gets a reply struct named <Interface><Call>Reply in the same way. These structs are generated types of the package module, and the face methods take and return them like any other payload. The wire backends emit the matching positional .proto messages and .fbs tables under the names assigned by lowering. When an induced payload has no finite bound in an encoding the manifest names — no finite FlatBuffers bound under flatbuffers; no finite proto3 bound, or a member proto3 cannot represent, under proto3 — its interface is skipped with the refusal constant described above rather than emitting a descriptor that names a missing codec.

The face opens no socket and holds no runtime. Each Client, Publisher and serve runs over a port that the application passes in, and a port comes from a runtime. The one runtime in this repository is ridl-loopback, which connects the faces of one process to each other. The cabin program binds every face to it:

    let rt = Loopback::new(CATALOG);
    let mut publisher = cabin::Publisher::new(rt.attach());
    let mut client = cabin::Client::new(rt.attach());
    let mut blocking_client =
        cabin::blocking::Client::new(rt.attach()).with_timeout(CLIENT_TIMEOUT);
    let mut handler = rt.handler();
    let mut provider = Cabin {
        levels: Vec::new(),
        average: api::Average::new(7).expect("7 is inside Average's range"),
    };

CATALOG is a constant of the program that holds the catalog the face was generated from, read from <api::Cabin as Interface>::CATALOG. Binding a face compares the port's catalog with it, and panics when they differ; see Failures. CLIENT_TIMEOUT is a Duration the program declares, and with_timeout comes from the Timeout trait of the prelude.

Implementing the provider

The application implements one method per command and per query. A command's method returns nothing, and a query's method returns the reply type:

struct Cabin {
    levels: Vec<i64>,
    average: api::Average,
}

impl cabin::Provider for Cabin {
    fn set_level(&mut self, level: &api::Level) {
        self.levels.push(level.get());
    }
    fn average(&mut self, _window: &api::Window) -> api::Average {
        self.average
    }
}

The method is called only with a request that passed every check: the payload verified, its typl constraints hold, and its require clauses are true. For a query, serve evaluates the ensure clauses on the reply before it sends it. The provider does not repeat those checks.

Async calls

The Client methods for a command and a query, next_event, and serve each return a named future. The futures are executor-agnostic: they take no dependency on an async runtime, and any executor can poll them. A call is sent when the method runs, not when its future is first polled.

Inside an async function, the calls read like this. This snippet is an illustration and is not compiled; port and handler stand for ports a runtime gives the application, and provider for a value that implements cabin::Provider:

// Illustration: any executor polls these futures.
let mut client = cabin::Client::new(port);
client.set_level(api::Level::new(42).expect("in range")).await?;
let average = client.average(api::Window::new(10).expect("in range")).await?;

// On the provider side, `serve` settles calls until the handler port fails.
// Its output is `Result<Infallible, ProviderError>`: it never resolves to `Ok`.
let failure = cabin::serve(handler, &mut provider).await;

No executor is required either. The cabin program polls the same futures by hand, the way a frame loop does. cx is a Context built from the waker ridl_rt::task::noop_waker(), and poll_once is a function of the program that polls a future once with it:

    {
        let mut serve = cabin::serve(&mut handler, &mut provider);
        let mut call = client.set_level(api::Level::new(42).expect("42 is inside Level's range"));
        // Step 1: the call was sent when `set_level` ran, and nothing has
        // served it yet.
        assert!(poll_once(&mut call, &mut cx).is_pending());
        // Step 2: one pass of the provider side settles it.
        assert!(poll_once(&mut serve, &mut cx).is_pending());
        // Step 3: the acknowledgment is taken.
        assert_eq!(poll_once(&mut call, &mut cx), Poll::Ready(Ok(())));
    }

ridl_rt::task exists only under ridl-rt's std feature, which the generated crate's own std feature turns on. A frame loop built without it uses core::task::Waker::noop() instead, which Rust 1.85 and later provide.

A serve future settles at most 32 calls in one poll. When it reaches that limit, it wakes its own waker and returns Pending, so that an executor can run other tasks before it polls the serve future again. Under noop_waker that wake is lost, so a frame loop that polls once per frame settles at most 32 calls per frame.

The future of a command or a query call, and the serve future, panic when they are polled again after they resolved. The next_event future does not: polled again, it waits for the next occurrence.

Blocking calls

The blocking module holds the same Client and serve for code that waits on a thread instead of polling. Each blocking call is ridl_rt::task::block_on over the async call, so the two forms cannot differ in what a call does; the blocking form only adds the wait and a timeout.

A blocking client takes its timeout with with_timeout (or set_timeout). The default is no timeout. A call returns its outcome, or the timeout result when the timeout passes first. A signal read and a subscribe_* call return at once, as in the async client.

Blocking serve takes the handler, the provider and an optional timeout. It returns Ok(()) when the timeout passes, so a thread that also does other work can call it in a loop. SERVE_PASS is a Duration of 50 ms the program declares:

/// Serves `provider` over `handler` on the calling thread until `done` is
/// set: `blocking::serve` returns `Ok(())` at each pass's timeout, and the
/// loop calls it again. A failure of the handler port ends the loop with it.
fn serve_until_done(
    handler: &mut ridl_loopback::HandlerHandle,
    provider: &mut Cabin,
    done: &AtomicBool,
) -> Result<(), ridl_rt::error::ProviderError> {
    while !done.load(Ordering::Acquire) {
        cabin::blocking::serve(&mut *handler, &mut *provider, Some(SERVE_PASS))?;
    }
    Ok(())
}

The cabin program runs that loop on a second thread and calls the blocking client on the first. DoneOnDrop is a type of the program that sets done when it is dropped, so that the serving loop also ends when the first thread panics:

    let done = AtomicBool::new(false);
    let served = std::thread::scope(|scope| {
        let serving = scope.spawn(|| serve_until_done(&mut handler, &mut provider, &done));
        let _ends_the_loop = DoneOnDrop(&done);
        let acknowledged =
            blocking_client.set_level(api::Level::new(43).expect("43 is inside Level's range"));
        done.store(true, Ordering::Release);
        assert_eq!(acknowledged, Ok(()));
        serving.join().expect("the serving thread does not panic")
    });

With no timeout, a blocking call returns only with its outcome, blocking next_event only with an event or a read failure, and blocking serve only with a failure. At the timeout, blocking next_event returns Ok(None).

Converting a payload between encodings

With [backend.rust] transcode = true in the manifest, the generated crate carries one function for each ordered pair of distinct encodings that [backend.rust] encodings lists, on each payload type that has a Payload implementation under both. The manifest must list at least two encodings.

[backend.rust]
encodings = ["flatbuffers", "proto3"]
transcode = true

The function is named transcode_<s>_to_<d>, where <s> is the source encoding and <d> the target, spelled as in the manifest with - replaced by _. For a type Speed the crate holds:

impl Speed {
    pub fn transcode_flatbuffers_to_proto3<'o>(
        bytes: &[u8],
        out: &'o mut [u8],
    ) -> Result<
        ridl_rt::payload::Ref<'o, Self, ridl_rt::encoding::Proto3>,
        ridl_rt::payload::TranscodeError,
    >;
    pub fn transcode_proto3_to_flatbuffers<'o>(/* the same parameters */)
        -> Result</* a Ref in the FlatBuffers encoding */, /* the same error */>;
}

The function verifies bytes under the source encoding, decodes the value, and encodes it under the target encoding into out. It calls ridl_rt::payload::transcode, which allocates no buffer of its own; the type's decode may allocate, as for a string field. An out that is too small is TranscodeError::Encode with EncodeError::Capacity, and bytes that do not verify are TranscodeError::Verify. A function exists only where the type has a Payload implementation under both encodings of the pair.

A podbuf buffer starts on an 8-byte boundary. A caller passes an out for a podbuf target, and bytes for a podbuf source, that starts on one: the type's <T as Payload<Podbuf>>::BUFFER is an Aligned array, and ridl_rt::podbuf::Aligned wraps any other array. An out that is not aligned is EncodeError::Unaligned, and bytes that are not aligned do not verify. A map is written in key order, so a value whose map is in another order decodes from a podbuf source in key order.

Cargo features

The generated Cargo.toml declares two features, both on by default:

FeatureEnables
stdThe blocking module of every face, the Timeout trait in the prelude of each interface that has a blocking module, and ridl-rt/std. With it off, only the async face remains.
validate-patternThe check of a typl match pattern in a constructor, through the regex crate. With it on, the crate links the standard library. With it off, new does not check patterns; range and length checks are not affected.

With std and validate-pattern both off, the generated crate and ridl-rt are both no_std and build for a target that has no standard library. Its lib.rs declares this with #![cfg_attr(not(any(feature = "std", feature = "validate-pattern")), no_std)]. The crate still needs an allocator: under the same condition its lib.rs links alloc under the name std (extern crate alloc as std;), because the package files name ::std::string::String and ::std::vec::Vec for strings, bytes, arrays and maps. Single-file mode writes no lib.rs, so the crate that includes the file decides. A crate that is always no_std declares extern crate alloc as std; in its own root. A crate that is no_std only under a condition declares it under the same condition: where the alias is declared, ::std names alloc, so a path that only the standard library has, such as ::std::sync::LazyLock, no longer resolves.

A target where regex is too large, or one that does not need the blocking face, turns the defaults off and selects what it needs. This Cargo.toml fragment is an illustration and is not compiled:

[dependencies]
veh_cabin = { path = "generated", default-features = false }

The generated crate depends on ridl-rt with one feature per encoding that the manifest names under [backend.rust] encodings — flatbuffers, proto3, podbuf, or a combination, which the codecs need — and no feature for a types-only build. An application that names ridl-rt itself, for example to use ridl_rt::task::noop_waker or the error types, selects from its features:

ridl-rt featureDefaultEnables
flatbuffersoffThe flatbuffers module the generated codec calls.
stdoffThe standard library; the task module: block_on, noop_waker and flag_waker; the face::Timeout trait; and the propagation hook of the trace module, set_propagation.
proto3offThe proto3 module the generated proto3 codec calls.
podbufoffThe podbuf module the generated podbuf codec calls: the preamble check, the in-place reads, the writer and the Aligned buffer type.

ridl-rt has no dependency in any feature combination. It builds for wasm32, and its minimum Rust version is 1.88. The generated crate declares edition 2021 and rust-version = "1.88", and builds with Rust 1.88 or later with every cargo feature on or with default features off. To keep Rust 1.88, resolve its dependencies as Stability describes. When you build the generated crate as its own workspace root, its edition 2021 selects resolver 2, which by default does not read rust-version. Resolve with CARGO_RESOLVER_INCOMPATIBLE_RUST_VERSIONS=fallback; cargo then selects the dependency releases that Rust 1.88, the manifest's rust-version, supports, as just compat-check does.

The wasm-exports feature

With [backend.rust] wasm = true in the manifest, or codec = true on another backend table, which implies it, the generated Cargo.toml declares a third feature, wasm-exports, off by default, which turns std on. It compiles the exports of the crate's wasm module when the target is wasm32, and nothing on any other target. The manifest declares no crate type, so a Rust program that depends on the crate links no export. The WebAssembly module is built from the crate's directory with:

CARGO_PROFILE_RELEASE_OPT_LEVEL=z CARGO_PROFILE_RELEASE_LTO=true \
CARGO_PROFILE_RELEASE_PANIC=abort \
cargo rustc --release --target wasm32-unknown-unknown --crate-type cdylib \
  --features wasm-exports

The module exports ridl_alloc, ridl_free, ridl_catalog_hash, ridl_verify and ridl_type_id. ridl_type_id returns the type id of a payload type named by its qualified name. It returns -1 for a name the list does not hold, an empty name, bytes that are not UTF-8, a null pointer with a length, a length above isize::MAX, and a pointer and length whose sum overflows the address space. A range past the end of the module's memory traps, as any read past it does.

The three variables are the recommended size settings, and cargo reads them from the environment. The module is target/wasm32-unknown-unknown/release/<crate name>.wasm.

Failures

A call reports its failure as a value. ClientError, CallError, Contract, Transport and ProviderError are in ridl_rt::error; the port errors, such as SendError and ReadError, are in ridl_rt::port.

A command or query call resolves to Result<_, ClientError>:

  • ClientError::Send — the call was not sent. A require clause that is false is reported here, as SendError::Contract(PreconditionFailed), because the client evaluates the require clauses before it sends anything.
  • ClientError::Call — the call was sent, and its outcome is a failure. CallError::Contract is a contract error the provider side reported: InvalidValue (the payload breaks its typl constraints), PreconditionFailed, ContractBroken (an ensure clause is false) or UnknownInteraction (the peers disagree on an interface number or an ordinal). CallError::Transport is an infrastructure failure: Timeout, Undelivered, Down, Corrupt or Busy.
  • ClientError::Read — the port failed while the reply was read.

When a call's max bound passes, a sent command resolves to Transport::Undelivered and a sent query to Transport::Timeout. The bound is measured on the port's clock, so it depends on the runtime. The clock of ridl-loopback moves only when the program calls Loopback::advance, so over it a bound passes only after the program advances the clock past it; until then, a call that is never served returns only at the blocking client's own timeout.

A signal read returns Result<Sample<T>, ReadError>. serve resolves to ProviderError::Serve when the handler refuses the interface's members, which it reports on the first poll, and to ProviderError::Claim when the handler port fails while a call is read. The calls it settled before a failure stay settled.

A catalog mismatch panics. Binding a Client or a Publisher, and starting serve, compare the port's catalog — the unit name and the catalog hash — with the CATALOG the face was generated from. When they differ, the face was generated from a different version of the unit than the one the runtime serves, and the bind panics with a message that names the interface and both catalogs. A program that must not panic compares the two itself before it binds. This snippet is an illustration and is not compiled; port stands for the port the program is about to bind:

use ridl_rt::contract::Interface; // carries `CATALOG`
use ridl_rt::port::Attached;      // carries `catalog()`

if port.catalog() != <api::Cabin as Interface>::CATALOG {
    // refuse the port
}

The catalog and its hash are described in The catalog descriptor.

TypeScript

ridl build --emit typescript writes the domain types of a package as one .ts file: branded scalars, interfaces, enums, enum sets, discriminated unions and init functions. It emits no face and no feature selection, so there is no async or blocking API to choose between.

With [backend.typescript] codec = true it also writes <base>.codec.ts, for every package that has a payload root with a codec (its own, or one of another package that its interfaces name): one encode<Name> and one decode<Name> function per payload type, which take and return the plain objects of <base>.ts and call the object API that flatc generates; one table per interface, which gives each interaction its interface number, its ordinal, its kind and the functions of its payloads; and the catalog's name and hash, the payload type names and the rule texts that the wasm codec takes at load. ridl does not run flatc. Build the files in two steps:

ridl build --emit typescript --emit flatbuffers --out-dir <out-dir>
flatc --ts --gen-object-api -o <out-dir> <out-dir>/<base>.fbs

The codec file imports flatc's output from ./<package path>.js (for veh.cabin, ./veh/cabin.js), so flatc's -o is the directory of the TypeScript files. When a payload type of an interaction is a named scalar or an enum set of another package (ridl.std.Name as the request of a query, for example), the codec file goes through that package's own codec file, <package>.codec.ts, which the build writes beside it; flatc then runs on that package's schema too, in the same directory:

flatc --ts --gen-object-api -o <out-dir> <out-dir>/<base>.fbs <out-dir>/<package>.fbs

A package with no such payload root, and ridl.std when no codec file imports its module, gets no codec file. The converters cover every typl kind: a union by its kind and value, a tuple by its named fields, an array, a map by its entries, an optional field (absent when the buffer leaves it out; an optional array, bytes or map field reads back as empty, because the object API cannot leave a vector out of the buffer), and an induced tuple of an interaction, which the codec file exports as an interface (export type X = Record<string, never> for one with no field). An optional array element, map key or map value is refused, as the Rust codec refuses it. A named scalar or an enum set of another package converts by a type assertion, and a payload type of another package that is one goes through that package's own codec file; a payload type that reaches a struct, a union or an enum of another package is refused with an error that names both. An internal payload type has no encode/decode, because <base>.ts does not export its type. The codec table design record gives the rules in full.

Documentation in the generated code

The doc comment of a declaration or a member (see Documenting your API) becomes the doc comment of the item generated from it: rustdoc in the Rust crate, TSDoc in the TypeScript module. The generated doc also lists the item's rules: the facts the schema states about the values the item admits and the way it is used. They are the same lines the editor's hover shows in its Contract list.

A generated doc holds these parts, in this order, and leaves out each part that is empty:

PartRustTypeScript
doc text/// lines; each link an intra-doc linkthe comment's text; each link {@link Name}
rulesa # Rules section, one bullet per rule@remarks, then a Rules: list
typl tagsnone@unit, @range, @bounds
@seea # See also section, one bullet eachone @see tag each
@sinceone Available since <version>. line eachone @since tag each
@deprecatedthe #[deprecated] attributeone @deprecated tag

A link, or an @see target, whose target has no item in the same output is written as a code span of the target's qualified name, such as veh.common.Speed, so that no doc in the crate or the module that ridl build emits holds a link that does not resolve. Both backends link only to a declaration of the package itself or to a declaration of another package that this package reaches through a type. A target that this package does not reach through a type is written as a code span. In both languages, so is an internal declaration, a constant the backend does not emit (such as a bytes constant), and a member of a named scalar or of a constant, which has no member item. In Rust it is also the case for an event, a fixed, an interface with no face module, a target whose path Rust spells with a raw identifier (r#move), and an item whose name the module of a child package takes, which a program reaches only through the hidden __ridl_package module. In TypeScript it is also the case for an interface, an interaction and a union arm, which the module holds no item for, and for a declaration of another package that the module does not import. @labels is not rendered.

A Rust link uses the short form [Speed] when that text is the target's Rust name and the target is in the module that holds the doc. When a Markdown reference definition uses the same label, the generated shortcut uses code formatting to preserve the resolved target. Other links use an absolute path from the crate root, such as crate::veh::common::Speed, which resolves in the crate ridl build emits. A program that brings a generated file in with include! inside a module of its own moves the items away from those absolute paths, so rustdoc reports those links as unresolved.

In the Rust face, the doc of an interaction goes on these methods: for a signal, the method of the async Client, of the blocking Client and of the Publisher; for a command or a query, the method of the async Client, of the blocking Client and of the Provider trait; for an event, the subscribe_<event> method of the async and of the blocking Client, and the method of the Publisher. The Client and Provider methods also carry the interaction's #[deprecated]; a Publisher method and a subscribe_<event> method carry the doc without #[deprecated]. The variant of an event in the Event enum carries only a sentence the backend writes. A method's doc is the interaction's doc text, then a sentence the backend writes about the method itself, then the # Rules section, then, for a command or a query, a # Parameters section (one bullet per parameter with its name and its doc, and the parameter's rules as nested bullets), then the # See also section and the Available since lines. The doc of an interface goes on its face module, in the same order around the module's own sentence.

What the generated code checks

The rules lists state the contract, not what the code checks. On each item the backend emits, every rule the schema declares is listed, in the same words in both languages, whether or not the generated code checks it. The sentence the backend writes about a method describes what that method does, its checks included: the doc of a named scalar's new says that it returns an error for a value that breaks a constraint the generated code checks, the doc of a Client call method says what happens when a require clause fails, and the doc of a query's Provider method says that serve discards a reply that breaks an ensure clause.

This table records, for each kind of rule, the generated item that checks it:

RuleRustTypeScript
rangechecked by new, TryFrom and verifynot checked
stepchecked by new, TryFrom and verifynot checked
unitnot checkednot checked
lengthnew, TryFrom, verify; encode for anonymous fieldsnot checked
patternchecked by new, TryFrom and verify, under a featurenot checked
collection boundchecked by encode and verifynot checked
timingnot checkednot checked
response boundmax checked by the future of a Client callnot checked
errorsnot checkednot checked
requirechecked by the call methods of Client and by servenot checked
ensurechecked by servenot checked

In the Rust column:

  • new and TryFrom are the constructors of a named scalar. verify is the FlatBuffers codec's check of a received payload. It also checks a field whose type is written inline, which has no constructor. encode also checks anonymous string and byte lengths and variable-length collection counts. Exact array counts are enforced by their Rust type. Other field constraints and map-key uniqueness are checked by verify.
  • The pattern is checked only when the crate is built with the validate-pattern feature (see Cargo features).
  • A call's future, in the async and in the blocking Client, resolves to a failure when the response bound's max passes (see Failures). Its min is not checked.
  • A query that declares an error type gets no face, so no generated item carries the errors rule.
  • A Client call method evaluates the require clauses before it sends. serve evaluates them before it calls the Provider, and evaluates the ensure clauses on the reply before it sends the reply. The Client does not evaluate ensure.
  • verify and the face exist only when the manifest names flatbuffers or proto3 under [backend.rust] encodings (see What the crate contains).

The TypeScript module holds types, constants, the <Name>Bits objects and init functions only, and none of them checks a value.

An item that the generated code needs but a program does not name carries no doc. In Rust it is pub(crate), such as the constraint check behind new and the codec's functions for each table, or pub under #[doc(hidden)] where code outside the crate must reach it, such as the descriptor of each interaction, which a runtime reads. Two private Rust constants are notes rather than such items: __RIDL_NO_FACE_<NAME>, left where an interface's face was not emitted, and __RIDL_FB_NO_CODEC_<Name>, left where a declaration's FlatBuffers codec was not emitted. Each carries a doc that says what was not emitted and why, for a reader of the generated source; rustdoc does not show them.

Further reading

  • The interaction face — the design record of everything the Rust backend emits for an interface.
  • ridl-rt — the design record of the runtime library: its ports, features and error types.