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 of the language binding (cluster C5.1-a).
6//!
7//! Safety classification: **SAFE (std-only)**. Pure build-time tool —
8//! `forbid(unsafe_code)`, no 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 extensions
18//! - Block F: status mapping (13 status classes, [`status`]).
19//! - Block G: QoS policy + type traits (22 policies, [`qos`]).
20//! - Block H: DCPS entity header stubs ([`dcps`]).
21//!
22//! # C5.2 extensions
23//! - DDS-PSM-CXX header skeleton layer ([`psm_cxx`]).
24//!
25//! # C6.1.D-cpp extensions
26//! - DDS-RPC C++ PSM codegen ([`rpc`]) — service interface, requester,
27//!   replier, ServiceTraits + RemoteException hierarchy. Spec §10.
28//!
29//! # Intentionally not in the crate
30//! - Bitset/Bitmask, Map, Fixed, Any, Interface, Valuetype.
31//! - Linker tests (static header generation is sufficient).
32//!
33//! # Example
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/// Configuration of the code generator.
84#[derive(Debug, Clone)]
85pub struct CppGenOptions {
86    /// Optional outer namespace that wraps the entire header.
87    /// `None` or empty = no wrapper.
88    pub namespace_prefix: Option<String>,
89    /// Optional include-guard prefix (comment marker in addition to
90    /// `#pragma once`). The foundation emits only `#pragma once`; the prefix
91    /// appears as a comment.
92    pub include_guard_prefix: Option<String>,
93    /// Indent width in spaces. Default 4.
94    pub indent_width: usize,
95    /// Spec §7.2.3 / §8.1.2 / §8.1.3 — opt-in: appends per-type AMQP codec
96    /// helpers (`to_amqp_value`, `to_json_string`) at the end of the
97    /// generated header. Default `false`, because the emitted calls
98    /// require a small C++ runtime header `<zerodds/amqp/codec.hpp>`
99    /// that ships as a separate library crate.
100    pub emit_amqp_helpers: bool,
101    /// Annex A.1 (idl4-cpp-1.0) — opt-in: appends CORBA-specific trait
102    /// specializations
103    /// (`CORBA::traits<T>::value_type/in_type/out_type/inout_type`)
104    /// per top-level type at the end. Default `false`.
105    pub emit_corba_traits: bool,
106}
107
108impl Default for CppGenOptions {
109    fn default() -> Self {
110        Self {
111            namespace_prefix: None,
112            include_guard_prefix: None,
113            indent_width: 4,
114            emit_amqp_helpers: false,
115            emit_corba_traits: false,
116        }
117    }
118}
119
120/// Block-E: mapping of Time/Duration identifiers to C++ type strings.
121///
122/// When an IDL member references `Time_t` (single-component scoped name),
123/// it is mapped to `DDS::Time_t`. Spec source: dds-psm-cxx §6.4.
124pub(crate) const TIME_DURATION_TYPES: &[(&str, &str)] = &[
125    ("Time_t", "DDS::Time_t"),
126    ("Duration_t", "DDS::Duration_t"),
127    ("Time", "DDS::Time_t"),
128    ("Duration", "DDS::Duration_t"),
129];
130
131/// Produces a complete C++17 header from an IDL specification.
132///
133/// # Errors
134/// - [`CppGenError::UnsupportedConstruct`]: IDL construct outside the current scope
135///   (e.g. `interface`, `valuetype`, `fixed`, `any`, `map`, `bitset`,
136///   `bitmask`).
137/// - [`CppGenError::InvalidName`]: An identifier collides with a
138///   reserved C++ keyword.
139/// - [`CppGenError::InheritanceCycle`]: Direct or indirect
140///   self-inheritance in the struct graph.
141pub fn generate_cpp_header(
142    ast: &Specification,
143    opts: &CppGenOptions,
144) -> Result<String, CppGenError> {
145    let mut out = emitter::emit_header(ast, opts)?;
146    if opts.emit_amqp_helpers {
147        amqp::emit_amqp_helpers(&mut out, ast)?;
148    }
149    if opts.emit_corba_traits {
150        corba_traits::emit_corba_traits(&mut out, ast)?;
151    }
152    Ok(out)
153}
154
155/// Convenience variant with the `emit_corba_traits` flag enabled.
156///
157/// Identical to [`generate_cpp_header`], but forces
158/// `opts.emit_corba_traits = true`. Cross-ref: `idl4-cpp-1.0` Annex A.1.
159///
160/// # Errors
161/// Same as [`generate_cpp_header`].
162pub fn generate_cpp_header_with_corba_traits(
163    ast: &Specification,
164    opts: &CppGenOptions,
165) -> Result<String, CppGenError> {
166    let opts = CppGenOptions {
167        emit_corba_traits: true,
168        ..opts.clone()
169    };
170    generate_cpp_header(ast, &opts)
171}
172
173/// Convenience variant with the `emit_amqp_helpers` flag enabled.
174///
175/// Identical to [`generate_cpp_header`], but forces
176/// `opts.emit_amqp_helpers = true`. Useful for tests and tooling
177/// that want to select the AMQP bindings path explicitly.
178///
179/// # Errors
180/// Same as [`generate_cpp_header`].
181pub fn generate_cpp_header_with_amqp(
182    ast: &Specification,
183    opts: &CppGenOptions,
184) -> Result<String, CppGenError> {
185    let opts = CppGenOptions {
186        emit_amqp_helpers: true,
187        ..opts.clone()
188    };
189    generate_cpp_header(ast, &opts)
190}
191
192#[cfg(test)]
193mod tests {
194    #![allow(clippy::expect_used, clippy::panic)]
195    use super::*;
196    use zerodds_idl::config::ParserConfig;
197
198    fn gen_cpp(src: &str) -> String {
199        let ast = zerodds_idl::parse(src, &ParserConfig::default()).expect("parse must succeed");
200        generate_cpp_header(&ast, &CppGenOptions::default()).expect("gen must succeed")
201    }
202
203    #[test]
204    fn empty_source_emits_only_preamble() {
205        let cpp = gen_cpp("");
206        assert!(cpp.contains("#pragma once"));
207        assert!(cpp.contains("Generated by zerodds idl-cpp"));
208        // No namespace open without a module.
209        assert!(!cpp.contains("namespace M {"));
210    }
211
212    #[test]
213    fn empty_module_emits_namespace() {
214        let cpp = gen_cpp("module M {};");
215        assert!(cpp.contains("namespace M {"));
216        assert!(cpp.contains("} // namespace M"));
217    }
218
219    #[test]
220    fn three_level_modules_nest() {
221        let cpp = gen_cpp("module A { module B { module C {}; }; };");
222        assert!(cpp.contains("namespace A {"));
223        assert!(cpp.contains("namespace B {"));
224        assert!(cpp.contains("namespace C {"));
225        assert!(cpp.contains("} // namespace C"));
226        assert!(cpp.contains("} // namespace B"));
227        assert!(cpp.contains("} // namespace A"));
228    }
229
230    #[test]
231    fn primitive_struct_member_uses_correct_cpp_types() {
232        let cpp = gen_cpp(
233            "struct S { boolean b; octet o; short s; long l; long long ll; \
234             unsigned short us; unsigned long ul; unsigned long long ull; \
235             float f; double d; };",
236        );
237        assert!(cpp.contains("bool b_;"));
238        assert!(cpp.contains("uint8_t o_;"));
239        assert!(cpp.contains("int16_t s_;"));
240        assert!(cpp.contains("int32_t l_;"));
241        assert!(cpp.contains("int64_t ll_;"));
242        assert!(cpp.contains("uint16_t us_;"));
243        assert!(cpp.contains("uint32_t ul_;"));
244        assert!(cpp.contains("uint64_t ull_;"));
245        assert!(cpp.contains("float f_;"));
246        assert!(cpp.contains("double d_;"));
247    }
248
249    #[test]
250    fn string_member_requires_string_include() {
251        let cpp = gen_cpp("struct S { string name; };");
252        assert!(cpp.contains("#include <string>"));
253        assert!(cpp.contains("std::string name_;"));
254    }
255
256    #[test]
257    fn sequence_member_uses_vector() {
258        let cpp = gen_cpp("struct S { sequence<long> data; };");
259        assert!(cpp.contains("#include <vector>"));
260        assert!(cpp.contains("std::vector<int32_t> data_;"));
261    }
262
263    #[test]
264    fn array_member_uses_std_array() {
265        let cpp = gen_cpp("struct S { long matrix[3][4]; };");
266        assert!(cpp.contains("#include <array>"));
267        assert!(cpp.contains("std::array<std::array<int32_t, 4>, 3>"));
268    }
269
270    #[test]
271    fn enum_emits_enum_class_int32_t() {
272        let cpp = gen_cpp("enum Color { RED, GREEN, BLUE };");
273        assert!(cpp.contains("enum class Color : int32_t"));
274        assert!(cpp.contains("RED,"));
275        assert!(cpp.contains("BLUE,"));
276    }
277
278    #[test]
279    fn typedef_emits_using_alias() {
280        let cpp = gen_cpp("typedef long MyInt;");
281        assert!(cpp.contains("using MyInt = int32_t;"));
282    }
283
284    #[test]
285    fn inheritance_emits_public_base() {
286        let cpp = gen_cpp("struct Parent { long x; }; struct Child : Parent { long y; };");
287        // Base reference is absolutely qualified (`::Parent`) so it resolves at
288        // any scope — see the type-path registry in emitter.rs.
289        assert!(cpp.contains("class Child : public ::Parent"));
290    }
291
292    /// Regression for Bug B (unqualified nested types) + Bug F (typedef-to-
293    /// primitive members silently dropped). Models the NGVA shape (AEP-4754
294    /// Vol V §8.1.3): typedef-in-units + enum + nested struct as members.
295    #[test]
296    fn ngva_typedef_serialized_and_nested_qualified() {
297        let cpp = gen_cpp(
298            "module nga { \
299               typedef double CurrentInAmpsType; \
300               enum CoordinateSystemType { COORDINATE_SYSTEM_TYPE__BNG }; \
301               struct LinearVelocity2DType { double xComponent; double yComponent; }; \
302               struct Navigation_Resource_Specification { \
303                 @key string<32>      vehicleId; \
304                 CurrentInAmpsType    batteryCurrent; \
305                 CoordinateSystemType coordinateSystem; \
306                 LinearVelocity2DType velocity; \
307               }; };",
308        );
309        // Bug F: no member is skipped (typedef-to-primitive resolves to double).
310        assert!(
311            !cpp.contains("not supported (skip)"),
312            "typedef/nested members must not be skipped (Bug F)"
313        );
314        // Bug B: nested struct + enum referenced with absolute namespace.
315        assert!(
316            cpp.contains("::nga::LinearVelocity2DType"),
317            "nested struct must be absolutely qualified (Bug B)"
318        );
319        assert!(
320            cpp.contains("::nga::CoordinateSystemType"),
321            "nested enum must be absolutely qualified (Bug B)"
322        );
323    }
324
325    /// Regression for Bug G: a self-referential recursive type
326    /// (XTypes §7.4.5) once stack-overflowed the generator (the encodability
327    /// walk `scoped_struct` → `typespec_supported` → `scoped_struct` cycled
328    /// forever). It must now generate without panicking, store the recursion
329    /// behind a `std::vector` (heap indirection), and serialize the recursive
330    /// member via the splice path — never dropping it ("not supported (skip)").
331    #[test]
332    fn recursive_struct_generates_without_overflow_and_no_skip() {
333        let cpp = gen_cpp(
334            "module conf { \
335               struct TreeNode { long value; sequence<TreeNode> children; }; \
336             };",
337        );
338        assert!(
339            cpp.contains("std::vector<::conf::TreeNode>"),
340            "recursion must be stored behind a vector (heap indirection)"
341        );
342        assert!(
343            !cpp.contains("not supported (skip)"),
344            "recursive member must be serialized, not dropped (no wire data loss)"
345        );
346        // The splice path references the type's own type_support, not an
347        // (infinite) inline expansion.
348        assert!(
349            cpp.contains("topic_type_support<::conf::TreeNode>"),
350            "recursive member must splice via its own topic_type_support"
351        );
352    }
353
354    /// Regression for Bug G: a forward-declared struct that is then
355    /// self-referential must also generate cleanly (the forward decl was the
356    /// second crash trigger).
357    #[test]
358    fn forward_declared_recursive_struct_generates() {
359        let cpp = gen_cpp(
360            "module conf { \
361               struct Node; \
362               struct Node { long v; sequence<Node> next; }; \
363             };",
364        );
365        assert!(cpp.contains("std::vector<::conf::Node>"));
366        assert!(!cpp.contains("not supported (skip)"));
367    }
368
369    #[test]
370    fn keyed_struct_marker_appears() {
371        let cpp = gen_cpp("struct S { @key long id; long val; };");
372        assert!(cpp.contains("// @key"));
373    }
374
375    #[test]
376    fn optional_member_uses_std_optional() {
377        let cpp = gen_cpp("struct S { @optional long maybe; };");
378        assert!(cpp.contains("#include <optional>"));
379        assert!(cpp.contains("std::optional<int32_t>"));
380    }
381
382    #[test]
383    fn exception_inherits_std_exception() {
384        let cpp = gen_cpp("exception NotFound { string what_; };");
385        assert!(cpp.contains("#include <exception>"));
386        assert!(cpp.contains("class NotFound : public std::exception"));
387    }
388
389    #[test]
390    fn union_uses_std_variant() {
391        let cpp = gen_cpp(
392            "union U switch (long) { case 1: long a; case 2: double b; default: octet c; };",
393        );
394        assert!(cpp.contains("#include <variant>"));
395        assert!(cpp.contains("std::variant<"));
396        assert!(cpp.contains("// case default"));
397    }
398
399    #[test]
400    fn time_t_member_maps_to_dds_time_t() {
401        let cpp = gen_cpp("struct S { Time_t t; };");
402        assert!(cpp.contains("DDS::Time_t"));
403    }
404
405    #[test]
406    fn duration_t_member_maps_to_dds_duration_t() {
407        let cpp = gen_cpp("struct S { Duration_t d; };");
408        assert!(cpp.contains("DDS::Duration_t"));
409    }
410
411    #[test]
412    fn reserved_field_name_is_rejected() {
413        let ast = zerodds_idl::parse("struct S { long class_field; };", &ParserConfig::default())
414            .expect("parse");
415        // "class_field" is not reserved; test with an annotation trick:
416        // we force a reserved name via the builder API.
417        // Instead, we check the path directly through check_identifier:
418        let res = type_map::check_identifier("class");
419        assert!(matches!(res, Err(CppGenError::InvalidName { .. })));
420        let _ = ast; // unused but illustrates the idea
421    }
422
423    #[test]
424    fn empty_source_includes_cstdint() {
425        let cpp = gen_cpp("");
426        assert!(cpp.contains("#include <cstdint>"));
427    }
428
429    #[test]
430    fn header_starts_with_generated_marker() {
431        let cpp = gen_cpp("");
432        assert!(cpp.starts_with("// Generated by zerodds idl-cpp."));
433    }
434
435    #[test]
436    fn pragma_once_appears_exactly_once() {
437        let cpp = gen_cpp("module M { struct S { long x; }; };");
438        let count = cpp.matches("#pragma once").count();
439        assert_eq!(count, 1);
440    }
441
442    #[test]
443    fn struct_has_default_constructor() {
444        let cpp = gen_cpp("struct S { long x; };");
445        assert!(cpp.contains("S() = default;"));
446        assert!(cpp.contains("~S() = default;"));
447    }
448
449    #[test]
450    fn struct_has_mutable_and_const_getter() {
451        let cpp = gen_cpp("struct S { long x; };");
452        // Mutable getter: returns int32_t&; const-version present too.
453        assert!(cpp.contains("int32_t& x()"));
454        assert!(cpp.contains("const int32_t& x() const"));
455    }
456
457    #[test]
458    fn struct_has_setter() {
459        let cpp = gen_cpp("struct S { long x; };");
460        assert!(cpp.contains("void x(const int32_t& value)"));
461    }
462
463    #[test]
464    fn struct_has_field_order_constructor() {
465        // The field-order ctor enables brace-init `S t{23, "A7"};` (the
466        // website C++ snippet). Default ctor + accessors stay intact.
467        let cpp = gen_cpp("struct S { long celsius; string sensor_id; };");
468        assert!(
469            cpp.contains("S(int32_t celsius, std::string sensor_id)"),
470            "field-order ctor signature missing:\n{cpp}"
471        );
472        assert!(
473            cpp.contains(": celsius_(std::move(celsius)), sensor_id_(std::move(sensor_id)) {}"),
474            "member-init list missing:\n{cpp}"
475        );
476        // Default ctor must still be present (brace-init coexists with it).
477        assert!(cpp.contains("S() = default;"));
478    }
479
480    #[test]
481    fn zero_field_struct_has_no_field_order_constructor() {
482        // A no-member struct must NOT emit a parameterless ctor — it would be
483        // ambiguous with the defaulted default constructor.
484        let cpp = gen_cpp("struct Empty { };");
485        assert!(cpp.contains("Empty() = default;"));
486        // Only the defaulted ctor line should mention `Empty(`; no extra
487        // `Empty()` redeclaration and no `std::move`.
488        assert_eq!(
489            cpp.matches("Empty(").count(),
490            2, // `Empty() = default;` and `~Empty() = default;` (destructor)
491            "unexpected extra constructor for zero-field struct:\n{cpp}"
492        );
493        assert!(!cpp.contains("std::move"));
494    }
495
496    #[test]
497    fn namespace_prefix_option_wraps_output() {
498        let ast =
499            zerodds_idl::parse("struct S { long x; };", &ParserConfig::default()).expect("parse");
500        let opts = CppGenOptions {
501            namespace_prefix: Some("zerodds".into()),
502            ..Default::default()
503        };
504        let cpp = generate_cpp_header(&ast, &opts).expect("gen");
505        assert!(cpp.contains("namespace zerodds {"));
506        assert!(cpp.contains("} // namespace zerodds"));
507    }
508
509    #[test]
510    fn non_service_interface_emits_pure_virtual_class() {
511        let ast = zerodds_idl::parse("interface I { void op(); };", &ParserConfig::default())
512            .expect("parse");
513        let cpp = generate_cpp_header(&ast, &CppGenOptions::default()).expect("ok");
514        assert!(cpp.contains("class I"));
515        assert!(cpp.contains("virtual ~I()"));
516        assert!(cpp.contains("= 0;"));
517    }
518
519    #[test]
520    fn const_decl_emits_constexpr() {
521        let cpp = gen_cpp("const long MAX = 100;");
522        assert!(cpp.contains("constexpr int32_t MAX = 100;"));
523    }
524
525    #[test]
526    fn options_have_sensible_defaults() {
527        let o = CppGenOptions::default();
528        assert_eq!(o.indent_width, 4);
529        assert!(o.namespace_prefix.is_none());
530        assert!(o.include_guard_prefix.is_none());
531    }
532
533    #[test]
534    fn options_clone_works() {
535        let o = CppGenOptions {
536            namespace_prefix: Some("foo".into()),
537            include_guard_prefix: Some("FOO_".into()),
538            indent_width: 2,
539            emit_amqp_helpers: false,
540            emit_corba_traits: false,
541        };
542        let cloned = o.clone();
543        assert_eq!(cloned.indent_width, 2);
544        assert_eq!(cloned.namespace_prefix.as_deref(), Some("foo"));
545    }
546}