zerodds-build
A
build.rshelper for ZeroDDS IDL: one Rust module per.idl, with#includedependency tracking so cargo re-runs when a header changes.
- Source: crates/zerodds-build
- API docs: docs.rs/zerodds-build
A prost-build-style build.rs helper for ZeroDDS IDL: Config::new().include_dir(...).compile(&["x.idl"]).
What it replaces
The supported way to generate Rust types from IDL inside a Cargo build. It replaces calling the CLI from a build.rs and the derive macro of the former zerodds-cdr-derive; a topic type is declared in IDL and gets its DdsType impl from dds_type_impls(true) (below). Migration notes: CHANGELOG, [1.0.0-rc.7], section “Removed”.
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 it 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 and nestedness, and lowers XTypes TypeObjects. build.rs scripts that called the low-level parse and emit directly skipped those steps silently: no #include dependency tracking, no vendor key pragmas, no default extensibility, no TypeObject block. zerodds-build runs the same composition library the CLI uses, 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
[build-dependencies]
zerodds-build = "1.0.0-rc.7"
// build.rs
fn main() -> Result<(), zerodds_build::Error> {
zerodds_build::Config::new()
.include_dir("idl")
.compile(&["idl/Robot.idl"])
}
// src/lib.rs
#[allow(non_snake_case, unused_imports)]
mod robot {
include!(concat!(env!("OUT_DIR"), "/Robot.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).
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.
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
fn main() -> Result<(), zerodds_build::Error> {
zerodds_build::Config::new()
.dds_type_impls(true)
.compile(&["idl/Robot.idl"])
}
participant.create_topic::<Robot>("entity.robot")?;
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.
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 |
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 in zerodds-ast-layout and 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
fn main() -> Result<(), zerodds_build::Error> {
zerodds_build::Config::new()
.dialect(zerodds_ast_layout::Dialect::Cyclone)
.axis(zerodds_ast_layout::AxisFlag::LengthCodes, "compact")
.compile(&["idl/Robot.idl"])
}
The default — no dialect call, no axis call — is Dialect::default(), the specification's clause-by-clause reading. 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.
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.
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.
Generation returns an error for an unsupported member rather than leaving it out.
zerodds-build
Ein Helfer für
build.rsfür ZeroDDS-IDL: ein Rust-Modul je.idl, mit Verfolgung der#include-Abhängigkeiten, damit cargo neu baut, wenn sich ein Header ändert.
- Quelle: crates/zerodds-build
- API-Docs: docs.rs/zerodds-build
Ein Helfer für build.rs nach dem Vorbild von prost-build für ZeroDDS-IDL: Config::new().include_dir(...).compile(&["x.idl"]).
Was er ersetzt
Der unterstützte Weg, in einem Cargo-Build Rust-Typen aus IDL zu erzeugen. Er ersetzt den Aufruf der CLI aus einem build.rs und das Derive-Makro der früheren Crate zerodds-cdr-derive; ein Topic-Typ wird in IDL deklariert und bekommt sein DdsType-Impl über dds_type_impls(true) (siehe unten). Migrationshinweise: CHANGELOG, [1.0.0-rc.7], Abschnitt „Removed“.
Schichten-Position
Layer 3 (Schema), nur zur Bauzeit gebraucht: unter [build-dependencies] eintragen, nicht unter [dependencies]. Hängt für die Komposition von zerodds-idl-compose ab und für die Ausgabe von zerodds-lang-rules und zerodds-lang-render.
Warum es ihn gibt
zerodds-idlc generate --lang rust erzeugt Datentypen und Codecs. Diese Crate bindet die Generierung in Cargo ein und fügt auf Wunsch die Brücke zu DdsType hinzu. Die Komposition davor ist mehr als ein Parsen: Sie verarbeitet #include/#define/#ifdef/#pragma, wendet die Key-Pragmas der Hersteller an (#pragma keylist / DCPS_DATA_KEY / cats), ergänzt Standard-Erweiterbarkeit und -Verschachtelung und senkt XTypes-TypeObjects ab. build.rs-Skripte, die Parser und Ausgabe direkt aufriefen, übersprangen diese Schritte stillschweigend: keine Verfolgung von #include-Abhängigkeiten, keine Key-Pragmas, keine Standard-Erweiterbarkeit, kein TypeObject-Block. zerodds-build nutzt dieselbe Kompositionsbibliothek wie die CLI, mit einer typisierten Konfiguration für Nutzer zur Bauzeit. Das ist gemeinsame Mechanik, keine Zusage, dass jede Option der CLI eine gleichwertige Methode im Build-Skript hat.
Verwendung
[build-dependencies]
zerodds-build = "1.0.0-rc.7"
// build.rs
fn main() -> Result<(), zerodds_build::Error> {
zerodds_build::Config::new()
.include_dir("idl")
.compile(&["idl/Robot.idl"])
}
// src/lib.rs
#[allow(non_snake_case, unused_imports)]
mod robot {
include!(concat!(env!("OUT_DIR"), "/Robot.rs"));
}
include! fügt die erzeugten Tokens in die einbindende Datei ein. Jede Lint-Freigabe, die der erzeugte Code braucht, steht deshalb als äußeres Attribut am umschließenden Modul (wie oben).
Jeder .idl-Pfad, der an compile() geht, und jede Datei, die er transitiv per #include einbindet, wird über cargo:rerun-if-changed gemeldet — auch eine Änderung an einer eingebundenen .idl löst einen neuen Build aus, nicht nur eine an der obersten Datei.
Topic-Typen
Der erzeugte Typ trägt einen Codec — ein Paar inhärenter Funktionen über zerodds_wire::Writer/Reader —, aber nicht den Trait zerodds_dcps::DdsType, den DomainParticipant::create_topic verlangt. dds_type_impls(true) schreibt dieses Impl dazu:
// build.rs
fn main() -> Result<(), zerodds_build::Error> {
zerodds_build::Config::new()
.dds_type_impls(true)
.compile(&["idl/Robot.idl"])
}
participant.create_topic::<Robot>("entity.robot")?;
Das Impl trägt TYPE_NAME aus dem qualifizierten IDL-Namen, EXTENSIBILITY aus der Annotation, HAS_KEY und den PLAIN_CDR2-BE-Key-Holder aus den @key-Membern, den vollständigen TypeIdentifier und das TypeObject aus derselben Berechnung, die die Discovery nutzt, field_value für den SQL-Filter und alle vier Codec-Paare — XCDR2 und XCDR1, Little-Endian und Big-Endian — aus demselben Emitter unter vier Layout-Schlüsseln.
Es ist standardmäßig aus: Es bringt zerodds-dcps, zerodds-types und zerodds-wire in die [dependencies] des Nutzers, also die ganze DCPS-Laufzeit, und diese Crate wird auch von Nutzern verwendet, die den Codec wollen und gar kein DDS im Abhängigkeitsgraphen.
Wo XCDR1 einen Typ nicht rahmen kann, werden die vier XCDR1-Methoden weggelassen; es gelten die ablehnenden Standardimplementierungen des Traits, und ein Kommentar in der erzeugten Datei nennt den Typ. Sie werden nicht an den XCDR2-Codec delegiert: Das legte Oktette der Version 2 unter einen Encapsulation-Header CDR_LE/CDR_BE, und kein Peer könnte das von einem Sample unterscheiden, das er lesen soll.
Config
Methode von Config |
was sie setzt |
|---|---|
include_dir(dir) |
ein Verzeichnis, in dem der Präprozessor nach #include sucht |
define(name, value) |
ein Präprozessormakro, wie es -D NAME[=VALUE] setzen würde |
out_dir(dir) |
wohin die erzeugten Dateien kommen (Standard: $OUT_DIR) |
default_extensibility(ext) |
was ein nicht annotiertes Aggregat ist: Final, Appendable oder Mutable (Standard: Appendable, XTypes 1.3 §7.3.1.2.1.8) |
typeobject(bool) |
das Modul type_objects_* mit den rohen Bytes der TypeObjects (Standard: an) |
header_comment(text) |
der Kopfkommentar in der erzeugten Datei |
type_identifier_constants(bool) |
eine Konstante zerodds_types::TypeIdentifier je Typ (Standard: aus) |
emit_cargo_directives(bool) |
ob compile() cargo:rerun-if-changed ausgibt (Standard: an) |
dds_type_impls(bool) |
impl zerodds_dcps::DdsType je Typ (Standard: aus) |
dialect(Dialect) |
die Antworten eines Peers auf alle Wire-Achsen auf einmal nachbilden (Standard: Dialect::default(), die Spezifikation) |
axis(AxisFlag, value) |
eine Antwort des Dialekts überschreiben; mehrere Aufrufe gelten in der angegebenen Reihenfolge |
compile_bundle(idls, name) setzt mehrere Eingabedateien zu einem erzeugten Modul zusammen, wenn sie Deklarationen teilen. compile(idls) erzeugt eine Datei je Eingabe.
Dialekte und Achsen-Flags
Config::dialect und Config::axis entsprechen zerodds-idlc generate --dialect/--<axis> und wireprobe-zerodds --dialect/--<axis>, gelesen aus derselben Tabelle in zerodds-ast-layout und in derselben Reihenfolge angewandt: erst der Dialekt, dann jede Überschreibung einer Achse. docs/wire-axis-flags.md enthält die Antwort jedes Dialekts auf jede Achse und die Werte, die axis annimmt.
// build.rs
fn main() -> Result<(), zerodds_build::Error> {
zerodds_build::Config::new()
.dialect(zerodds_ast_layout::Dialect::Cyclone)
.axis(zerodds_ast_layout::AxisFlag::LengthCodes, "compact")
.compile(&["idl/Robot.idl"])
}
Der Standard — kein Aufruf von dialect, keiner von axis — ist Dialect::default(), die Lesart der Spezifikation Klausel für Klausel. Ein unbekannter Achsenwert führt nicht zu einem Panic im Build: compile() und compile_bundle() geben Err(Error::Axis { flag, value, detail }) zurück und nennen den fehlerhaften Aufruf .axis(...).
Externe Speicherung
Erzeugtes Rust speichert @external T-Member in Box<T> und @optional @external T-Member in Option<Box<T>>. Dadurch wird ein optionaler Selbstverweis endlich, ohne seine Darstellung auf der Leitung zu ändern.
Noch nicht abgedeckt
Nur das Rust-Backend ist angebunden. Die Kompositionspipeline (zerodds-idl-compose) ist sprachunabhängig, und zerodds-lang-rules trägt die Regeln für jedes Ziel, das die CLI kennt; eine Crate nach Art von zerodds-build für eine andere Sprache riefe dasselbe compose() und den passenden Regelsatz auf.
Für einen nicht unterstützten Member gibt die Generierung einen Fehler zurück, statt ihn wegzulassen.