The catalog descriptor
The catalog descriptor is a small binary file, one per unit, that lists what a
runtime needs to route and check the traffic of that unit's interfaces: the
interface numbers, the members and their ordinals, the maximum encoded size of
each payload, the timing bounds, and the retired interfaces. A program reads it
without the compiler and without decoding the IR. A unit is one ridl.toml
with a [package] table and every source package in its directory tree; the
unit's name is the manifest's name, which is also the name of its root source
package (the design record).
What it is for
A process that carries interactions between other processes — a gateway, a broker, a bridge between two transports, or the engine of a runtime — does not link the generated crate of every unit it carries. It still has to answer questions such as:
- Which interface does the number in this frame name, and is that interface retired?
- Is this member a signal, an event, a command or a query, and which payload does it carry?
- What is the largest payload this member can send, so that a buffer can be sized in advance, and so that a larger payload can be refused? The size counts the payload alone, without the envelope and the framing a transport adds.
- What timing bound does this member declare?
- Do both peers speak the same version of the unit?
The catalog descriptor answers each of these from one file per unit, read at startup.
What is built. The compiler writes the descriptor, the ridl-descriptor
crate reads and verifies it, and ridl describe prints it. No gateway, broker
or engine that reads it exists in this repository yet; those are the consumers
it is designed for. The generated Rust face does not read the file either: it
carries the catalog name and hash it needs as constants (see
The catalog and the face). Codegen plugins do not
read it; the codegen model carries the same facts (see
Writing a codegen plugin).
Producing it
--emit catalog writes one <unit>.catalog.binfb file per unit that declares
at least one interface, or a service with an inline body, in any of its source
packages:
ridl build examples/cabin --emit catalog --out-dir out
# writes out/veh.cabin.catalog.binfb
A unit with no interface gets no file. The flag combines with the others, so one build can write the Rust crate and the descriptor together. The other emit targets stay one file per source package; only the catalog is per unit.
Reading it
ridl describe verifies a descriptor and prints it as JSON:
ridl describe out/veh.cabin.catalog.binfb
The top of the output for examples/cabin, with the 32 bytes of the hash left
out. toolchain is the version of ridl that wrote the file, so it changes
with each release; the excerpt shows it as <version>:
{
"compatible": [],
"hash": [ ... ],
"interfaces": [
{
"members": [ ... ],
"name": "Cabin",
"number": 1,
"provisional": true,
"reserved_ordinals": []
},
...
],
"name": "veh.cabin",
"retired": [],
"toolchain": "<version>",
"version": 1
}
The member average, a query, as it appears in members:
{
"kind": "Query",
"limit": null,
"name": "average",
"ordinal": 4,
"payloads": [
{
"max_sizes": [
{ "bytes": 46, "cause": "Unspecified", "encoding": "FlatBuffers", "state": "Bounded" }
],
"role": "request",
"type_name": "Window"
},
{
"max_sizes": [
{ "bytes": 44, "cause": "Unspecified", "encoding": "FlatBuffers", "state": "Bounded" }
],
"role": "response",
"type_name": "Average"
}
],
"timing": { "max_us": "200000", "min_us": null, "mode": "Range" }
}
The keys are in alphabetical order, and an absent timing bound or limit is
null. A file that is not a valid descriptor, or that has a schema version this toolchain
does not read, makes ridl describe exit with code 2. The
CLI reference has the details.
What it contains
- The catalog: the unit
name; thehash, which identifies this version of the unit (see below);toolchain, the version ofridlthat wrote the file; the schemaversion; theinterfaces; theretiredinterfaces, each with its name and number, so a reader can refuse a peer that still uses one; andcompatible, the hashes of earlier catalogs of the unit that the toolchain judged compatible or carried over unchanged with this one (see The compatible catalogs). - Each interface: its
name, qualified relative to the unit (Cabinfor an interface of the root source package,cluster.Speedfor one of the source packageclusterbelow it); itsnumber, which is one space per unit; andprovisional, which is true while the number is not yet recorded in the unit'sinterfaces.lock, the one lock file beside the unit'sridl.toml.Cabinabove is provisional becauseexamples/cabinhas no lock file; a number becomes permanent whenridl lockrecords it.reserved_ordinalslists the ordinals thatreservedtombstones hold. The ridl reference, section 11, is the rule for the names and the numbers. - Each member: its
name, itsordinal(its position in the interface body), itskind, itspayloads, itstimingwhen the member declares one, and itslimit, the payload limit in bytes that a[limits]entry of the unit'sridl.tomlgives it, absent when no entry applies. - Each payload: its
role(valuefor a signal or afixed,occurrencefor an event,requestfor a command or a query,responsefor a query's reply), itstype_name, and onemax_sizesrow per encoding. A row isBounded, withbytesthe maximum encoded size of the payload, envelope and framing excluded; orUnbounded, withbytes0 and acausethat says why the compiler cannot bound it in that encoding. For example, a FlatBuffers row isUnboundedwhen the payload's largest encoding would exceed 4 GiB, the most that FlatBuffers' 32-bit offsets can address. An encoding with no row has no size computed by this toolchain. A stream payload, a request of zero or of several parameters, and an inlineT | Ereply have no row in any encoding. Apodbufrow is present for a payload with a podbuf root, sized byridl_ir::projection::podbuf::max_size, and a proto3 row is present only for a payload that is a struct, a union, a named scalar, an enum or an enum set, the kinds the proto3 schema gives a message of their own. A named scalar, an enum and an enum set are sized as their root message<Name>Box.
The descriptor does not contain type layouts, field lists, constraints other than the ones that bound a payload's size, contract clauses, or initial values. A reader that needs a payload's shape reads the schema that the wire backend emits.
The compatible catalogs
The hash identifies one version of the unit exactly. compatible lists, as
32-byte hashes, the earlier versions of the unit that ridl diff's classifier,
scoped to the unit, judged compatible with this one, newest first. When the
current hash is the newest published one, the catalog did not change, and the
published list is carried whatever the classifier's verdict. The current hash
is never in it. A reader built for one of those versions can be accepted by a
provider that serves this one, which is the use the frame specification makes
of the list (section 6.1). No runtime in this repository accepts an
attach on the strength of the list.
The list comes from the published baseline. ridl baseline records, for each
unit with an interface, a <unit>.catalogs file beside the snapshots, and
ridl build reads it. The list is empty when there is no baseline, when the
unit has no file, and after a breaking change that changes the catalog, which
restarts the chain. A file is used only when its newest hash is the hash
recomputed from the snapshots published beside it; a file that does not
describe those snapshots, such as one left behind by a hand-edited baseline,
restarts the chain instead. A breaking change that leaves the catalog hash as
it was, such as the removal of a declaration no interface reaches, keeps the
chain. A change in another unit counts for this unit only when an interface of
this unit reaches the changed declaration. ridlc build reads no baseline and
writes an empty list. The CLI reference
states the file format and the rules.
The catalog and the face
The catalog hash is a SHA-256 hash over the unit's interfaces, from every
source package of the unit, their numbers, and every declaration they reach, in
the unit or in another one. Doc comments, labels and deprecated do not change
it; any other change to an interface or to a type it uses does, so a change in
any source package of the unit that an interface reaches changes the one hash.
A [limits] entry in the unit's ridl.toml that gives an interaction its
payload limit changes the hash too, so adding, removing or editing such an
entry changes the hash of the interaction's unit. The design record's
hash section has the exact input.
The generated Rust face carries the same unit name and hash as its CATALOG.
When a face binds to a port, it compares its CATALOG with the catalog of
that port, and panics when they differ. So a face generated from a different
version of the unit than the one its runtime serves fails at bind time,
instead of misreading payloads. The face compares itself with its own
port only; it does not compare catalogs with the party at the other end. The
failures section of "Using the generated Rust
code" shows what that check does.
Stability
The file is a FlatBuffers buffer with the file identifier RDLC. Its schema
evolves by appending only: a field is added at the end of its table, and no
field is removed, reordered or retyped. A reader built against an older schema
ignores the fields it does not know. The version field is the schema version,
now 1; a reader refuses a version other than the one it was built for.
A Rust program reads the file with the ridl-descriptor crate, published on
crates.io. Its verify function checks the whole buffer before it returns a
view, so a later read cannot fail on a malformed offset. With its default
features off the crate is no_std with alloc and keeps verify, the
accessors and finish; the parts that build a descriptor from the IR need the
standard library and come with the std feature. The default dedup feature
makes finish write each repeated vtable, string and byte vector once. With it
off as well, a reader links no hashing crate, and finish writes a larger but
valid file.
Not built yet
The design also defines a system descriptor, one per deployment. It is not built; the design record lists it with the other parts that are not built.
Further reading
- The catalog descriptor — the design record: the schema, the hash input, the size states, and the verifier.