zerodds-build
A prost-build-style build.rs helper for
ZeroDDS IDL: Config::new().include_dir(...).compile(&["x.idl"]).
Layer position
Layer 3 (schema), build-time-only dependency (put it under
[build-dependencies], not [dependencies]). Depends on
zerodds-idl-compose for the composition pipeline and on
zerodds-lang-rules + zerodds-lang-render for the emission.
Why this exists
zerodds-idlc generate --lang rust emits data types and codecs. This
crate integrates generation into Cargo and, on request, adds the DdsType
bridge. The composition in front of both is more than a parse: it preprocesses
#include/#define/#ifdef/#pragma, applies vendor key-pragmas
(#pragma keylist / DCPS_DATA_KEY / cats), patches default
extensibility/nestedness, and lowers XTypes TypeObjects. Before this crate
existed, five in-repo build.rs scripts (crates/spatial-dds,
crates/spatial-ros2, crates/corba-interop, two zerodds-examples/*)
called the low-level parse and emit directly and silently skipped all four
of those steps — no #include dependency tracking (editing an included
file did not trigger a rebuild), no vendor key-pragma support, no
default-extensibility, no TypeObject block. zerodds-build runs the same
composition library the CLI uses (zerodds-idl-compose), with a typed
configuration surface for build-time consumers. This is shared machinery,
not a claim that every CLI option has an identical build-script method.
Usage
[]
= "1.0.0-rc.7"
// build.rs
// src/lib.rs
include! splices the generated tokens into the including file, so
any lint allow the generated code needs goes on the wrapping module as an
outer attribute (as above).
Topic types
The generated type carries a codec — a pair of inherent functions over
zerodds_wire::Writer/Reader — but not the zerodds_dcps::DdsType
trait that DomainParticipant::create_topic takes. dds_type_impls(true)
writes that impl too:
// build.rs
participant.?;
The impl carries TYPE_NAME from the IDL scoped name, EXTENSIBILITY
from the annotation, HAS_KEY and the PLAIN_CDR2-BE key holder from the
@key members, the complete TypeIdentifier and TypeObject from the
same computation discovery uses, field_value for the SQL filter, and
all four codec pairs — XCDR2 and XCDR1, little-endian and big-endian —
from the same emitter under four layout keys.
It is off by default: it puts zerodds-dcps, zerodds-types and
zerodds-wire into the consumer's [dependencies], which is the whole
DCPS runtime, and this crate is also used by consumers that want the
codec and no DDS in the graph at all.
Where XCDR1 cannot frame a type, the four XCDR1 methods are left out
and the trait's own refusing defaults stand, with a comment in the
generated file naming the type. They are not delegated to the XCDR2
codec: that would put version-2 octets under a CDR_LE/CDR_BE
encapsulation header, which no peer can tell from a sample it is meant
to read.
Every .idl path passed to compile(), plus every file it transitively
#includes, is reported via cargo:rerun-if-changed — editing an included
.idl triggers a rebuild, not just editing the top-level file.
Config
Config method |
what it sets |
|---|---|
include_dir(dir) |
a directory the preprocessor searches for #include |
define(name, value) |
a preprocessor macro, as -D NAME[=VALUE] would |
out_dir(dir) |
where the generated files go (default: $OUT_DIR) |
default_extensibility(ext) |
what an unannotated aggregate is: Final, Appendable or Mutable (default: Appendable, XTypes 1.3 §7.3.1.2.1.8) |
typeobject(bool) |
the type_objects_* module of raw TypeObject bytes (default: on) |
header_comment(text) |
the header comment in the generated file |
type_identifier_constants(bool) |
a zerodds_types::TypeIdentifier constant per type (default: off) |
emit_cargo_directives(bool) |
whether compile() prints cargo:rerun-if-changed (default: on) |
dds_type_impls(bool) |
impl zerodds_dcps::DdsType per type (default: off) |
dialect(Dialect) |
reproduce one peer's answers to every wire axis at once (default: Dialect::default(), the specification) |
axis(AxisFlag, value) |
override one answer of the dialect; several calls apply in the order given |
The CLI exposes --out and --default-extensibility, as well as its own
--lang, --encoding and --endian options. Include directories, macro
definitions, Cargo rebuild directives and optional DdsType bridging are
configured through this crate's API, not corresponding CLI flags. See the
CLI reference.
compile_bundle(idls, name) composes several input files into one generated
module when they share declarations. compile(idls) emits per-input files.
Dialects and axis flags
Config::dialect and Config::axis are zerodds-idlc generate --dialect/--<axis> and wireprobe-zerodds --dialect/--<axis>, read
from the same table (crates/ast-layout/src/axes.rs), applied in the
same order: the dialect first, then each axis override. docs/wire-axis-flags.md
has every dialect's answer to every axis and the values axis takes.
// build.rs
Neither reaches the codec's field-by-field logic alone: dialect and
axis decide the LayoutKey the codec is planned under (offsets,
delimiters, length codes, PID framing, ...) and, for a source whose
@hashid or unannotated-scope reading a dialect settles, the same
three lowering options zerodds-idlc reads off DialectLowering. The
default — no dialect call, no axis call — is Dialect::default(),
the specification's clause-by-clause reading, which is what every
existing build.rs using this crate before this option existed already
got: adding neither leaves the generated module unchanged.
An unrecognised axis value is not a build-time panic: compile() and
compile_bundle() return Err(Error::Axis { flag, value, detail }),
naming which .axis(...) call was wrong — there is no command line
to refuse it at here, so the Result is where a typo in a value
string surfaces.
External storage
Generated Rust stores @external T members in Box<T>, and
@optional @external T members in Option<Box<T>>. This makes an optional
self-reference finite without changing its wire representation. Defaults,
codecs and the supported DdsType::field_value paths use the same storage
mapping. Consumers of older inline-generated fields must regenerate and
adjust their constructors. The compiled storage regression is in
the DdsType tests.
Not yet covered
Only the Rust backend is wired. The composition pipeline
(zerodds-idl-compose) is language-agnostic and zerodds-lang-rules
carries the rules for every target the CLI knows, so a zerodds-build-style
crate for another language would call the same compose() and the matching
rule set.
Rust maps and fixed<P,S> now have codecs. This does not imply that every
IDL construct has a Rust writer: generation returns an error for unsupported
members rather than leaving them out. The regression coverage lives in
the codec tests and
compiled DdsType tests.