Skip to main content

zerodds_lang_rules/emit/
interface_cpp.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! An IDL `interface`, as the C++ language binding DDS-RPC 1.0 §7.11.1
4//! specifies.
5//!
6//! `adapter::interface` reads the interface; this writes the C++. The
7//! split is the one every column in this crate keeps — one reading of
8//! the IDL clause, one spelling per language — and here it earns itself
9//! twice over, because §7.11.2.3 makes the Java function-call binding
10//! *the same entities as C++* and `emit::interface_java` therefore walks
11//! the same [`Service`] values.
12//!
13//! ## What comes out
14//!
15//! Per interface `I`, in the namespace its module names:
16//!
17//! - the topic types of §7.5.1.1.4-7 — `I_<op>_In`, `I_<op>_Out`,
18//!   `I_Call`, `I_Return`, `I_Request`, `I_Reply` — whose names come
19//!   from `adapter::interface` and whose shape is
20//!   `crates/rpc/src/codegen.rs`'s;
21//! - `ISupport`, so §7.11.1.1.2 rule 3's `SupportType` typedef names
22//!   something that exists;
23//! - `I` and `IAsync`, the two abstract classes of rules 1-7;
24//! - `IRequester` and `IReplier`, the typed aliases of §7.11.1.1.1 over
25//!   the generic `dds::rpc::Requester` / `dds::rpc::Replier` templates;
26//! - `IClient` and `IService`, the function-call entities of §7.11.1.5.
27//!
28//! ## Reserved words
29//!
30//! §7.11.1.1.2 rule 8 prefixes an identifier that is a C++ reserved word
31//! with `cxx_`. This binding does that for every identifier it introduces:
32//! the interface, its operations, its parameters. The type declarations
33//! come from `adapter::cpp`, which gives a struct, an enumeration, an
34//! exception or a module named by a reserved word the same prefix
35//! (`adapter::cpp::cxx_name`); this binding refers to them by that spelling.
36//!
37//! ## What this does not do
38//!
39//! It does not implement `dds::rpc`. §7.11.1.4 lists some twenty class
40//! templates — `Requester`, `Replier`, `future`, `LoanedSamples` and the
41//! rest — which are the C++ PSM's runtime and not a per-service
42//! artefact. What is generated here uses them: `IClient`'s operations
43//! call through a `Requester`, `IService::dispatch` answers through a
44//! `Replier`, and the topic types have the codec the two ask for
45//! (`mod codec`, `mod calls`).
46
47mod calls;
48mod codec;
49mod enhanced;
50mod enhanced_codec;
51
52use crate::adapter::cpp::{
53    CppMapping, NATIVE_NAMESPACE, cpp_mapping, cpp_primitive, cpp_value_type, module_names,
54    nest_by_module, nest_by_module_placed,
55};
56use crate::adapter::cpp_psm::psm_type;
57use crate::adapter::interface::{
58    self, BindingError, Operation, Origin, Parameter, RETURN_MEMBER, Service, ServiceMapping,
59};
60use zerodds_ast::{Ast, ModuleNode, ParamDirection, PrimitiveKind, TypeKind, TypeNode};
61use zerodds_ast_core::Id;
62use zerodds_lang_cst::common::{Doc, Ident, Path};
63use zerodds_lang_cst::cpp;
64
65/// The namespace the DDS-RPC C++ runtime lives in. DDS-RPC 1.0
66/// §7.11.1.4.1: "The `dds`, and `dds::rpc` namespaces define the classes
67/// and functions for the request-reply style language bindings."
68const RPC: &str = "dds::rpc";
69
70/// How deep an alias chain is followed before the model is taken to hold
71/// a cycle. The same bound `adapter::cpp` uses for a type spelling.
72const MAX_ALIAS_DEPTH: usize = 64;
73
74/// The declarations an interface produces, each with the module whose
75/// namespace it stands in and whether it is a topic type.
76///
77/// Under `--cpp-mapping psm` the topic types (the `In`, `Out`, `Result`,
78/// `Call`, `Return`, `Request` and `Reply` structures and the case labels)
79/// are the native types the codec reads and writes: they stand in
80/// `zd_native` (DDS-PSM-Cxx 1.0 §8.1). The interfaces, the clients and the
81/// services are the application's and stand in the namespaces of the IDL,
82/// which see the topic types through a `using` declaration each.
83pub(super) struct Sink {
84    items: Vec<(Id<ModuleNode>, bool, cpp::Decl)>,
85    /// Topic types the application's namespace does not see through a
86    /// `using` declaration, because it declares a type of its own by that name.
87    unseen: Vec<(Id<ModuleNode>, String)>,
88}
89
90impl Sink {
91    fn new() -> Self {
92        Self {
93            items: Vec::new(),
94            unseen: Vec::new(),
95        }
96    }
97
98    /// A topic type the application's namespace declares its own type for
99    /// (the `Out` structure under the PSM mapping, whose members the
100    /// application sees as Table 7.1 spells them): no `using` for it.
101    pub(super) fn push_topic_unseen(&mut self, item: (Id<ModuleNode>, cpp::Decl)) {
102        if let Some(topic) = topic_name(&item.1) {
103            self.unseen.push((item.0, topic));
104        }
105        self.items.push((item.0, true, item.1));
106    }
107
108    /// A declaration of the application, or one at file scope.
109    pub(super) fn push(&mut self, item: (Id<ModuleNode>, cpp::Decl)) {
110        self.items.push((item.0, false, item.1));
111    }
112
113    /// A topic type or a case label.
114    pub(super) fn push_topic(&mut self, item: (Id<ModuleNode>, cpp::Decl)) {
115        self.items.push((item.0, true, item.1));
116    }
117
118    /// The declarations, nested into the namespaces of their modules.
119    fn nest(self, ast: &Ast) -> Result<Vec<cpp::Decl>, BindingError> {
120        if !is_psm() {
121            let plain = self
122                .items
123                .into_iter()
124                .map(|(module, _, decl)| (module, decl))
125                .collect();
126            return nest_by_module(ast, plain).map_err(BindingError::from);
127        }
128        let mut placed = Vec::with_capacity(self.items.len());
129        let unseen = self.unseen;
130        for (module, native, decl) in self.items {
131            let using = if native {
132                topic_name(&decl)
133                    .filter(|topic| {
134                        !unseen
135                            .iter()
136                            .any(|(at, name)| *at == module && name == topic)
137                    })
138                    .map(|topic| {
139                        let scope = module_names(ast, module)?;
140                        let mut path = format!("::{NATIVE_NAMESPACE}");
141                        for segment in scope {
142                            path.push_str("::");
143                            path.push_str(&segment);
144                        }
145                        Ok::<_, crate::adapter::cst::MaterialiseError>(cpp::Decl::Verbatim(
146                            format!("using {path}::{topic};"),
147                        ))
148                    })
149                    .transpose()?
150            } else {
151                None
152            };
153            placed.push((module, native, decl));
154            if let Some(using) = using {
155                placed.push((module, false, using));
156            }
157        }
158        nest_by_module_placed(ast, placed).map_err(BindingError::from)
159    }
160}
161
162/// The name a topic type or a case label is declared under.
163fn topic_name(decl: &cpp::Decl) -> Option<String> {
164    if let cpp::Decl::Class(class) = decl {
165        return Some(class.name.as_str().to_string());
166    }
167    if let cpp::Decl::Variable(variable) = decl {
168        return Some(variable.name.as_str().to_string());
169    }
170    None
171}
172
173/// The C++ declarations every interface of `ast` produces.
174///
175/// The list is module-nested, exactly as [`crate::adapter::cpp::cpp_items`]
176/// returns its own, so a caller appends the two and renders one file.
177///
178/// # Errors
179/// [`BindingError`] — the interface did not reach the seam, or a type in
180/// it has no C++ spelling. Never a dropped operation.
181pub fn cpp_interface_items(ast: &Ast) -> Result<Vec<cpp::Decl>, BindingError> {
182    cpp_interface_items_with(ast, ServiceMapping::Basic)
183}
184
185/// The types the operations of the remote interfaces of `ast` carry (the
186/// parameters, the return values and the members of the exceptions they
187/// raise), each once: the types whose codecs a binding of the interfaces
188/// calls, in any language.
189///
190/// # Errors
191/// [`BindingError`] — the interface did not reach the seam.
192pub fn remote_member_types(ast: &Ast) -> Result<Vec<Id<TypeNode>>, BindingError> {
193    let remote: Vec<Service> = interface::services(ast)?
194        .into_iter()
195        .filter(Service::is_remote)
196        .collect();
197    codec::member_types(ast, &remote)
198}
199
200/// [`cpp_interface_items`] for the given service mapping.
201///
202/// [`ServiceMapping::Enhanced`] writes the topic types of DDS-RPC 1.0
203/// §7.5.1.2 — the `@Choice @Autoid` `Request` and `Reply` structures and
204/// the `In`, `Out` and `Result` structures — their PL_CDR2 codec, the
205/// interface classes and the function-call classes over the structures
206/// (`${I}Client`, `${I}Service`); the Basic mapping's `Call` union is not
207/// written.
208///
209/// # Errors
210/// As [`cpp_interface_items`].
211pub fn cpp_interface_items_with(
212    ast: &Ast,
213    mapping: ServiceMapping,
214) -> Result<Vec<cpp::Decl>, BindingError> {
215    let mut out = Sink::new();
216    let services = interface::services(ast)?;
217    let root = ast.root_module();
218    if services.iter().any(Service::is_remote) {
219        // The classes below name `dds::rpc` and the topic types' codecs
220        // name `zerodds::wire`; a header that includes neither is not one a
221        // consumer can include on its own.
222        out.push((
223            root,
224            cpp::Decl::Verbatim("#include <dds/rpc.hpp>".to_string()),
225        ));
226        let remote: Vec<Service> = services
227            .iter()
228            .filter(|service| service.is_remote())
229            .cloned()
230            .collect();
231        for decl in codec::member_codecs(ast, &remote)? {
232            out.push((root, decl));
233        }
234        // The Basic mapping is classic CDR unless the params say XCDR2
235        // (§7.5.1.1): a nested `@appendable` or `@mutable` member is
236        // written differently there, so the codecs exist under both.
237        if !mapping.is_enhanced() {
238            let classic = codec::member_codecs_xcdr1(ast, &remote)?;
239            if !classic.is_empty() {
240                out.push((root, codec::in_xcdr1_namespace(classic)));
241            }
242        }
243    }
244    // §7.5.1.1.5 rule 4a names the constant after the exception alone, so
245    // two interfaces of one module that raise it share the one constant.
246    let mut declared_hashes: Vec<(Id<ModuleNode>, String)> = Vec::new();
247    let mut declared_exceptions = Vec::new();
248    for service in services {
249        if mapping.is_enhanced() {
250            enhanced::declare(
251                ast,
252                &Service { mapping, ..service },
253                &mut out,
254                &mut declared_exceptions,
255            )?;
256        } else {
257            declare(ast, &service, &mut out, &mut declared_hashes)?;
258        }
259    }
260    out.nest(ast)
261}
262
263/// The C++ declarations one interface produces.
264///
265/// # Errors
266/// As [`cpp_interface_items`].
267pub fn cpp_service_items(ast: &Ast, service: &Service) -> Result<Vec<cpp::Decl>, BindingError> {
268    let mut out = Sink::new();
269    declare(ast, service, &mut out, &mut Vec::new())?;
270    out.nest(ast)
271}
272
273fn declare(
274    ast: &Ast,
275    service: &Service,
276    out: &mut Sink,
277    declared_hashes: &mut Vec<(Id<ModuleNode>, String)>,
278) -> Result<(), BindingError> {
279    let module = service.module;
280    let name = cxx_escape(&service.name);
281    let operations = service.all_operations();
282
283    // The topic types first: everything below names them, and a C++
284    // class whose member names a class the compiler has not seen does
285    // not compile.
286    for operation in &operations {
287        out.push_topic((
288            module,
289            cpp::Decl::Class(struct_of(
290                &interface::in_struct_name(&name, &cxx_escape(&operation.name)),
291                request_fields(ast, service, operation)?,
292                Doc::line("The `in` and `inout` parameters of one call. DDS-RPC 1.0 §7.5.1.1.4."),
293            )),
294        ));
295        let out_struct = (
296            module,
297            cpp::Decl::Class(struct_of(
298                &interface::out_struct_name(&name, &cxx_escape(&operation.name)),
299                reply_fields(ast, service, operation)?,
300                Doc::line(
301                    "The return value and the `out` and `inout` parameters of one call. DDS-RPC 1.0 §7.5.1.1.5.",
302                ),
303            )),
304        );
305        if is_psm() && operation.has_outputs() {
306            // The `future` of an asynchronous call holds this structure
307            // (rule 7.i), and the application sees its members as Table 7.1
308            // spells them: the topic structure is native, in `zd_native`, and
309            // the application's own `Out` follows it in the IDL's namespace.
310            out.push_topic_unseen(out_struct);
311            out.push((
312                module,
313                cpp::Decl::Class(struct_of(
314                    &interface::out_struct_name(&name, &cxx_escape(&operation.name)),
315                    reply_fields_for_application(ast, service, operation)?,
316                    Doc::line("The return value and the `out` and `inout` parameters of one call, as the application sees them. DDS-RPC 1.0 §7.5.1.1.5 and §7.11.1.1.2 rule 7.i."),
317                )),
318            ));
319        } else {
320            out.push_topic(out_struct);
321        }
322    }
323
324    // §7.5.1.1.6 rule 4 and §7.5.1.1.5 rule 4a: the case labels are
325    // constants beside the unions, one per operation and one per
326    // exception, so a reader of the header can see which value selects
327    // which case without recomputing an MD5.
328    for operation in &operations {
329        let operation_name = cxx_escape(&operation.name);
330        out.push_topic((
331            module,
332            hash_constant(
333                &interface::operation_hash_constant(&name, &operation_name),
334                interface::operation_hash(&operation.name),
335                "The case label of one operation. DDS-RPC 1.0 §7.5.1.1.6 rules 4 and 5a.",
336            ),
337        ));
338    }
339    for operation in &operations {
340        for raised in &operation.raises {
341            let key = (module, raised.scoped_name.clone());
342            if declared_hashes.contains(&key) {
343                continue;
344            }
345            declared_hashes.push(key);
346            out.push_topic((
347                module,
348                hash_constant(
349                    &interface::exception_hash_constant(&cxx_escape(&raised.name)),
350                    interface::exception_hash(&raised.scoped_name),
351                    "The case label of one raised exception. DDS-RPC 1.0 §7.5.1.1.5 rule 4.",
352                ),
353            ));
354        }
355    }
356
357    // §7.5.1.1.5: one `Result` union per operation, over the `Out`
358    // structure and the exceptions the operation raises.
359    for operation in &operations {
360        out.push_topic((
361            module,
362            cpp::Decl::Class(result_union_of(service, &name, operation)),
363        ));
364    }
365
366    out.push_topic((
367        module,
368        cpp::Decl::Class(union_of(
369            &interface::call_union_name(&name),
370            &operations,
371            &name,
372            false,
373            Doc::line("The request union over every operation. DDS-RPC 1.0 §7.5.1.1.6."),
374        )),
375    ));
376    out.push_topic((
377        module,
378        cpp::Decl::Class(union_of(
379            &interface::return_union_name(&name),
380            &operations,
381            &name,
382            true,
383            Doc::line("The reply union. DDS-RPC 1.0 §7.5.1.1.7."),
384        )),
385    ));
386
387    let request = interface::request_type_name(&name);
388    let reply = interface::reply_type_name(&name);
389    out.push_topic((
390        module,
391        cpp::Decl::Class(struct_of(
392            &request,
393            vec![
394                field(qualified(RPC, "RequestHeader"), "header"),
395                field(
396                    cpp::Type::named(Path::single(&interface::call_union_name(&name))),
397                    interface::DATA_MEMBER,
398                ),
399            ],
400            Doc::line("The request topic type. DDS-RPC 1.0 §7.5.1.1.6."),
401        )),
402    ));
403    out.push_topic((
404        module,
405        cpp::Decl::Class(struct_of(
406            &reply,
407            vec![
408                field(qualified(RPC, "ReplyHeader"), "header"),
409                field(
410                    cpp::Type::named(Path::single(&interface::return_union_name(&name))),
411                    interface::DATA_MEMBER,
412                ),
413            ],
414            Doc::line("The reply topic type. DDS-RPC 1.0 §7.5.1.1.7."),
415        )),
416    ));
417    if service.is_remote() {
418        // The octets of the two topic types: what `dds::rpc::Requester` and
419        // `dds::rpc::Replier` ask `dds_type_traits` for.
420        let root = ast.root_module();
421        let synthesized = codec::synthesized_codecs(ast, service, &name, &operations)?;
422        for decl in synthesized.iter().cloned() {
423            out.push((root, decl));
424        }
425        out.push((root, codec::in_xcdr1_namespace(synthesized)));
426        out.push((root, codec::traits(ast, service, &name)?));
427    }
428    out.push((module, cpp::Decl::Class(support_class(service, &name))));
429
430    // `I` names `IAsync` in a typedef and `IAsync` names `I` in one, so
431    // one of the two has to be announced before the other is defined.
432    out.push((
433        module,
434        cpp::Decl::Forward {
435            is_struct: false,
436            name: Ident::new(&format!("{name}Async")),
437        },
438    ));
439    out.push((
440        module,
441        cpp::Decl::Class(sync_interface(ast, service, &name, &operations)?),
442    ));
443    out.push((
444        module,
445        cpp::Decl::Class(async_interface(ast, service, &name, &operations)?),
446    ));
447
448    if service.is_remote() {
449        out.push((module, requester_alias(&name, &request, &reply)));
450        out.push((module, replier_alias(&name, &request, &reply)));
451        out.push((
452            module,
453            cpp::Decl::Class(calls::client_class(
454                ast,
455                service,
456                &name,
457                &operations,
458                calls::Shape::Basic,
459            )?),
460        ));
461        out.push((
462            module,
463            cpp::Decl::Class(calls::service_class(
464                ast,
465                service,
466                &name,
467                &operations,
468                calls::Shape::Basic,
469            )?),
470        ));
471    }
472    Ok(())
473}
474
475// -- the topic types ---------------------------------------------------
476
477fn request_fields(
478    ast: &Ast,
479    service: &Service,
480    operation: &Operation,
481) -> Result<Vec<cpp::ClassMember>, BindingError> {
482    let mut out = Vec::new();
483    for param in operation.inputs() {
484        out.push(field(
485            cpp_value_type(ast, param.type_, &at(service, operation, &param.name))?,
486            &cxx_escape(&param.name),
487        ));
488    }
489    // §7.5.1.1.4 rule 3c: an operation with no `in`/`inout` parameters
490    // carries a single `dummy` of type `dds::rpc::UnusedMember`, not an
491    // empty structure — XCDR gives an empty structure no octets, and a
492    // reader cannot tell a sample that arrived from one that did not.
493    if out.is_empty() {
494        out.push(field(
495            qualified(RPC, "UnusedMember"),
496            interface::DUMMY_MEMBER,
497        ));
498    }
499    Ok(out)
500}
501
502/// The members of the `Out` structure as the application sees them under
503/// the PSM mapping: [`reply_fields`] with Table 7.1's spellings.
504fn reply_fields_for_application(
505    ast: &Ast,
506    service: &Service,
507    operation: &Operation,
508) -> Result<Vec<cpp::ClassMember>, BindingError> {
509    let mut out = Vec::new();
510    for param in operation.outputs() {
511        out.push(field(
512            user_type(ast, param.type_, &at(service, operation, &param.name))?,
513            &cxx_escape(&param.name),
514        ));
515    }
516    if let Some(returns) = operation.returns {
517        let taken: Vec<String> = operation
518            .outputs()
519            .map(|param| cxx_escape(&param.name))
520            .collect();
521        out.push(field(
522            user_type(ast, returns, &at(service, operation, "return"))?,
523            &return_member_name(&taken),
524        ));
525    }
526    Ok(out)
527}
528
529/// The native `Out` structure of `operation`, spelled so that it is not
530/// taken for the application's own of that name.
531pub(super) fn native_out_type(
532    ast: &Ast,
533    module: Id<ModuleNode>,
534    name: &str,
535    operation: &Operation,
536) -> Result<cpp::Type, BindingError> {
537    let out = interface::out_struct_name(name, &cxx_escape(&operation.name));
538    if !is_psm() {
539        return Ok(cpp::Type::named(Path::single(&out)));
540    }
541    let mut path = format!("::{NATIVE_NAMESPACE}");
542    for segment in module_names(ast, module).map_err(BindingError::from)? {
543        path.push_str("::");
544        path.push_str(&segment);
545    }
546    path.push_str("::");
547    path.push_str(&out);
548    Ok(cpp::Type::named(Path::single(&path)))
549}
550
551fn reply_fields(
552    ast: &Ast,
553    service: &Service,
554    operation: &Operation,
555) -> Result<Vec<cpp::ClassMember>, BindingError> {
556    // §7.5.1.1.5 rule 3a: the `out` and `inout` parameters first, in
557    // declaration order and under their formal names; rule 3b: the
558    // return value *last*.
559    let mut out = Vec::new();
560    for param in operation.outputs() {
561        out.push(field(
562            cpp_value_type(ast, param.type_, &at(service, operation, &param.name))?,
563            &cxx_escape(&param.name),
564        ));
565    }
566    if let Some(returns) = operation.returns {
567        let taken: Vec<String> = operation
568            .outputs()
569            .map(|param| cxx_escape(&param.name))
570            .collect();
571        out.push(field(
572            cpp_value_type(ast, returns, &at(service, operation, "return"))?,
573            &return_member_name(&taken),
574        ));
575    }
576    // Rule 3c: nothing at all becomes the `dummy` member.
577    if out.is_empty() {
578        out.push(field(
579            qualified(RPC, "UnusedMember"),
580            interface::DUMMY_MEMBER,
581        ));
582    }
583    Ok(out)
584}
585
586/// The name §7.5.1.1.5 rule 3b gives the return value, with the
587/// collision rule: "If an operation has an out argument named `return_`
588/// and the operation has a regular return value, the regular return
589/// value in the Out structure shall be represented as `return_N` member
590/// of the appropriate type where N is the first integer in the range 1
591/// to 2^31 that avoids the collision."
592fn return_member_name(taken: &[String]) -> String {
593    if !taken.iter().any(|name| name == RETURN_MEMBER) {
594        return RETURN_MEMBER.to_string();
595    }
596    for n in 1..=i32::MAX {
597        let candidate = format!("{RETURN_MEMBER}{n}");
598        if !taken.iter().any(|name| name == &candidate) {
599            return candidate;
600        }
601    }
602    // Unreachable below 2^31 parameters; a name no IDL identifier can
603    // take is still better than two members sharing one.
604    format!("{RETURN_MEMBER} ")
605}
606
607/// The call union, as a class with a discriminator and one field per
608/// case.
609///
610/// A class and not a C++ `union`, for the reason
611/// `crate::adapter::cpp`'s module documentation gives about IDL unions:
612/// two cases may carry the same type, and a member of a `union` with a
613/// non-trivial constructor — which `std::string` and `std::vector` both
614/// have — has to be placed and destroyed by hand.
615fn union_of(
616    name: &str,
617    operations: &[Operation],
618    service: &str,
619    reply: bool,
620    doc: Doc,
621) -> cpp::Class {
622    let mut members = vec![
623        discriminator_field(),
624        // §7.5.1.1.6 rule 3: "The Call union shall have a default case
625        // label containing a member named `unknownOp` of type
626        // `dds::rpc::UnknownOperation`." Without it a client built
627        // against a newer interface selects a case the service does not
628        // have, and the sample decodes as whichever case happens to be
629        // first.
630        case_field(
631            qualified(RPC, "UnknownOperation"),
632            interface::UNKNOWN_OP_MEMBER,
633            Doc::line("Live on the `default` case. DDS-RPC 1.0 §7.5.1.1.6 rule 3."),
634        ),
635    ];
636    for operation in operations {
637        let operation_name = cxx_escape(&operation.name);
638        let case = if reply {
639            interface::result_union_name(service, &operation_name)
640        } else {
641            interface::in_struct_name(service, &operation_name)
642        };
643        members.push(case_field(
644            cpp::Type::named(Path::single(&case)),
645            &operation_name,
646            Doc::line(&format!(
647                "Live when `_d` is `{}`.",
648                interface::operation_hash_constant(service, &operation_name)
649            )),
650        ));
651    }
652    cpp::Class {
653        doc,
654        is_struct: true,
655        name: Ident::new(name),
656        bases: Vec::new(),
657        sections: vec![cpp::Section {
658            access: cpp::Access::Public,
659            members,
660        }],
661    }
662}
663
664/// The `_d` member every synthesized union carries.
665///
666/// The discriminator of §7.5.1.1.6 rule 2 is an IDL `long`, so it takes
667/// the spelling `adapter::cpp` gives one and not the bare `int32_t` the
668/// CST would also accept: two spellings of one width in one header is a
669/// diff waiting to be read as a difference.
670fn discriminator_field() -> cpp::ClassMember {
671    cpp::ClassMember::Field(cpp::Field {
672        doc: Doc::line("Which case this sample carries. DDS-RPC 1.0 §7.5.1.1.6 rule 2."),
673        type_: cpp_primitive(PrimitiveKind::Int32),
674        name: Ident::new("_d"),
675        dims: Vec::new(),
676        default: None,
677        is_static: false,
678        is_constexpr: false,
679        bits: None,
680    })
681}
682
683/// One case member of a synthesized union.
684fn case_field(type_: cpp::Type, name: &str, doc: Doc) -> cpp::ClassMember {
685    cpp::ClassMember::Field(cpp::Field {
686        doc,
687        type_,
688        name: Ident::new(name),
689        dims: Vec::new(),
690        default: None,
691        is_static: false,
692        is_constexpr: false,
693        bits: None,
694    })
695}
696
697/// One `${I}_${op}_Hash` or `${exception}_Ex_Hash` constant.
698///
699/// `inline constexpr` and not a macro, for the reason
700/// `crate::adapter::cpp` gives about IDL constants: an inline variable
701/// is one object across the program and warns nowhere it is unused.
702fn hash_constant(name: &str, value: i32, doc: &str) -> cpp::Decl {
703    cpp::Decl::Variable(cpp::Variable {
704        doc: Doc::line(doc),
705        type_: cpp_primitive(PrimitiveKind::Int32),
706        name: Ident::new(name),
707        value: Some(cpp::Expr::Literal(zerodds_lang_cst::common::Literal::Int(
708            value.to_string(),
709        ))),
710        is_static: false,
711        is_constexpr: true,
712        is_inline: true,
713    })
714}
715
716/// `${I}_${op}_Result` — §7.5.1.1.5.
717///
718/// The `dds::RETCODE_OK` case carries the `Out` structure under
719/// `result`; every exception the operation raises carries its own,
720/// under the lower-cased name with `_ex` appended.
721fn result_union_of(service: &Service, name: &str, operation: &Operation) -> cpp::Class {
722    let operation_name = cxx_escape(&operation.name);
723    let mut members = vec![
724        discriminator_field(),
725        case_field(
726            cpp::Type::named(Path::single(&interface::out_struct_name(
727                name,
728                &operation_name,
729            ))),
730            interface::RESULT_MEMBER,
731            Doc::line(&format!(
732                "Live when `_d` is `dds::RETCODE_OK` ({}). DDS-RPC 1.0 §7.5.1.1.5 rule 3a.",
733                interface::RETCODE_OK
734            )),
735        ),
736    ];
737    for raised in &operation.raises {
738        members.push(case_field(
739            cpp::Type::named(Path::single(&cxx_escape(&raised.name))),
740            // Rule 5b names the member after the IDL exception; `<name>_ex`
741            // is not a reserved word, so rule 8 does not prefix it.
742            &interface::exception_member_name(&raised.name),
743            Doc::line(&format!(
744                "Live when `_d` is `{}`. DDS-RPC 1.0 §7.5.1.1.5 rule 5.",
745                interface::exception_hash_constant(&cxx_escape(&raised.name))
746            )),
747        ));
748    }
749    let _ = service;
750    cpp::Class {
751        doc: Doc::line("The outcome of one operation. DDS-RPC 1.0 §7.5.1.1.5."),
752        is_struct: true,
753        name: Ident::new(&interface::result_union_name(name, &operation_name)),
754        bases: Vec::new(),
755        sections: vec![cpp::Section {
756            access: cpp::Access::Public,
757            members,
758        }],
759    }
760}
761
762/// `ISupport` — what §7.11.1.1.2 rule 3's `SupportType` typedef names.
763///
764/// The rule requires the typedef; the DDS-Cxx-PSM type support behind it
765/// is generated by the type-support pass, which is why this carries the
766/// two things a service's support type has to answer and no more: which
767/// topic types the service uses, and what its two topics are called
768/// (§7.4.1.1).
769fn support_class(service: &Service, name: &str) -> cpp::Class {
770    // §7.4.1's two names, which are not the type names: the topic is
771    // `<interface_name>_<service_name>_Request` with the interface name
772    // fully qualified, and the type is `${interfaceName}_Request`
773    // (§7.5.1.1.6). `SupportType` names both so a caller never has to
774    // build either.
775    let style = interface::BindingStyle::FunctionCall;
776    let mut members = vec![
777        alias("RequestType", &interface::request_type_name(name)),
778        alias("ReplyType", &interface::reply_type_name(name)),
779        topic_name_method(
780            "request_topic_name",
781            &service.request_topic_name(style),
782            "The request topic's name. DDS-RPC 1.0 §7.4.1, §7.4.2.",
783        ),
784        topic_name_method(
785            "reply_topic_name",
786            &service.reply_topic_name(style),
787            "The reply topic's name. DDS-RPC 1.0 §7.4.1, §7.4.2.",
788        ),
789        topic_name_method(
790            "service_name",
791            &service.service_name,
792            "The `<service_name>` segment of §7.4.1, `\"Service\"` unless \
793             `@service(name = \"…\")` supplied one.",
794        ),
795    ];
796    members.push(calls::defaults_method(service));
797    // §7.4.3: one alias per parent interface, announced through
798    // `PID_TOPIC_ALIASES` at discovery.
799    for (at, alias_name) in service.topic_aliases(style, false).into_iter().enumerate() {
800        members.push(topic_name_method(
801            &format!("request_topic_alias_{at}"),
802            &alias_name,
803            "A request-topic alias from a parent interface. DDS-RPC 1.0 §7.4.3.",
804        ));
805    }
806    for (at, alias_name) in service.topic_aliases(style, true).into_iter().enumerate() {
807        members.push(topic_name_method(
808            &format!("reply_topic_alias_{at}"),
809            &alias_name,
810            "A reply-topic alias from a parent interface. DDS-RPC 1.0 §7.4.3.",
811        ));
812    }
813    cpp::Class {
814        doc: Doc::line("The service's type support: its topic types and their topic names."),
815        is_struct: true,
816        name: Ident::new(&format!("{name}Support")),
817        bases: Vec::new(),
818        sections: vec![cpp::Section {
819            access: cpp::Access::Public,
820            members,
821        }],
822    }
823}
824
825fn topic_name_method(method: &str, topic: &str, doc: &str) -> cpp::ClassMember {
826    cpp::ClassMember::Method(cpp::Method {
827        doc: Doc::line(doc),
828        returns: cpp::Type::Pointer(Box::new(cpp::Type::Const(Box::new(cpp::Type::Primitive(
829            cpp::Primitive::Char,
830        ))))),
831        name: Ident::new(method),
832        params: Vec::new(),
833        is_const: false,
834        is_static: true,
835        is_virtual: false,
836        is_pure: false,
837        is_constexpr: true,
838        is_noexcept: true,
839        is_override: false,
840        body: Some(cpp::Block {
841            statements: vec![cpp::Statement::Return(Some(cpp::Expr::Literal(
842                zerodds_lang_cst::common::Literal::Str(topic.to_string()),
843            )))],
844        }),
845    })
846}
847
848// -- the two abstract classes ------------------------------------------
849
850/// `class I` — §7.11.1.1.2 rules 1-6.
851fn sync_interface(
852    ast: &Ast,
853    service: &Service,
854    name: &str,
855    operations: &[Operation],
856) -> Result<cpp::Class, BindingError> {
857    let mut members = vec![
858        alias("SupportType", &format!("{name}Support")),
859        alias("RequestType", &interface::request_type_name(name)),
860        alias("ReplyType", &interface::reply_type_name(name)),
861        // Rule 4.
862        alias("AsyncInterfaceType", &format!("{name}Async")),
863        // Rule 2.
864        cpp::ClassMember::Destructor(cpp::Destructor {
865            doc: Doc::line("Rule 2: a public virtual destructor."),
866            name: Ident::new(name),
867            is_virtual: true,
868            body: cpp::FunctionBody::Defaulted,
869        }),
870    ];
871    let _ = operations;
872    for operation in &service.operations {
873        members.push(cpp::ClassMember::Method(sync_method(
874            ast, service, operation,
875        )?));
876    }
877    // Rule 6.f sends the attributes to DDS-Cxx-PSM §7.4.6, which names
878    // the functions after the attribute rather than `get_`/`set_`. The
879    // `get_`/`set_` names of DDS-RPC §7.5.1.1.3 are the *implied IDL*,
880    // and the implied IDL is what the topic types above are built from —
881    // the two are one clause apart and name two different things.
882    for attribute in &service.attributes {
883        let at = format!("{}::{}", service.name, attribute.name);
884        let type_ = user_type(ast, attribute.type_, &at)?;
885        let spelled = cxx_escape(&attribute.name);
886        if is_native(ast, attribute.type_) {
887            // `NT attribute();` and `void attribute(NT attrib);`
888            members.push(cpp::ClassMember::Method(attribute_method(
889                &spelled,
890                type_.clone(),
891                Vec::new(),
892                false,
893            )));
894            if !attribute.readonly {
895                members.push(cpp::ClassMember::Method(attribute_method(
896                    &spelled,
897                    cpp::Type::Void,
898                    vec![cpp::Param {
899                        type_,
900                        name: Some(Ident::new(&spelled)),
901                        default: None,
902                    }],
903                    false,
904                )));
905            }
906        } else {
907            // `CT& attribute();`, `const CT& attribute() const;` and
908            // `void attribute(const CT& attrib);`
909            members.push(cpp::ClassMember::Method(attribute_method(
910                &spelled,
911                cpp::Type::Ref(Box::new(type_.clone())),
912                Vec::new(),
913                false,
914            )));
915            members.push(cpp::ClassMember::Method(attribute_method(
916                &spelled,
917                cpp::Type::Ref(Box::new(cpp::Type::Const(Box::new(type_.clone())))),
918                Vec::new(),
919                true,
920            )));
921            if !attribute.readonly {
922                members.push(cpp::ClassMember::Method(attribute_method(
923                    &spelled,
924                    cpp::Type::Void,
925                    vec![cpp::Param {
926                        type_: cpp::Type::Ref(Box::new(cpp::Type::Const(Box::new(type_)))),
927                        name: Some(Ident::new(&spelled)),
928                        default: None,
929                    }],
930                    false,
931                )));
932            }
933        }
934    }
935    Ok(cpp::Class {
936        doc: Doc::line(
937            "The service, as the abstract class DDS-RPC 1.0 §7.11.1.1.2 rule 1 requires.",
938        ),
939        is_struct: false,
940        name: Ident::new(name),
941        bases: bases(ast, service, "")?,
942        sections: vec![cpp::Section {
943            access: cpp::Access::Public,
944            members,
945        }],
946    })
947}
948
949/// One synchronous operation — rule 6.
950fn sync_method(
951    ast: &Ast,
952    service: &Service,
953    operation: &Operation,
954) -> Result<cpp::Method, BindingError> {
955    let mut params = Vec::new();
956    let mut returns = cpp::Type::Void;
957
958    // Rule 6.h: an operation returning a constructed type takes it as a
959    // leading `T&` named `cxx_return` and returns `void`. Rule 6.g: a
960    // primitive or an enumeration comes back by value.
961    if let Some(target) = operation.returns {
962        let type_ = user_type(ast, target, &at(service, operation, "return"))?;
963        if is_native(ast, target) {
964            returns = type_;
965        } else {
966            params.push(cpp::Param {
967                type_: cpp::Type::Ref(Box::new(type_)),
968                name: Some(Ident::new("cxx_return")),
969                default: None,
970            });
971        }
972    }
973
974    // Rule 6.b: the number and the order of the arguments are the IDL's.
975    // Rule 6.e: DDS-Cxx-PSM §7.4.5 decides the form each takes.
976    for param in &operation.params {
977        params.push(cpp::Param {
978            type_: passed(ast, param, &at(service, operation, &param.name))?,
979            name: Some(Ident::new(&cxx_escape(&param.name))),
980            default: None,
981        });
982    }
983
984    Ok(cpp::Method {
985        doc: doc_for(operation),
986        returns,
987        name: Ident::new(&cxx_escape(&operation.name)),
988        params,
989        is_const: false,
990        is_static: false,
991        is_virtual: true,
992        is_pure: true,
993        is_constexpr: false,
994        // Rule 6.c: no exception specification, whatever the IDL raises.
995        is_noexcept: false,
996        is_override: false,
997        body: None,
998    })
999}
1000
1001/// `class IAsync` — §7.11.1.1.2 rules 1-5 and 7.
1002fn async_interface(
1003    ast: &Ast,
1004    service: &Service,
1005    name: &str,
1006    operations: &[Operation],
1007) -> Result<cpp::Class, BindingError> {
1008    let mut members = vec![
1009        alias("SupportType", &format!("{name}Support")),
1010        alias("RequestType", &interface::request_type_name(name)),
1011        alias("ReplyType", &interface::reply_type_name(name)),
1012        // Rule 5.
1013        alias("InterfaceType", name),
1014        cpp::ClassMember::Destructor(cpp::Destructor {
1015            doc: Doc::line("Rule 2: a public virtual destructor."),
1016            name: Ident::new(&format!("{name}Async")),
1017            is_virtual: true,
1018            body: cpp::FunctionBody::Defaulted,
1019        }),
1020    ];
1021    let _ = operations;
1022    for operation in &service.operations {
1023        members.push(cpp::ClassMember::Method(async_method(
1024            ast, service, name, operation,
1025        )?));
1026    }
1027    // Rule 7.f: "Attribute getter functions shall take no parameters and
1028    // shall return `dds::rpc::future` of the same type as the
1029    // attribute." Rule 7.g: a setter takes its parameter per
1030    // DDS-Cxx-PSM §7.4.6 and returns `dds::rpc::future<void>`. Rule 7.a
1031    // gives both the `_async` suffix, over the §7.4.6 name.
1032    for attribute in &service.attributes {
1033        let at = format!("{}::{}", service.name, attribute.name);
1034        // The future holds the value as the topic type does; the argument of
1035        // the setter is what the application passes.
1036        let payload = user_type(ast, attribute.type_, &at)?;
1037        let type_ = user_type(ast, attribute.type_, &at)?;
1038        let spelled = cxx_escape(&attribute.name);
1039        members.push(cpp::ClassMember::Method(async_attribute_method(
1040            &spelled,
1041            future_of(payload),
1042            Vec::new(),
1043        )));
1044        if !attribute.readonly {
1045            let passed = if is_native(ast, attribute.type_) {
1046                cpp::Type::Const(Box::new(type_))
1047            } else {
1048                cpp::Type::Ref(Box::new(cpp::Type::Const(Box::new(type_))))
1049            };
1050            members.push(cpp::ClassMember::Method(async_attribute_method(
1051                &spelled,
1052                future_of(cpp::Type::Void),
1053                vec![cpp::Param {
1054                    type_: passed,
1055                    name: Some(Ident::new(&spelled)),
1056                    default: None,
1057                }],
1058            )));
1059        }
1060    }
1061    let bases = bases(ast, service, "Async")?;
1062    Ok(cpp::Class {
1063        doc: Doc::line(
1064            "The service's asynchronous half, as DDS-RPC 1.0 §7.11.1.1.2 rule 1 requires.",
1065        ),
1066        is_struct: false,
1067        name: Ident::new(&format!("{name}Async")),
1068        bases,
1069        sections: vec![cpp::Section {
1070            access: cpp::Access::Public,
1071            members,
1072        }],
1073    })
1074}
1075
1076/// What the `future` of an asynchronous operation holds.
1077///
1078/// Rule 7.i: an operation with `out` or `inout` parameters comes back as a
1079/// `future` of the reply struct, because a `future` has one value and the
1080/// call has several. Rule 7.h: one with an ordinary return comes back as
1081/// a `future` of that. Otherwise `future<void>`.
1082fn async_payload(
1083    ast: &Ast,
1084    service: &Service,
1085    name: &str,
1086    operation: &Operation,
1087) -> Result<cpp::Type, BindingError> {
1088    Ok(if operation.has_outputs() {
1089        cpp::Type::named(Path::single(&interface::out_struct_name(
1090            name,
1091            &cxx_escape(&operation.name),
1092        )))
1093    } else if let Some(target) = operation.returns {
1094        // The application's type of the value (Table 7.1 under the PSM
1095        // mapping), as the synchronous call returns it.
1096        user_type(ast, target, &at(service, operation, "return"))?
1097    } else {
1098        cpp::Type::Void
1099    })
1100}
1101
1102/// One asynchronous operation — rule 7.
1103fn async_method(
1104    ast: &Ast,
1105    service: &Service,
1106    name: &str,
1107    operation: &Operation,
1108) -> Result<cpp::Method, BindingError> {
1109    let payload = async_payload(ast, service, name, operation)?;
1110
1111    // Rule 7.b: `In` and `InOut` only, no `Out`. Rule 7.d/7.e: every
1112    // argument is `const`.
1113    let mut params = Vec::new();
1114    for param in operation.inputs() {
1115        let type_ = user_type(ast, param.type_, &at(service, operation, &param.name))?;
1116        params.push(cpp::Param {
1117            type_: if is_native(ast, param.type_) {
1118                cpp::Type::Const(Box::new(type_))
1119            } else {
1120                cpp::Type::Ref(Box::new(cpp::Type::Const(Box::new(type_))))
1121            },
1122            name: Some(Ident::new(&cxx_escape(&param.name))),
1123            default: None,
1124        });
1125    }
1126
1127    Ok(cpp::Method {
1128        doc: doc_for(operation),
1129        returns: future_of(payload),
1130        // Rule 7.a.
1131        name: Ident::new(&format!("{}_async", cxx_escape(&operation.name))),
1132        params,
1133        is_const: false,
1134        is_static: false,
1135        is_virtual: true,
1136        is_pure: true,
1137        is_constexpr: false,
1138        is_noexcept: false,
1139        is_override: false,
1140        body: None,
1141    })
1142}
1143
1144// -- the endpoints -----------------------------------------------------
1145
1146/// `using IRequester = dds::rpc::core::Requester<I_Request, I_Reply>;`
1147///
1148/// §7.11.1.1.1: the binding "shall use … the structs synthesized from an
1149/// IDL interface", and the `Requester` template "provide[s] a uniform
1150/// interface to send, receive, allocate, initialize, and destroy data".
1151/// A per-service alias and not a per-service subclass: there is nothing
1152/// for a subclass to add that the two type arguments do not already say,
1153/// and an alias is the same type, so a `Requester<I_Request, I_Reply>`
1154/// obtained anywhere else is the same object.
1155fn requester_alias(name: &str, request: &str, reply: &str) -> cpp::Decl {
1156    endpoint_alias(&format!("{name}Requester"), "Requester", request, reply)
1157}
1158
1159/// `using IReplier = dds::rpc::core::Replier<I_Request, I_Reply>;` — §7.11.1.1.1.
1160fn replier_alias(name: &str, request: &str, reply: &str) -> cpp::Decl {
1161    endpoint_alias(&format!("{name}Replier"), "Replier", request, reply)
1162}
1163
1164fn endpoint_alias(name: &str, template: &str, request: &str, reply: &str) -> cpp::Decl {
1165    cpp::Decl::Alias(cpp::Alias {
1166        doc: Doc::line(&format!(
1167            "The service's `{template}`, over its two topic types. DDS-RPC 1.0 §7.11.1.1.1."
1168        )),
1169        name: Ident::new(name),
1170        type_: cpp::Type::template(
1171            Path::new(&["dds", "rpc", "core", template]),
1172            vec![
1173                cpp::Type::named(Path::single(request)),
1174                cpp::Type::named(Path::single(reply)),
1175            ],
1176        ),
1177    })
1178}
1179
1180// -- the shapes the rules are written in -------------------------------
1181
1182/// Whether the types of the application are the PSM's (`--cpp-mapping psm`).
1183pub(super) fn is_psm() -> bool {
1184    cpp_mapping() == CppMapping::Psm
1185}
1186
1187/// The type a parameter, a return value or an attribute has where the
1188/// application sees it.
1189///
1190/// Under the native mapping that is the type the topic structures hold.
1191/// Under the PSM mapping (DDS-PSM-Cxx 1.0 §8.1) it is the PSM's spelling of
1192/// Table 7.1 — `std::wstring` for `wstring`, `std::map` for `map`,
1193/// `dds::core::array` for an array — and the generated bodies convert
1194/// between it and the topic structures with `zerodds::psm::Conv`.
1195pub(super) fn user_type(ast: &Ast, id: Id<TypeNode>, at: &str) -> Result<cpp::Type, BindingError> {
1196    if is_psm() {
1197        return Ok(cpp::Type::named(Path::single(&psm_type(ast, id, at)?)));
1198    }
1199    Ok(cpp_value_type(ast, id, at)?)
1200}
1201
1202/// `value`, a value of the topic structures' type `id`, as the application
1203/// sees it; or `value` as the topic structures hold it. `value` itself under
1204/// the native mapping.
1205pub(super) fn converted(
1206    ast: &Ast,
1207    id: Id<TypeNode>,
1208    at: &str,
1209    value: cpp::Expr,
1210    to_native: bool,
1211) -> Result<cpp::Expr, BindingError> {
1212    if !is_psm() {
1213        return Ok(value);
1214    }
1215    let conv = format!("Conv<{}>", psm_type(ast, id, at)?);
1216    let function = if to_native {
1217        "to_native"
1218    } else {
1219        "from_native"
1220    };
1221    Ok(cpp::Expr::Call {
1222        callee: Box::new(cpp::Expr::Path(Path::new(&[
1223            "zerodds",
1224            "psm",
1225            conv.as_str(),
1226            function,
1227        ]))),
1228        args: vec![value],
1229    })
1230}
1231
1232/// DDS-Cxx-PSM §7.4.5's table, for one parameter.
1233///
1234/// A native type passes `IN` by value and `OUT`/`INOUT` as `T&`; a
1235/// constructed one passes `IN` as `const T&` and the other two as `T&`.
1236fn passed(ast: &Ast, param: &Parameter, at: &str) -> Result<cpp::Type, BindingError> {
1237    let type_ = user_type(ast, param.type_, at)?;
1238    Ok(match param.direction {
1239        ParamDirection::In if is_native(ast, param.type_) => type_,
1240        ParamDirection::In => cpp::Type::Ref(Box::new(cpp::Type::Const(Box::new(type_)))),
1241        ParamDirection::Out | ParamDirection::InOut => cpp::Type::Ref(Box::new(type_)),
1242    })
1243}
1244
1245/// Whether the C++ PSM's table calls this a *native* type.
1246///
1247/// §7.4.5 splits its table by "native" against "type", and
1248/// §7.11.1.1.2 rule 6.g names "primitive types and enumeration types"
1249/// together as what an operation returns by value. A bitmask and a
1250/// bitset are integer holders in `adapter::cpp` and belong on the same
1251/// side; a string, a sequence, an array, a map, a struct, a union and a
1252/// valuetype do not.
1253fn is_native(ast: &Ast, id: Id<TypeNode>) -> bool {
1254    native_at(ast, id, 0)
1255}
1256
1257/// zerodds-lint: recursion-depth 64 (alias chains; bounded by `MAX_ALIAS_DEPTH`)
1258fn native_at(ast: &Ast, id: Id<TypeNode>, depth: usize) -> bool {
1259    if depth > MAX_ALIAS_DEPTH {
1260        return false;
1261    }
1262    let Ok(node) = ast.type_(id) else {
1263        return false;
1264    };
1265    match &node.kind {
1266        TypeKind::Primitive(_) | TypeKind::Enumeration { .. } | TypeKind::Bitmask { .. } => true,
1267        TypeKind::Alias { aliased } => aliased
1268            .resolved()
1269            .is_some_and(|target| native_at(ast, target, depth + 1)),
1270        _ => false,
1271    }
1272}
1273
1274/// The base classes an interface's abstract class carries.
1275///
1276/// IDL inheritance, carried into the mapping: §7.11.1.1.2 rule 6 gives
1277/// `${interface}` "pure virtual functions for all the operations and
1278/// attributes defined in the IDL", and the ones a base defines are
1279/// defined in the IDL of the base. §7.5.1.1.8 keeps the *topics* per
1280/// hierarchy level, which is why the topic types above carry only this
1281/// interface's own operations.
1282fn bases(ast: &Ast, service: &Service, suffix: &str) -> Result<Vec<cpp::Base>, BindingError> {
1283    service
1284        .bases
1285        .iter()
1286        .map(|base| {
1287            // The base may be declared in another module.
1288            let module = interface::service(ast, base.id)?.module;
1289            Ok(cpp::Base {
1290                access: cpp::Access::Public,
1291                virtual_: false,
1292                path: enhanced::path_in_idl(
1293                    ast,
1294                    service.module,
1295                    module,
1296                    &format!("{}{suffix}", cxx_escape(&base.name)),
1297                )?,
1298            })
1299        })
1300        .collect()
1301}
1302
1303/// One accessor of DDS-Cxx-PSM §7.4.6's table.
1304fn attribute_method(
1305    name: &str,
1306    returns: cpp::Type,
1307    params: Vec<cpp::Param>,
1308    is_const: bool,
1309) -> cpp::Method {
1310    cpp::Method {
1311        doc: Doc::line("An attribute accessor. DDS-Cxx-PSM 1.0 §7.4.6."),
1312        returns,
1313        name: Ident::new(name),
1314        params,
1315        is_const,
1316        is_static: false,
1317        is_virtual: true,
1318        is_pure: true,
1319        is_constexpr: false,
1320        is_noexcept: false,
1321        is_override: false,
1322        body: None,
1323    }
1324}
1325
1326/// One asynchronous accessor — rules 7.a, 7.f and 7.g.
1327fn async_attribute_method(name: &str, returns: cpp::Type, params: Vec<cpp::Param>) -> cpp::Method {
1328    cpp::Method {
1329        doc: Doc::line("An attribute accessor. DDS-RPC 1.0 §7.11.1.1.2 rules 7.f and 7.g."),
1330        returns,
1331        name: Ident::new(&format!("{name}_async")),
1332        params,
1333        is_const: false,
1334        is_static: false,
1335        is_virtual: true,
1336        is_pure: true,
1337        is_constexpr: false,
1338        is_noexcept: false,
1339        is_override: false,
1340        body: None,
1341    }
1342}
1343
1344/// `dds::rpc::future<T>` — §7.11.1.4.13.
1345fn future_of(payload: cpp::Type) -> cpp::Type {
1346    cpp::Type::template(Path::new(&["dds", "rpc", "future"]), vec![payload])
1347}
1348
1349fn struct_of(name: &str, members: Vec<cpp::ClassMember>, doc: Doc) -> cpp::Class {
1350    cpp::Class {
1351        doc,
1352        is_struct: true,
1353        name: Ident::new(name),
1354        bases: Vec::new(),
1355        sections: vec![cpp::Section {
1356            access: cpp::Access::Public,
1357            members,
1358        }],
1359    }
1360}
1361
1362fn field(type_: cpp::Type, name: &str) -> cpp::ClassMember {
1363    cpp::ClassMember::Field(cpp::Field {
1364        doc: Doc::none(),
1365        type_,
1366        name: Ident::new(name),
1367        dims: Vec::new(),
1368        default: None,
1369        is_static: false,
1370        is_constexpr: false,
1371        bits: None,
1372    })
1373}
1374
1375fn alias(name: &str, target: &str) -> cpp::ClassMember {
1376    cpp::ClassMember::Alias(cpp::Alias {
1377        doc: Doc::none(),
1378        name: Ident::new(name),
1379        type_: cpp::Type::named(Path::single(target)),
1380    })
1381}
1382
1383fn qualified(namespace: &str, name: &str) -> cpp::Type {
1384    let mut segments: Vec<&str> = namespace.split("::").collect();
1385    segments.push(name);
1386    cpp::Type::named(Path::new(&segments))
1387}
1388
1389/// The `raises` clause, as documentation. See
1390/// [`Operation::raises_line`].
1391fn doc_for(operation: &Operation) -> Doc {
1392    operation
1393        .raises_line()
1394        .map_or_else(Doc::none, |line| Doc::line(&line))
1395}
1396
1397fn at(service: &Service, operation: &Operation, what: &str) -> String {
1398    format!("{}::{}({what})", service.name, operation.name)
1399}
1400
1401/// DDS-RPC 1.0 §7.11.1.1.2 rule 8.
1402///
1403/// "Any identifier (e.g., parameter, operation name, exception,
1404/// interface, module) in an IDL file that is a reserved word in C++
1405/// shall be prefixed by `cxx_`."
1406#[must_use]
1407pub fn cxx_escape(name: &str) -> String {
1408    // One definition of the prefix rule, shared with the declarations
1409    // `adapter::cpp` writes, so a name and every reference to it agree.
1410    crate::adapter::cpp::cxx_name(name)
1411}