zerodds-build

A build.rs helper for ZeroDDS IDL: one Rust module per .idl, with #include dependency tracking so cargo re-runs when a header changes.

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.rs für ZeroDDS-IDL: ein Rust-Modul je .idl, mit Verfolgung der #include-Abhängigkeiten, damit cargo neu baut, wenn sich ein Header ändert.

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.