IDL & codegen
One OMG IDL 4.2 grammar drives type generation for seven languages. Same .idl file, same wire bytes, idiomatic native types in every binding.
Learn — what is IDL?
OMG IDL — the Interface Definition Language — is a language-agnostic way to describe data types and remote interfaces. It first shipped in 1991 alongside CORBA. The current revision, IDL 4.2 (ISO/IEC 19516), drops legacy CORBA-only constructs and adds a clean set of annotations for modern middleware.
An IDL file looks roughly like a C header: it declares structs, unions, enums, interfaces and modules. The IDL compiler reads that file and generates language-native code — Rust types, C++ headers, Java classes, Python modules — that all encode and decode the same byte layout on the wire.
IDL is what makes DDS, CORBA, DDS-RPC and DDS-XRCE language-agnostic. Write the contract once, generate for every consumer. The wire format (XCDR1, XCDR2 for DDS; CDR for CORBA) is defined by the standard, not by the language.
@topic
@appendable
struct SensorReading {
@key long sensor_id;
double value;
@optional sequence<string> tags;
};
Why — why IDL matters
Three reasons IDL is non-negotiable for a serious middleware stack. First, language coverage: without a single source of truth, a multi-language deployment turns into N hand-written serializers, all subtly out of sync. With IDL, every binding renders the same type from the same source — and a CI step verifies that the renders stay aligned.
Second, wire-stable evolution: XTypes 1.3 annotations (@mutable, @final, @appendable, @optional) tell the codegen how a type may change over time. The annotation system means an old reader stays compatible with a new writer, and CI can flag breaking changes before they leave the merge request.
Third, vendor-lock-in avoidance: IDL is an OMG standard, not a vendor format. Move your IDL from RTI Connext, OpenSplice, Cyclone DDS or Fast-DDS into ZeroDDS without rewriting interfaces — only the codegen target changes.
ZeroDDS' own IDL parser is grammar-driven (not regex-based), with a vendor-extension pipeline that accepts dialect quirks from the four established vendors. That is the migration enabler: existing IDL files just compile.
What we built
Eight crates make up the IDL stack. Click into any crate name for its spec-coverage report.
At the centre is the parser. Around it sit seven codegen back-ends — one per target language — plus a CLI (idlc) that orchestrates them. The parser produces a typed AST; each codegen back-end is a small visitor that emits language-native source.
| Crate | Role | Spec / status |
|---|---|---|
idl | Parser + AST | OMG IDL 4.2 (ISO/IEC 19516) — grammar-driven, vendor-extension pipeline for RTI / OpenSplice / Cyclone / Fast-DDS dialects. |
idlc | CLI compiler | zerodds-idlc --<lang> -o <dir> file.idl — orchestrates parser + one of the seven codegen back-ends. |
idl-rust | Rust codegen | Native Rust types with DdsType-Trait, #[derive] macros, async-friendly. Targets stable Rust 1.88+. |
idl-cpp | C++17 codegen | DDS-PSM-Cxx 1.0 compliant headers. ABI-stable consumption, namespaces match IDL modules. |
idl-csharp | C# / .NET codegen | Idiomatic C# with records / async-await, NuGet-packageable, .NET 8 + netstandard2.1 targets. |
idl-java | Java codegen | OMG DDS-Java-PSM (Pure Java, no JNI). Records, sealed types, virtual threads ready. |
py | Python decorator | Codegen-frei: @idl_struct-Decorator wraps eine @dataclass + Field-Type-Marker; Encoder/Decoder entsteht zur Decorator-Zeit. Byte-identisch zum Rust-Codegen-Pfad. |
idl-ts | TypeScript codegen | Two flavours: Node.js bundle and a browser bundle (WebSocket-only). Strict TypeScript, no any. |
idl-c | C99 codegen | POD structs + XCDR2 encoder/decoder tables via zerodds-idl-cpp::c_mode. Vendor spec zerodds-xcdr2-c-1.0. Combine with zerodds-c-api (-lzerodds) for the DCPS runtime. |
cdr / cdr-derive | CDR encoders | cdr + cdr-derive: CDR / XCDR1 / XCDR2 encoders shared by every codegen target. Byte-identical to Cyclone capture replays in CI. |
Vendor spec: zerodds-idl-rust-1.0 →
Spec coverage — construct by construct
Every IDL 4.2 construct, where we implement it, how we test it. Each row links to a dedicated doc page, the source files on GitHub, and the corresponding test files.
Read the full OMG IDL 4.2 specification (PDF, omg.org) →
| Construct | Spec § | Our doc | Implementation | Tests | Status |
|---|---|---|---|---|---|
| Modules | §7.4.1 | module → | idl/src/ast | parse_omg.rs | ✓ RC3 |
| Constants | §7.4.1.4.3 | constant → | idl/src/ast | parse_omg.rs | ✓ RC3 |
| Primitive types (short, long, float, double, char, …) | §7.4.1.4.4 | primitives → | idl/src/ast | parse_omg.rs | ✓ RC3 |
| String, wstring | §7.4.1.4.4.4 | string → | idl/src/ast | parse_omg.rs | ✓ RC3 |
| Sequence<T> | §7.4.13.4.2.1 | sequence → | idl-rust/src | roundtrip.rs | ✓ RC3 |
| Array | §7.4.5 | array → | idl-rust/src | roundtrip.rs | ✓ RC3 |
| Struct | §7.4.13.4.1 | struct → | struct_emit.rs | roundtrip.rs | ✓ RC3 |
| Union | §7.4.13.4.2 | union → | idl-rust/src | union_validation.rs | ✓ RC3 |
| Enum | §7.4.1.4.4.6 | enum → | enum_emit.rs | parse_omg.rs | ✓ RC3 |
| Bitset | §7.4.13.4.6 | bitset → | bitset_emit.rs | bitfield_validation.rs | ✓ RC3 |
| Bitmask | §7.4.13.4.7 | bitmask → | idl-rust/src | bitfield_validation.rs | ✓ RC3 |
| Typedef | §7.4.3 | typedef → | typedef_emit.rs | anon_types.rs | ✓ RC3 |
| Forward declarations | §7.4.6 | forward decl → | idl/src/ast | forward_decl_completion.rs | ✓ RC3 |
| Interface (CORBA / DDS-RPC) | §7.4.10 | interface → | corba-codegen | corba-codegen/tests | ✓ RC3 |
| ValueType (CORBA legacy) | §7.4.12 | valuetype → | corba-codegen | corba-codegen/tests | ✓ RC3 |
| Annotations (@key, @final, @mutable, @optional, …) | §8 (Building Block) | annotations → | annotations.rs | annotations.rs | ✓ RC3 |
| Preprocessor (#include, #define, #if) | §7.3 | preprocessor → | idl/src/preprocessor | preprocessor.rs | ✓ RC3 |
How — using the IDL stack
From a .idl file to native types takes one command per target language. Build-system integration via build.rs / CMake / Gradle / setup.py.
Install
The idlc CLI ships as a separate crate. Install from crates.io or as a signed binary.
cargo install zerodds-idlc
Generate code
Pick a target language and point idlc at your .idl file. The output is one file per top-level module.
zerodds-idlc --c -o gen/c types.idl
zerodds-idlc --cpp -o gen/cpp types.idl
zerodds-idlc --rust -o gen/rust types.idl
zerodds-idlc --ts -o gen/ts types.idl
zerodds-idlc --csharp -o gen/cs types.idl
zerodds-idlc --java -o gen/java types.idl
zerodds-idlc --python -o gen/py types.idl
Build-system integration
Every binding ships a thin build helper. The Rust example below regenerates types.rs whenever types.idl changes.
// build.rs
fn main() {
zerodds_idlc::build()
.file("types.idl")
.out_dir(env!("OUT_DIR"))
.generate()
.expect("idl codegen");
}
Lint your IDL
dds-lint reads .idl files and flags common issues: missing @key annotations, types that break wire compatibility, accidental @mutable on hot-path messages. Use it as a pre-commit hook or CI step.
dds-lint check types.idl
Annotations cheatsheet
XTypes 1.3 annotations control wire-compatible evolution. Pick the right annotation up front — changing it later is a breaking change.
| Annotation | Use |
|---|---|
@topic | @topic — mark a struct as a top-level DDS topic type. CI checks topic-name uniqueness. |
@key | @key — designate fields as the keyset for instance handling. Must be invariant across the lifetime of an instance. |
@appendable | @appendable — default. Fields may be added at the end without breaking older readers. |
@mutable | @mutable — fields may be added or removed by name. Slightly larger wire size; more flexibility. |
@final | @final — frozen. Adding a field is a breaking change. Use for high-rate, locked-in types. |
@optional | @optional — field may be absent on the wire. Readers see Option<T>. |
Per-language pointers
- Rust
-
cargo add zerodds-dcps zerodds-idlc. Use build.rs to regenerate types.rs on every cargo build. Async via dcps-async.
- C++
-
zerodds-idlc --cpp emits C++17 headers under your include path. Configure CMake to call zerodds-idlc before compilation.
- C# / .NET
-
zerodds-idlc --csharp emits a .cs file per module. Add it to your MSBuild project. Target .NET 8+ or netstandard2.1.
- Java
-
zerodds-idlc --java emits a .java file per IDL declaration under the standard Maven layout (one file per package). Pure Java — no JNI.
- Python
-
zerodds-idlc --python emits one Python module per IDL module with @idl_struct decorators and type hints. Same wire bytes as the Rust path.
- TypeScript
-
zerodds-idlc --ts emits a TypeScript module. Pair with ts-node for the Node.js bundle or ts-wasm for the browser bundle (WebSocket-only). Strict TypeScript.
- C
-
zerodds-idlc --c emits a plain C99 header with POD structs and XCDR2 typesupport tables for your IDL types — wire-compatible with every other ZeroDDS codegen target. Independent from the runtime: combine with the zerodds-c-api FFI library (-lzerodds) for full DCPS access from C.