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        assert!(cpp.contains("class Child : public Parent"));
288    }
289
290    #[test]
291    fn keyed_struct_marker_appears() {
292        let cpp = gen_cpp("struct S { @key long id; long val; };");
293        assert!(cpp.contains("// @key"));
294    }
295
296    #[test]
297    fn optional_member_uses_std_optional() {
298        let cpp = gen_cpp("struct S { @optional long maybe; };");
299        assert!(cpp.contains("#include <optional>"));
300        assert!(cpp.contains("std::optional<int32_t>"));
301    }
302
303    #[test]
304    fn exception_inherits_std_exception() {
305        let cpp = gen_cpp("exception NotFound { string what_; };");
306        assert!(cpp.contains("#include <exception>"));
307        assert!(cpp.contains("class NotFound : public std::exception"));
308    }
309
310    #[test]
311    fn union_uses_std_variant() {
312        let cpp = gen_cpp(
313            "union U switch (long) { case 1: long a; case 2: double b; default: octet c; };",
314        );
315        assert!(cpp.contains("#include <variant>"));
316        assert!(cpp.contains("std::variant<"));
317        assert!(cpp.contains("// case default"));
318    }
319
320    #[test]
321    fn time_t_member_maps_to_dds_time_t() {
322        let cpp = gen_cpp("struct S { Time_t t; };");
323        assert!(cpp.contains("DDS::Time_t"));
324    }
325
326    #[test]
327    fn duration_t_member_maps_to_dds_duration_t() {
328        let cpp = gen_cpp("struct S { Duration_t d; };");
329        assert!(cpp.contains("DDS::Duration_t"));
330    }
331
332    #[test]
333    fn reserved_field_name_is_rejected() {
334        let ast = zerodds_idl::parse("struct S { long class_field; };", &ParserConfig::default())
335            .expect("parse");
336        // "class_field" is not reserved; test with an annotation trick:
337        // we force a reserved name via the builder API.
338        // Instead, we check the path directly through check_identifier:
339        let res = type_map::check_identifier("class");
340        assert!(matches!(res, Err(CppGenError::InvalidName { .. })));
341        let _ = ast; // unused but illustrates the idea
342    }
343
344    #[test]
345    fn empty_source_includes_cstdint() {
346        let cpp = gen_cpp("");
347        assert!(cpp.contains("#include <cstdint>"));
348    }
349
350    #[test]
351    fn header_starts_with_generated_marker() {
352        let cpp = gen_cpp("");
353        assert!(cpp.starts_with("// Generated by zerodds idl-cpp."));
354    }
355
356    #[test]
357    fn pragma_once_appears_exactly_once() {
358        let cpp = gen_cpp("module M { struct S { long x; }; };");
359        let count = cpp.matches("#pragma once").count();
360        assert_eq!(count, 1);
361    }
362
363    #[test]
364    fn struct_has_default_constructor() {
365        let cpp = gen_cpp("struct S { long x; };");
366        assert!(cpp.contains("S() = default;"));
367        assert!(cpp.contains("~S() = default;"));
368    }
369
370    #[test]
371    fn struct_has_mutable_and_const_getter() {
372        let cpp = gen_cpp("struct S { long x; };");
373        // Mutable getter: returns int32_t&; const-version present too.
374        assert!(cpp.contains("int32_t& x()"));
375        assert!(cpp.contains("const int32_t& x() const"));
376    }
377
378    #[test]
379    fn struct_has_setter() {
380        let cpp = gen_cpp("struct S { long x; };");
381        assert!(cpp.contains("void x(const int32_t& value)"));
382    }
383
384    #[test]
385    fn struct_has_field_order_constructor() {
386        // The field-order ctor enables brace-init `S t{23, "A7"};` (the
387        // website C++ snippet). Default ctor + accessors stay intact.
388        let cpp = gen_cpp("struct S { long celsius; string sensor_id; };");
389        assert!(
390            cpp.contains("S(int32_t celsius, std::string sensor_id)"),
391            "field-order ctor signature missing:\n{cpp}"
392        );
393        assert!(
394            cpp.contains(": celsius_(std::move(celsius)), sensor_id_(std::move(sensor_id)) {}"),
395            "member-init list missing:\n{cpp}"
396        );
397        // Default ctor must still be present (brace-init coexists with it).
398        assert!(cpp.contains("S() = default;"));
399    }
400
401    #[test]
402    fn zero_field_struct_has_no_field_order_constructor() {
403        // A no-member struct must NOT emit a parameterless ctor — it would be
404        // ambiguous with the defaulted default constructor.
405        let cpp = gen_cpp("struct Empty { };");
406        assert!(cpp.contains("Empty() = default;"));
407        // Only the defaulted ctor line should mention `Empty(`; no extra
408        // `Empty()` redeclaration and no `std::move`.
409        assert_eq!(
410            cpp.matches("Empty(").count(),
411            2, // `Empty() = default;` and `~Empty() = default;` (destructor)
412            "unexpected extra constructor for zero-field struct:\n{cpp}"
413        );
414        assert!(!cpp.contains("std::move"));
415    }
416
417    #[test]
418    fn namespace_prefix_option_wraps_output() {
419        let ast =
420            zerodds_idl::parse("struct S { long x; };", &ParserConfig::default()).expect("parse");
421        let opts = CppGenOptions {
422            namespace_prefix: Some("zerodds".into()),
423            ..Default::default()
424        };
425        let cpp = generate_cpp_header(&ast, &opts).expect("gen");
426        assert!(cpp.contains("namespace zerodds {"));
427        assert!(cpp.contains("} // namespace zerodds"));
428    }
429
430    #[test]
431    fn non_service_interface_emits_pure_virtual_class() {
432        let ast = zerodds_idl::parse("interface I { void op(); };", &ParserConfig::default())
433            .expect("parse");
434        let cpp = generate_cpp_header(&ast, &CppGenOptions::default()).expect("ok");
435        assert!(cpp.contains("class I"));
436        assert!(cpp.contains("virtual ~I()"));
437        assert!(cpp.contains("= 0;"));
438    }
439
440    #[test]
441    fn const_decl_emits_constexpr() {
442        let cpp = gen_cpp("const long MAX = 100;");
443        assert!(cpp.contains("constexpr int32_t MAX = 100;"));
444    }
445
446    #[test]
447    fn options_have_sensible_defaults() {
448        let o = CppGenOptions::default();
449        assert_eq!(o.indent_width, 4);
450        assert!(o.namespace_prefix.is_none());
451        assert!(o.include_guard_prefix.is_none());
452    }
453
454    #[test]
455    fn options_clone_works() {
456        let o = CppGenOptions {
457            namespace_prefix: Some("foo".into()),
458            include_guard_prefix: Some("FOO_".into()),
459            indent_width: 2,
460            emit_amqp_helpers: false,
461            emit_corba_traits: false,
462        };
463        let cloned = o.clone();
464        assert_eq!(cloned.indent_width, 2);
465        assert_eq!(cloned.namespace_prefix.as_deref(), Some("foo"));
466    }
467}