Skip to main content

zerodds_lang_rules/adapter/
cpp_classic.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The classic OMG IDL to C++ 1.3 mapping of topic types, as a layer over the native one.
4//!
5//! [`CppMapping::Classic`](super::cpp::CppMapping::Classic) keeps the wire codec, the
6//! `TypeObject` and the endpoint gates where they are, as `CppMapping::Psm` does, and gives the
7//! application the types the classic mapping names, in C++03:
8//!
9//! | IDL | native (`zd_native::…`) | classic (the IDL's own namespaces) |
10//! |---|---|---|
11//! | `long`, `unsigned long`, … | `std::int32_t`, … | `CORBA::Long`, `CORBA::ULong`, … |
12//! | `octet`, `boolean`, `char`, `wchar` | `zerodds::wire::Octet`, `bool`, `char`, `std::uint16_t` | `CORBA::Octet`, `CORBA::Boolean`, `CORBA::Char`, `CORBA::WChar` |
13//! | `string`, `wstring` | `std::string` | `CORBA::String_var`, `CORBA::WString_var` |
14//! | `sequence<T>` | `std::vector<T>` | `zerodds::classic::Sequence<T>` |
15//! | `T a[2][3]` | `std::array<std::array<T, 3>, 2>` | `T a[2][3]` |
16//! | `map<K,V>` | `zerodds::Map<K, V>` | `zerodds::Map<K, V>` (a string key is a `std::string`) |
17//! | `@optional T m` | `T *`, NULL when absent | `zerodds::classic::Optional<T>` |
18//! | `@external T m` | `std::shared_ptr<T>` | `zerodds::classic::Var<T>` |
19//! | `struct` | struct, the base a public base class | struct with public members, `S_var` |
20//! | `union` | `_d()` and one accessor per case | class: `_d()`, one accessor per case |
21//! | `enum` | `enum class` | plain `enum`, the values written out |
22//! | `bitmask` | an unsigned integer and constants | the same |
23//! | `typedef` | `using` | `typedef` |
24//!
25//! The classic declarations stand between the markers `ZD-CLASSIC-BEGIN` and `ZD-CLASSIC-END` and
26//! are C++03: they include `zerodds/classic.hpp` and nothing newer. The native types, the codec and
27//! `zerodds::classic::Conv` after them are C++17. `Conv<C>` (`zerodds/classic/convert.hpp`) names
28//! the native type of each classic type and converts both ways; the `topic_type_support` of a
29//! classic type calls the native one's `encode` and `decode` through it, so a classic sample takes
30//! the same bytes, the same `TypeObject` and the same type name as the native one.
31//!
32//! Not remapped, and left to the native types by name: constants, exceptions. Refused by name,
33//! because the classic mapping has no form for them: `fixed`, `bitset`, value types, `any`, and an
34//! array as the element of a sequence.
35
36use super::cpp::{cxx_name, module_names};
37use super::cpp_psm::{in_namespaces, native_path, psm_path};
38use crate::adapter::c::ordered_types;
39use crate::adapter::cst::MaterialiseError;
40use crate::default_common::resolve_alias;
41use zerodds_ast::{Ast, CaseLabel, CharWidth, MemberNode, PrimitiveKind, TypeKind, TypeNode};
42use zerodds_ast_core::Id;
43
44/// The marker that opens the C++03 part of a classic header.
45pub const BEGIN_MARKER: &str = "ZD-CLASSIC-BEGIN";
46/// The marker that closes it.
47pub const END_MARKER: &str = "ZD-CLASSIC-END";
48
49fn no_form(construct: &str, at: &str) -> MaterialiseError {
50    MaterialiseError::NoLanguageForm {
51        construct: construct.to_string(),
52        language: "C++ (classic mapping)",
53        at: at.to_string(),
54    }
55}
56
57const MAX_DEPTH: usize = 64;
58
59/// A classic type as it stands in a declaration: the type and, for an array, the dimensions
60/// that follow the declared name.
61struct Spelled {
62    type_: String,
63    dims: String,
64}
65
66impl Spelled {
67    fn plain(type_: String) -> Self {
68        Self {
69            type_,
70            dims: String::new(),
71        }
72    }
73
74    /// `T name[2][3]`.
75    fn declare(&self, name: &str) -> String {
76        format!("{} {name}{}", self.type_, self.dims)
77    }
78}
79
80const fn primitive(kind: PrimitiveKind) -> &'static str {
81    match kind {
82        PrimitiveKind::Boolean => "::CORBA::Boolean",
83        PrimitiveKind::Char => "::CORBA::Char",
84        PrimitiveKind::WChar => "::CORBA::WChar",
85        PrimitiveKind::Octet => "::CORBA::Octet",
86        PrimitiveKind::UInt8 => "::CORBA::UInt8",
87        PrimitiveKind::Int8 => "::CORBA::Int8",
88        PrimitiveKind::Int16 => "::CORBA::Short",
89        PrimitiveKind::UInt16 => "::CORBA::UShort",
90        PrimitiveKind::Int32 => "::CORBA::Long",
91        PrimitiveKind::UInt32 => "::CORBA::ULong",
92        PrimitiveKind::Int64 => "::CORBA::LongLong",
93        PrimitiveKind::UInt64 => "::CORBA::ULongLong",
94        PrimitiveKind::Float32 => "::CORBA::Float",
95        PrimitiveKind::Float64 => "::CORBA::Double",
96        PrimitiveKind::Float128 => "::CORBA::LongDouble",
97    }
98}
99
100/// The classic spelling of the type `id`.
101///
102/// # Errors
103/// A construct the classic mapping has no form for, or a type nested too deep.
104fn spell(ast: &Ast, id: Id<TypeNode>, at: &str, key: bool) -> Result<Spelled, MaterialiseError> {
105    spell_at(ast, id, at, key, 0)
106}
107
108/// zerodds-lint: recursion-depth 64 (element, key and value types; bounded by `MAX_DEPTH`)
109fn spell_at(
110    ast: &Ast,
111    id: Id<TypeNode>,
112    at: &str,
113    key: bool,
114    depth: usize,
115) -> Result<Spelled, MaterialiseError> {
116    if depth > MAX_DEPTH {
117        return Err(MaterialiseError::Unevaluated {
118            construct: format!("type spelling — it nests past {MAX_DEPTH}"),
119            at: at.to_string(),
120        });
121    }
122    let Ok(node) = ast.type_(id) else {
123        return Err(no_form("a type the model would not hand over", at));
124    };
125    let inner =
126        |r: &zerodds_ast_core::Ref<TypeNode>, key: bool| -> Result<String, MaterialiseError> {
127            let Some(target) = r.resolved() else {
128                return Err(no_form("an unresolved element type", at));
129            };
130            let spelled = spell_at(ast, target, at, key, depth + 1)?;
131            if spelled.dims.is_empty() {
132                Ok(spelled.type_)
133            } else {
134                Err(no_form(
135                    "an array as the element of a sequence or a map, which has no slice type here",
136                    at,
137                ))
138            }
139        };
140    Ok(match &node.kind {
141        TypeKind::Primitive(kind) => Spelled::plain(primitive(*kind).to_string()),
142        // A string key is a `std::string`: `String_var` converts to `const char*` and would be
143        // compared by address.
144        TypeKind::String { .. } if key => Spelled::plain("std::string".to_string()),
145        TypeKind::String {
146            width: CharWidth::WChar,
147            ..
148        } => Spelled::plain("::CORBA::WString_var".to_string()),
149        TypeKind::String { .. } => Spelled::plain("::CORBA::String_var".to_string()),
150        TypeKind::Fixed { .. } => {
151            return Err(no_form(
152                "a fixed-point type; the classic mapping's CORBA::Fixed is not provided",
153                at,
154            ));
155        }
156        TypeKind::Sequence { element, .. } => Spelled::plain(format!(
157            "::zerodds::classic::Sequence< {} >",
158            inner(element, false)?
159        )),
160        TypeKind::Array { element, dims } => {
161            let element = inner(element, false)?;
162            let mut suffix = String::new();
163            for dim in dims {
164                let length =
165                    ast.eval_integral(*dim)
166                        .map_err(|_| MaterialiseError::Unevaluated {
167                            construct: "array length".to_string(),
168                            at: at.to_string(),
169                        })?;
170                suffix.push_str(&format!("[{length}]"));
171            }
172            Spelled {
173                type_: element,
174                dims: suffix,
175            }
176        }
177        TypeKind::Map {
178            key: k,
179            value,
180            bound,
181        } => {
182            let bound = match bound.expr() {
183                Some(expr) => format!(
184                    ", {}",
185                    ast.eval_integral(expr)
186                        .map_err(|_| MaterialiseError::Unevaluated {
187                            construct: "map bound".to_string(),
188                            at: at.to_string(),
189                        })?
190                ),
191                None => String::new(),
192            };
193            Spelled::plain(format!(
194                "::zerodds::Map< {}, {}{bound} >",
195                inner(k, true)?,
196                inner(value, false)?
197            ))
198        }
199        TypeKind::Alias { aliased } if node.name.is_empty() => {
200            let Some(target) = aliased.resolved() else {
201                return Err(no_form("an unresolved typedef", at));
202            };
203            spell_at(ast, target, at, key, depth + 1)?
204        }
205        TypeKind::Structure { .. }
206        | TypeKind::Union { .. }
207        | TypeKind::Enumeration { .. }
208        | TypeKind::Bitmask { .. }
209        | TypeKind::Alias { .. } => Spelled::plain(psm_path(ast, id)),
210        _ => return Err(no_form("this construct", at)),
211    })
212}
213
214/// A member's declaration, its classic type, and the default its constructor gives it.
215struct Member {
216    name: String,
217    spelled: Spelled,
218    /// The mem-initialiser: value-initialisation, or the first enumerator of an enumeration.
219    init: String,
220}
221
222fn enumerator_path(ast: &Ast, enumeration: Id<TypeNode>, enumerator: &str) -> Option<String> {
223    let node = ast.type_(enumeration).ok()?;
224    let chain = module_names(ast, node.module).ok()?;
225    let mut path = String::new();
226    for module in chain {
227        path.push_str(&format!("::{module}"));
228    }
229    Some(format!("{path}::{}", cxx_name(enumerator)))
230}
231
232fn member(ast: &Ast, member: &MemberNode, owner: &str) -> Result<Member, MaterialiseError> {
233    let Some(target) = member.type_.resolved() else {
234        return Err(MaterialiseError::Unevaluated {
235            construct: "member type".to_string(),
236            at: format!("{owner}::{}", member.name),
237        });
238    };
239    let name = cxx_name(&member.name);
240    let inner = spell(ast, target, &member.name, false)?;
241    // The same precedence as the native struct (`cpp::held`): `@external` first, then
242    // `@optional`, then a member that only closes a cycle.
243    let held = super::cpp::held(ast, member);
244    let (spelled, init) = if held == super::cpp::Held::Shared {
245        (
246            Spelled::plain(format!("::zerodds::classic::Var< {} >", inner.type_)),
247            format!("{name}()"),
248        )
249    } else if held == super::cpp::Held::Plain {
250        (
251            Spelled::plain(format!("::zerodds::classic::Optional< {} >", inner.type_)),
252            format!("{name}()"),
253        )
254    } else {
255        // Table 9: an enumeration starts at its first enumerator, not at zero.
256        let resolved = resolve_alias(ast, target);
257        let first = matches!(
258            ast.type_(resolved).map(|node| &node.kind),
259            Ok(TypeKind::Enumeration { .. })
260        )
261        .then(|| crate::emit::default_enumerator_name(ast, resolved))
262        .flatten()
263        .and_then(|first| enumerator_path(ast, resolved, &first));
264        let init = match (first, inner.dims.is_empty()) {
265            (Some(first), true) => format!("{name}({first})"),
266            _ => format!("{name}()"),
267        };
268        (inner, init)
269    };
270    Ok(Member {
271        name,
272        spelled,
273        init,
274    })
275}
276
277/// The struct of a structure, its `_var`, and the specialisation of `zerodds::classic::Conv`.
278fn structure(
279    ast: &Ast,
280    id: Id<TypeNode>,
281    node: &TypeNode,
282) -> Result<(String, String), MaterialiseError> {
283    let name = cxx_name(&node.name);
284    let base = ast
285        .base_of(id)
286        .filter(|_| matches!(node.kind, TypeKind::Structure { .. }));
287    let mut own = Vec::new();
288    for handle in ast.members_of(id) {
289        let Ok(m) = ast.member(*handle) else {
290            return Err(MaterialiseError::NoNode {
291                kind: "member",
292                at: node.name.clone(),
293            });
294        };
295        own.push(member(ast, m, &node.name)?);
296    }
297    let mut text = format!(
298        "/// `{}` as the classic mapping has it: public members.\nstruct {name}{} {{\n",
299        node.name,
300        base.map_or(String::new(), |b| format!(" : public {}", psm_path(ast, b))),
301    );
302    for m in &own {
303        text.push_str(&format!("    {};\n", m.spelled.declare(&m.name)));
304    }
305    if own.is_empty() {
306        text.push_str(&format!("    {name}() {{}}\n"));
307    } else {
308        let inits: Vec<&str> = own.iter().map(|m| m.init.as_str()).collect();
309        text.push_str(&format!("    {name}() : {} {{}}\n", inits.join(", ")));
310    }
311    text.push_str(&format!(
312        "}};\ntypedef ::zerodds::classic::Var< {name} > {name}_var;\n"
313    ));
314
315    // The conversion: every member, the base's first, by name.
316    let path = psm_path(ast, id);
317    let native = native_path(ast, id);
318    let mut all = Vec::new();
319    for handle in ast.all_members_of(id) {
320        let Ok(m) = ast.member(handle) else {
321            return Err(MaterialiseError::NoNode {
322                kind: "member",
323                at: node.name.clone(),
324            });
325        };
326        all.push(member(ast, m, &node.name)?);
327    }
328    let mut conv = format!(
329        "template <>\nstruct Conv<{path}> {{\n    typedef {native} Native;\n    static Native to_native(const {path}& v) {{\n        Native n;\n"
330    );
331    for m in &all {
332        conv.push_str(&format!(
333            "        to_native_member(n.{0}, v.{0});\n",
334            m.name
335        ));
336    }
337    conv.push_str(&format!(
338        "        return n;\n    }}\n    static {path} from_native(const Native& n) {{\n        {path} v;\n"
339    ));
340    for m in &all {
341        conv.push_str(&format!(
342            "        from_native_member(v.{0}, n.{0});\n",
343            m.name
344        ));
345    }
346    conv.push_str("        return v;\n    }\n};\n");
347    Ok((text, conv))
348}
349
350/// The class of a union and the specialisation of `zerodds::classic::Conv`.
351fn union_class(
352    ast: &Ast,
353    id: Id<TypeNode>,
354    node: &TypeNode,
355    discriminator: &zerodds_ast_core::Ref<TypeNode>,
356) -> Result<(String, String), MaterialiseError> {
357    let name = cxx_name(&node.name);
358    let Some(disc) = discriminator.resolved() else {
359        return Err(MaterialiseError::Unevaluated {
360            construct: "union discriminator".to_string(),
361            at: node.name.clone(),
362        });
363    };
364    let disc_type = spell(ast, disc, &node.name, false)?.type_;
365    struct Case {
366        name: String,
367        type_: String,
368        label: Option<i128>,
369    }
370    let mut cases: Vec<Case> = Vec::new();
371    for case in ast.cases_of(id) {
372        let Ok(case_node) = ast.case(*case) else {
373            return Err(MaterialiseError::NoNode {
374                kind: "case",
375                at: node.name.clone(),
376            });
377        };
378        let Ok(m) = ast.member(case_node.member) else {
379            return Err(MaterialiseError::NoNode {
380                kind: "member",
381                at: node.name.clone(),
382            });
383        };
384        let mut label = None;
385        for candidate in &case_node.labels {
386            if let CaseLabel::Value(expr) = candidate {
387                label =
388                    Some(
389                        ast.eval_integral(*expr)
390                            .map_err(|_| MaterialiseError::Unevaluated {
391                                construct: "union case label".to_string(),
392                                at: node.name.clone(),
393                            })?,
394                    );
395                break;
396            }
397        }
398        let built = member(ast, m, &node.name)?;
399        if !built.spelled.dims.is_empty() {
400            return Err(no_form("an array as the case of a union", &m.name));
401        }
402        cases.push(Case {
403            name: built.name,
404            type_: built.spelled.type_,
405            label,
406        });
407    }
408    let mut inits = vec!["d_()".to_string()];
409    inits.extend(cases.iter().map(|c| format!("{}_()", c.name)));
410    let mut text = format!(
411        "/// `{}` as the classic mapping has it: `_d()` and an accessor and a modifier per case.\nclass {name} {{\npublic:\n    {name}() : {} {{}}\n\n    {disc_type} _d() const {{ return d_; }}\n    void _d({disc_type} value) {{ d_ = value; }}\n\n",
412        node.name,
413        inits.join(", ")
414    );
415    for case in &cases {
416        let select = case.label.map_or(String::new(), |value| {
417            format!(" d_ = static_cast< {disc_type} >({value});")
418        });
419        text.push_str(&format!(
420            "    const {0}& {1}() const {{ return {1}_; }}\n    {0}& {1}() {{ return {1}_; }}\n    void {1}(const {0}& value) {{ {1}_ = value;{select} }}\n",
421            case.type_, case.name
422        ));
423    }
424    text.push_str(&format!("\nprivate:\n    {disc_type} d_;\n"));
425    for case in &cases {
426        text.push_str(&format!("    {} {}_;\n", case.type_, case.name));
427    }
428    text.push_str(&format!(
429        "}};\ntypedef ::zerodds::classic::Var< {name} > {name}_var;\n"
430    ));
431
432    let path = psm_path(ast, id);
433    let native = native_path(ast, id);
434    let mut conv = format!(
435        "template <>\nstruct Conv<{path}> {{\n    typedef {native} Native;\n    static Native to_native(const {path}& v) {{\n        Native n;\n"
436    );
437    // The cases first: a setter selects its own discriminator value; the discriminator last.
438    for case in &cases {
439        conv.push_str(&format!(
440            "        {{\n            typedef std::remove_cv<std::remove_reference<decltype(n.{0}())>::type>::type Member;\n            Member member{{}};\n            to_native_member(member, v.{0}());\n            n.{0}(member);\n        }}\n",
441            case.name
442        ));
443    }
444    conv.push_str(&format!(
445        "        n._d(Conv<{disc_type}>::to_native(v._d()));\n        return n;\n    }}\n    static {path} from_native(const Native& n) {{\n        {path} v;\n"
446    ));
447    for case in &cases {
448        conv.push_str(&format!(
449            "        {{\n            {1} member;\n            from_native_member(member, n.{0}());\n            v.{0}(member);\n        }}\n",
450            case.name, case.type_
451        ));
452    }
453    conv.push_str(&format!(
454        "        v._d(Conv<{disc_type}>::from_native(n._d()));\n        return v;\n    }}\n}};\n"
455    ));
456    Ok((text, conv))
457}
458
459/// The text of the classic layer for `ast`: the C++03 declarations between the markers, and the
460/// `zerodds::classic::Conv` specialisations after them.
461///
462/// # Errors
463/// A construct the classic mapping has no form for, or a model that contradicts itself.
464pub fn classic_layer(ast: &Ast) -> Result<String, MaterialiseError> {
465    let order = ordered_types(ast);
466    let pair = crate::emit::type_pair_cpp::has_pair_types(ast);
467    let runtime_defined = |node: &TypeNode| -> Result<bool, MaterialiseError> {
468        Ok(pair
469            && crate::emit::type_pair_cpp::is_runtime_defined(
470                &module_names(ast, node.module)?,
471                &node.name,
472            ))
473    };
474    let mut forward = String::new();
475    let mut types = String::new();
476    let mut conversions = String::new();
477
478    for id in &order {
479        let Ok(node) = ast.type_(*id) else {
480            return Err(no_form(
481                "a type the model would not hand over",
482                "forward declarations",
483            ));
484        };
485        if node.name.is_empty() || runtime_defined(node)? {
486            continue;
487        }
488        if matches!(
489            node.kind,
490            TypeKind::Structure { .. } | TypeKind::Union { .. }
491        ) {
492            let chain = module_names(ast, node.module)?;
493            let keyword = if matches!(node.kind, TypeKind::Union { .. }) {
494                "class"
495            } else {
496                "struct"
497            };
498            forward.push_str(&in_namespaces(
499                &chain,
500                &format!("{keyword} {};", cxx_name(&node.name)),
501            ));
502        }
503    }
504    for id in order {
505        let Ok(node) = ast.type_(id) else {
506            return Err(no_form(
507                "a type the model would not hand over",
508                "type declarations",
509            ));
510        };
511        if node.name.is_empty() || runtime_defined(node)? {
512            continue;
513        }
514        let chain = module_names(ast, node.module)?;
515        let name = cxx_name(&node.name);
516        let (text, conv) = match &node.kind {
517            TypeKind::Structure { .. } => {
518                let (class, conv) = structure(ast, id, node)?;
519                (class, Some(conv))
520            }
521            TypeKind::Union { discriminator, .. } => {
522                let (class, conv) = union_class(ast, id, node, discriminator)?;
523                (class, Some(conv))
524            }
525            TypeKind::Enumeration { .. } => {
526                let mut values = Vec::new();
527                for enumerator in ast.enumerators_of(id) {
528                    let Ok(e) = ast.enumerator(*enumerator) else {
529                        return Err(MaterialiseError::NoNode {
530                            kind: "enumerator",
531                            at: node.name.clone(),
532                        });
533                    };
534                    let value = ast.effective_enumerator_value(*enumerator).ok_or_else(|| {
535                        MaterialiseError::Unevaluated {
536                            construct: "enumerator value".to_string(),
537                            at: e.name.clone(),
538                        }
539                    })?;
540                    if i32::try_from(value).is_err() {
541                        return Err(no_form(
542                            "an enumerator beyond 32 signed bits, which a C++03 enumeration does not hold",
543                            &e.name,
544                        ));
545                    }
546                    values.push(format!("    {} = {value}", cxx_name(&e.name)));
547                }
548                let native = native_path(ast, id);
549                let path = psm_path(ast, id);
550                let conv = format!(
551                    "template <>\nstruct Conv<{path}> {{\n    typedef {native} Native;\n    typedef std::underlying_type<Native>::type Underlying;\n    static Native to_native({path} v) {{ return static_cast<Native>(static_cast<Underlying>(v)); }}\n    static {path} from_native(Native n) {{ return static_cast<{path}>(static_cast<Underlying>(n)); }}\n}};\n"
552                );
553                (
554                    format!(
555                        "/// An enumeration, with the numbering of the IDL.\nenum {name} {{\n{}\n}};\n",
556                        values.join(",\n")
557                    ),
558                    Some(conv),
559                )
560            }
561            TypeKind::Bitmask { bits } => {
562                let holder = match ast.effective_bit_bound(id) {
563                    0..=8 => "::CORBA::Octet",
564                    9..=16 => "::CORBA::UShort",
565                    17..=32 => "::CORBA::ULong",
566                    _ => "::CORBA::ULongLong",
567                };
568                let mut text = format!(
569                    "/// A bitmask, as the unsigned integer of its bound.\ntypedef {holder} {name};\n"
570                );
571                for bit in bits {
572                    let Ok(b) = ast.bit_value(*bit) else {
573                        return Err(MaterialiseError::NoNode {
574                            kind: "bit",
575                            at: node.name.clone(),
576                        });
577                    };
578                    let position = ast.effective_bit_position(*bit).ok_or_else(|| {
579                        MaterialiseError::Unevaluated {
580                            construct: "bit position".to_string(),
581                            at: b.name.clone(),
582                        }
583                    })?;
584                    text.push_str(&format!(
585                        "const {name} {} ZD_CLASSIC_CONSTANT = static_cast< {name} >({}ULL << {position});\n",
586                        cxx_name(&b.name),
587                        1
588                    ));
589                }
590                (text, None)
591            }
592            TypeKind::Alias { aliased } => {
593                let Some(target) = aliased.resolved() else {
594                    return Err(no_form("an unresolved typedef", &node.name));
595                };
596                let spelled = spell(ast, target, &node.name, false)?;
597                (format!("typedef {};\n", spelled.declare(&name)), None)
598            }
599            TypeKind::Bitset { .. } => {
600                return Err(no_form("a bit set has no classic mapping", &node.name));
601            }
602            TypeKind::Native
603            | TypeKind::ValueBox { .. }
604            | TypeKind::Valuetype { .. }
605            | TypeKind::ValueBase
606            | TypeKind::ObjectRef { .. } => {
607                return Err(no_form(
608                    "a native type, value box, value type or interface has no classic mapping here",
609                    &node.name,
610                ));
611            }
612            // Spellings of anonymous constructs (a typedef names them), `any`, and annotation
613            // declarations, which carry no data: nothing to declare.
614            TypeKind::Primitive(_)
615            | TypeKind::Fixed { .. }
616            | TypeKind::String { .. }
617            | TypeKind::Sequence { .. }
618            | TypeKind::Array { .. }
619            | TypeKind::Map { .. }
620            | TypeKind::Any
621            | TypeKind::AnnotationType { .. } => continue,
622        };
623        // DDS-XTypes 1.3 §7.5.1.3.2.1 and §7.5.1.3.3: beside every declared type the three
624        // map classes of the clause.
625        let mut text = text;
626        if matches!(
627            node.kind,
628            TypeKind::Structure { .. }
629                | TypeKind::Union { .. }
630                | TypeKind::Enumeration { .. }
631                | TypeKind::Bitmask { .. }
632                | TypeKind::Alias { .. }
633        ) {
634            for (label, key) in [
635                ("UInt32", "::CORBA::ULong"),
636                ("UInt64", "::CORBA::ULongLong"),
637                ("String", "std::string"),
638            ] {
639                text.push_str(&format!(
640                    "typedef ::zerodds::Map< {key}, {name} > {label}{name}Map;\n"
641                ));
642            }
643        }
644        types.push_str(&in_namespaces(&chain, &text));
645        types.push('\n');
646        if let Some(conv) = conv {
647            conversions.push_str(&conv);
648            conversions.push('\n');
649        }
650    }
651    Ok(format!(
652        "// ---- The classic layer: IDL to C++ 1.3 over the native types above. ----\n\
653         // The native types are in `zd_native`; these are what an application writes.\n\
654         // `zerodds::classic::Conv` hands them to the codec.\n\n\
655         // {BEGIN_MARKER} (C++03 from here)\n\
656         #include <string>\n\
657         #include \"zerodds/classic.hpp\"\n\
658         #include \"zerodds/map.hpp\"\n\
659         // A constant at namespace scope has internal linkage; a translation unit that does not use\n\
660         // it is not warned about it.\n\
661         #if defined(__GNUC__)\n\
662         #define ZD_CLASSIC_CONSTANT __attribute__((unused))\n\
663         #else\n\
664         #define ZD_CLASSIC_CONSTANT\n\
665         #endif\n\
666         {forward}\n{types}\
667         // {END_MARKER}\n\n\
668         #include \"zerodds/classic/convert.hpp\"\n\
669         namespace zerodds {{\nnamespace classic {{\n\n{conversions}}} // namespace classic\n}} // namespace zerodds\n"
670    ))
671}