Skip to main content

zerodds_idl_cpp/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! IDL4 → C++17-Header-Codegen (OMG IDL4-CPP-Mapping, formal/2018-07-01).
4//!
5//! Crate `zerodds-idl-cpp` — Foundation des Sprach-Bindings (Cluster C5.1-a).
6//!
7//! Safety classification: **SAFE (std-only)**. Reines Build-Zeit-Tool —
8//! `forbid(unsafe_code)`, kein no_std-Use-Case.
9//!
10//! # Scope (C5.1-a)
11//! - Block A: Header-Layout (`#pragma once`, `namespace`, includes).
12//! - Block B: Primitive-Mapping (boolean → bool, octet → uint8_t, ...).
13//! - Block C: struct/enum/union/typedef/sequence/array/inheritance.
14//! - Block D: Exception → `class X : public std::exception`.
15//! - Block E: Time/Duration → DDS::Time_t / DDS::Duration_t.
16//!
17//! # C5.1-b Erweiterungen
18//! - Block F: Status-Mapping (13 Status-Klassen, [`status`]).
19//! - Block G: QoS-Policy + Type-Traits (22 Policies, [`qos`]).
20//! - Block H: DCPS-Entity-Header-Stubs ([`dcps`]).
21//!
22//! # C5.2 Erweiterungen
23//! - DDS-PSM-CXX-Header-Skeleton-Layer ([`psm_cxx`]).
24//!
25//! # C6.1.D-cpp Erweiterungen
26//! - DDS-RPC C++ PSM-Codegen ([`rpc`]) — Service-Interface, Requester,
27//!   Replier, ServiceTraits + RemoteException-Hierarchie. Spec §10.
28//!
29//! # Bewusst nicht im Crate
30//! - Bitset/Bitmask, Map, Fixed, Any, Interface, Valuetype.
31//! - Linker-Tests (statische Header-Generation reicht).
32//!
33//! # Beispiel
34//!
35//! ```
36//! use zerodds_idl::config::ParserConfig;
37//! use zerodds_idl_cpp::{generate_cpp_header, CppGenOptions};
38//!
39//! let ast = zerodds_idl::parse(
40//!     "module M { struct S { long x; }; };",
41//!     &ParserConfig::default(),
42//! )
43//! .expect("parse");
44//! let cpp = generate_cpp_header(&ast, &CppGenOptions::default()).expect("gen");
45//! assert!(cpp.contains("namespace M"));
46//! assert!(cpp.contains("class S"));
47//! ```
48
49#![forbid(unsafe_code)]
50#![warn(missing_docs)]
51#![allow(
52    clippy::manual_pattern_char_comparison,
53    clippy::if_same_then_else,
54    clippy::collapsible_if,
55    clippy::useless_conversion,
56    clippy::approx_constant
57)]
58
59pub(crate) mod amqp;
60pub(crate) mod bitset;
61pub mod c_mode;
62pub(crate) mod corba_traits;
63pub mod dcps;
64pub mod emitter;
65pub mod error;
66pub mod psm_cxx;
67pub mod qos;
68pub mod rpc;
69pub mod status;
70pub mod type_map;
71pub(crate) mod verbatim;
72
73pub use c_mode::{CGenOptions, generate_c_header};
74pub use error::CppGenError;
75pub use psm_cxx::{
76    emit_condition_skeleton, emit_core_basics, emit_exception_hierarchy,
77    emit_full_psm_cxx_skeleton, emit_listener_skeleton, emit_psm_cxx_includes,
78    emit_reference_value_pattern,
79};
80
81use zerodds_idl::ast::Specification;
82
83/// Konfiguration des Code-Generators.
84#[derive(Debug, Clone)]
85pub struct CppGenOptions {
86    /// Optionaler aeusserer Namespace, in den der gesamte Header gewickelt
87    /// wird. `None` oder leer = kein Wrapper.
88    pub namespace_prefix: Option<String>,
89    /// Optionaler include-Guard-Prefix (Kommentar-Marker zusaetzlich zu
90    /// `#pragma once`). Foundation legt nur `#pragma once`; der Prefix
91    /// erscheint als Kommentar.
92    pub include_guard_prefix: Option<String>,
93    /// Indent-Breite in Leerzeichen. Default 4.
94    pub indent_width: usize,
95    /// Spec §7.2.3 / §8.1.2 / §8.1.3 — opt-in: fügt am Ende des
96    /// generierten Headers per-Type AMQP-Codec-Helper an
97    /// (`to_amqp_value`, `to_json_string`). Default `false`, weil
98    /// die emittierten Calls einen kleinen C++-Runtime-Header
99    /// `<zerodds/amqp/codec.hpp>` voraussetzen, der als separate
100    /// Library-Crate kommt.
101    pub emit_amqp_helpers: bool,
102    /// Annex A.1 (idl4-cpp-1.0) — opt-in: fügt am Ende
103    /// CORBA-spezifische Trait-Spezialisierungen
104    /// (`CORBA::traits<T>::value_type/in_type/out_type/inout_type`)
105    /// pro Top-Level-Type an. Default `false`.
106    pub emit_corba_traits: bool,
107}
108
109impl Default for CppGenOptions {
110    fn default() -> Self {
111        Self {
112            namespace_prefix: None,
113            include_guard_prefix: None,
114            indent_width: 4,
115            emit_amqp_helpers: false,
116            emit_corba_traits: false,
117        }
118    }
119}
120
121/// Block-E: Mapping von Time/Duration-Identifiern auf C++-Type-Strings.
122///
123/// Wenn ein IDL-Member `Time_t` referenziert (single-component scoped name),
124/// wird er auf `DDS::Time_t` gemappt. Spec-Quelle: dds-psm-cxx §6.4.
125pub(crate) const TIME_DURATION_TYPES: &[(&str, &str)] = &[
126    ("Time_t", "DDS::Time_t"),
127    ("Duration_t", "DDS::Duration_t"),
128    ("Time", "DDS::Time_t"),
129    ("Duration", "DDS::Duration_t"),
130];
131
132/// Erzeugt einen vollstaendigen C++17-Header aus einer IDL-Specification.
133///
134/// # Errors
135/// - [`CppGenError::UnsupportedConstruct`]: IDL-Konstrukt außerhalb des aktuellen Scopes
136///   (z.B. `interface`, `valuetype`, `fixed`, `any`, `map`, `bitset`,
137///   `bitmask`).
138/// - [`CppGenError::InvalidName`]: Ein Identifier kollidiert mit einem
139///   reservierten C++-Keyword.
140/// - [`CppGenError::InheritanceCycle`]: Direkte oder indirekte
141///   Self-Inheritance im Struct-Graphen.
142pub fn generate_cpp_header(
143    ast: &Specification,
144    opts: &CppGenOptions,
145) -> Result<String, CppGenError> {
146    let mut out = emitter::emit_header(ast, opts)?;
147    if opts.emit_amqp_helpers {
148        amqp::emit_amqp_helpers(&mut out, ast)?;
149    }
150    if opts.emit_corba_traits {
151        corba_traits::emit_corba_traits(&mut out, ast)?;
152    }
153    Ok(out)
154}
155
156/// Convenience-Variante mit aktiviertem `emit_corba_traits`-Flag.
157///
158/// Identisch zu [`generate_cpp_header`], aber zwingt
159/// `opts.emit_corba_traits = true`. Cross-Ref: `idl4-cpp-1.0` Annex A.1.
160///
161/// # Errors
162/// Wie [`generate_cpp_header`].
163pub fn generate_cpp_header_with_corba_traits(
164    ast: &Specification,
165    opts: &CppGenOptions,
166) -> Result<String, CppGenError> {
167    let opts = CppGenOptions {
168        emit_corba_traits: true,
169        ..opts.clone()
170    };
171    generate_cpp_header(ast, &opts)
172}
173
174/// Convenience-Variante mit aktiviertem `emit_amqp_helpers`-Flag.
175///
176/// Identisch zu [`generate_cpp_header`], aber zwingt
177/// `opts.emit_amqp_helpers = true`. Nützlich für Tests und
178/// Tooling, die den AMQP-Bindings-Pfad explizit auswählen wollen.
179///
180/// # Errors
181/// Wie [`generate_cpp_header`].
182pub fn generate_cpp_header_with_amqp(
183    ast: &Specification,
184    opts: &CppGenOptions,
185) -> Result<String, CppGenError> {
186    let opts = CppGenOptions {
187        emit_amqp_helpers: true,
188        ..opts.clone()
189    };
190    generate_cpp_header(ast, &opts)
191}
192
193#[cfg(test)]
194mod tests {
195    #![allow(clippy::expect_used, clippy::panic)]
196    use super::*;
197    use zerodds_idl::config::ParserConfig;
198
199    fn gen_cpp(src: &str) -> String {
200        let ast = zerodds_idl::parse(src, &ParserConfig::default()).expect("parse must succeed");
201        generate_cpp_header(&ast, &CppGenOptions::default()).expect("gen must succeed")
202    }
203
204    #[test]
205    fn empty_source_emits_only_preamble() {
206        let cpp = gen_cpp("");
207        assert!(cpp.contains("#pragma once"));
208        assert!(cpp.contains("Generated by zerodds idl-cpp"));
209        // Kein Namespace-Open ohne Module.
210        assert!(!cpp.contains("namespace M {"));
211    }
212
213    #[test]
214    fn empty_module_emits_namespace() {
215        let cpp = gen_cpp("module M {};");
216        assert!(cpp.contains("namespace M {"));
217        assert!(cpp.contains("} // namespace M"));
218    }
219
220    #[test]
221    fn three_level_modules_nest() {
222        let cpp = gen_cpp("module A { module B { module C {}; }; };");
223        assert!(cpp.contains("namespace A {"));
224        assert!(cpp.contains("namespace B {"));
225        assert!(cpp.contains("namespace C {"));
226        assert!(cpp.contains("} // namespace C"));
227        assert!(cpp.contains("} // namespace B"));
228        assert!(cpp.contains("} // namespace A"));
229    }
230
231    #[test]
232    fn primitive_struct_member_uses_correct_cpp_types() {
233        let cpp = gen_cpp(
234            "struct S { boolean b; octet o; short s; long l; long long ll; \
235             unsigned short us; unsigned long ul; unsigned long long ull; \
236             float f; double d; };",
237        );
238        assert!(cpp.contains("bool b_;"));
239        assert!(cpp.contains("uint8_t o_;"));
240        assert!(cpp.contains("int16_t s_;"));
241        assert!(cpp.contains("int32_t l_;"));
242        assert!(cpp.contains("int64_t ll_;"));
243        assert!(cpp.contains("uint16_t us_;"));
244        assert!(cpp.contains("uint32_t ul_;"));
245        assert!(cpp.contains("uint64_t ull_;"));
246        assert!(cpp.contains("float f_;"));
247        assert!(cpp.contains("double d_;"));
248    }
249
250    #[test]
251    fn string_member_requires_string_include() {
252        let cpp = gen_cpp("struct S { string name; };");
253        assert!(cpp.contains("#include <string>"));
254        assert!(cpp.contains("std::string name_;"));
255    }
256
257    #[test]
258    fn sequence_member_uses_vector() {
259        let cpp = gen_cpp("struct S { sequence<long> data; };");
260        assert!(cpp.contains("#include <vector>"));
261        assert!(cpp.contains("std::vector<int32_t> data_;"));
262    }
263
264    #[test]
265    fn array_member_uses_std_array() {
266        let cpp = gen_cpp("struct S { long matrix[3][4]; };");
267        assert!(cpp.contains("#include <array>"));
268        assert!(cpp.contains("std::array<std::array<int32_t, 4>, 3>"));
269    }
270
271    #[test]
272    fn enum_emits_enum_class_int32_t() {
273        let cpp = gen_cpp("enum Color { RED, GREEN, BLUE };");
274        assert!(cpp.contains("enum class Color : int32_t"));
275        assert!(cpp.contains("RED,"));
276        assert!(cpp.contains("BLUE,"));
277    }
278
279    #[test]
280    fn typedef_emits_using_alias() {
281        let cpp = gen_cpp("typedef long MyInt;");
282        assert!(cpp.contains("using MyInt = int32_t;"));
283    }
284
285    #[test]
286    fn inheritance_emits_public_base() {
287        let cpp = gen_cpp("struct Parent { long x; }; struct Child : Parent { long y; };");
288        assert!(cpp.contains("class Child : public Parent"));
289    }
290
291    #[test]
292    fn keyed_struct_marker_appears() {
293        let cpp = gen_cpp("struct S { @key long id; long val; };");
294        assert!(cpp.contains("// @key"));
295    }
296
297    #[test]
298    fn optional_member_uses_std_optional() {
299        let cpp = gen_cpp("struct S { @optional long maybe; };");
300        assert!(cpp.contains("#include <optional>"));
301        assert!(cpp.contains("std::optional<int32_t>"));
302    }
303
304    #[test]
305    fn exception_inherits_std_exception() {
306        let cpp = gen_cpp("exception NotFound { string what_; };");
307        assert!(cpp.contains("#include <exception>"));
308        assert!(cpp.contains("class NotFound : public std::exception"));
309    }
310
311    #[test]
312    fn union_uses_std_variant() {
313        let cpp = gen_cpp(
314            "union U switch (long) { case 1: long a; case 2: double b; default: octet c; };",
315        );
316        assert!(cpp.contains("#include <variant>"));
317        assert!(cpp.contains("std::variant<"));
318        assert!(cpp.contains("// case default"));
319    }
320
321    #[test]
322    fn time_t_member_maps_to_dds_time_t() {
323        let cpp = gen_cpp("struct S { Time_t t; };");
324        assert!(cpp.contains("DDS::Time_t"));
325    }
326
327    #[test]
328    fn duration_t_member_maps_to_dds_duration_t() {
329        let cpp = gen_cpp("struct S { Duration_t d; };");
330        assert!(cpp.contains("DDS::Duration_t"));
331    }
332
333    #[test]
334    fn reserved_field_name_is_rejected() {
335        let ast = zerodds_idl::parse("struct S { long class_field; };", &ParserConfig::default())
336            .expect("parse");
337        // "class_field" ist nicht reserviert; Test mit Annotation-Trick:
338        // wir erzwingen via Builder-API einen Reserved-Name.
339        // Stattdessen pruefen wir den Pfad direkt ueber check_identifier:
340        let res = type_map::check_identifier("class");
341        assert!(matches!(res, Err(CppGenError::InvalidName { .. })));
342        let _ = ast; // ungenutzt aber zeigt die Idee
343    }
344
345    #[test]
346    fn empty_source_includes_cstdint() {
347        let cpp = gen_cpp("");
348        assert!(cpp.contains("#include <cstdint>"));
349    }
350
351    #[test]
352    fn header_starts_with_generated_marker() {
353        let cpp = gen_cpp("");
354        assert!(cpp.starts_with("// Generated by zerodds idl-cpp."));
355    }
356
357    #[test]
358    fn pragma_once_appears_exactly_once() {
359        let cpp = gen_cpp("module M { struct S { long x; }; };");
360        let count = cpp.matches("#pragma once").count();
361        assert_eq!(count, 1);
362    }
363
364    #[test]
365    fn struct_has_default_constructor() {
366        let cpp = gen_cpp("struct S { long x; };");
367        assert!(cpp.contains("S() = default;"));
368        assert!(cpp.contains("~S() = default;"));
369    }
370
371    #[test]
372    fn struct_has_mutable_and_const_getter() {
373        let cpp = gen_cpp("struct S { long x; };");
374        // Mutable getter: returns int32_t&; const-version present too.
375        assert!(cpp.contains("int32_t& x()"));
376        assert!(cpp.contains("const int32_t& x() const"));
377    }
378
379    #[test]
380    fn struct_has_setter() {
381        let cpp = gen_cpp("struct S { long x; };");
382        assert!(cpp.contains("void x(const int32_t& value)"));
383    }
384
385    #[test]
386    fn namespace_prefix_option_wraps_output() {
387        let ast =
388            zerodds_idl::parse("struct S { long x; };", &ParserConfig::default()).expect("parse");
389        let opts = CppGenOptions {
390            namespace_prefix: Some("zerodds".into()),
391            ..Default::default()
392        };
393        let cpp = generate_cpp_header(&ast, &opts).expect("gen");
394        assert!(cpp.contains("namespace zerodds {"));
395        assert!(cpp.contains("} // namespace zerodds"));
396    }
397
398    #[test]
399    fn non_service_interface_emits_pure_virtual_class() {
400        let ast = zerodds_idl::parse("interface I { void op(); };", &ParserConfig::default())
401            .expect("parse");
402        let cpp = generate_cpp_header(&ast, &CppGenOptions::default()).expect("ok");
403        assert!(cpp.contains("class I"));
404        assert!(cpp.contains("virtual ~I()"));
405        assert!(cpp.contains("= 0;"));
406    }
407
408    #[test]
409    fn const_decl_emits_constexpr() {
410        let cpp = gen_cpp("const long MAX = 100;");
411        assert!(cpp.contains("constexpr int32_t MAX = 100;"));
412    }
413
414    #[test]
415    fn options_have_sensible_defaults() {
416        let o = CppGenOptions::default();
417        assert_eq!(o.indent_width, 4);
418        assert!(o.namespace_prefix.is_none());
419        assert!(o.include_guard_prefix.is_none());
420    }
421
422    #[test]
423    fn options_clone_works() {
424        let o = CppGenOptions {
425            namespace_prefix: Some("foo".into()),
426            include_guard_prefix: Some("FOO_".into()),
427            indent_width: 2,
428            emit_amqp_helpers: false,
429            emit_corba_traits: false,
430        };
431        let cloned = o.clone();
432        assert_eq!(cloned.indent_width, 2);
433        assert_eq!(cloned.namespace_prefix.as_deref(), Some("foo"));
434    }
435}