1use crate::contract;
35use crate::encode::ProbeError;
36use crate::fill_cpp::cpp_name;
37use zerodds_ast::{Ast, CharWidth, PrimitiveKind, TypeKind, TypeNode};
38use zerodds_ast_core::Id;
39
40enum Place {
43 Accessor {
45 object: String,
47 name: String,
49 },
50 Element(String),
53}
54
55impl Place {
56 fn read(&self) -> String {
57 match self {
58 Self::Accessor { object, name } => format!("{object}.{name}()"),
59 Self::Element(expression) => expression.clone(),
60 }
61 }
62}
63
64#[derive(Debug, Default)]
65struct Dump {
66 body: String,
67 depth: usize,
68 next: usize,
69 rec: Vec<Id<TypeNode>>,
74}
75
76impl Dump {
77 fn line(&mut self, text: &str) {
78 for _ in 0..self.depth {
79 self.body.push_str(" ");
80 }
81 self.body.push_str(text);
82 self.body.push('\n');
83 }
84
85 fn open(&mut self, text: &str) {
86 self.line(text);
87 self.depth += 1;
88 }
89
90 fn close(&mut self, text: &str) {
91 self.depth = self.depth.saturating_sub(1);
92 self.line(text);
93 }
94
95 fn raw(&mut self, literal: &str) {
96 let escaped = literal.replace('\\', "\\\\").replace('"', "\\\"");
97 self.line(&format!("zdj_raw(\"{escaped}\");"));
98 }
99
100 fn temporary(&mut self, stem: &str) -> String {
103 self.next += 1;
104 format!("zd_{stem}_{}", self.next)
105 }
106}
107
108pub fn dump_fastdds(ast: &Ast, type_: Id<TypeNode>) -> Result<String, ProbeError> {
113 contract::reject_alias_cycle(ast)?;
115 let mut out = Dump {
116 rec: crate::recursion::recursive_aggregates(ast, type_),
117 ..Dump::default()
118 };
119 if !out.rec.is_empty() {
120 out.open("struct zd_rec {");
125 for position in 0..out.rec.len() {
126 let id = out.rec[position];
127 let name = crate::fill_cpp::cpp_name(ast, id);
128 out.open(&format!(
129 "static void dump_{position}(const {name} &zdv) {{"
130 ));
131 emit_rec_body(ast, id, &mut out, &format!("<{name}>"))?;
132 out.close("}");
133 }
134 out.close("};");
135 }
136 emit(
137 ast,
138 type_,
139 &Place::Element(String::from("sample")),
140 &mut out,
141 "",
142 )?;
143 out.line("zdj_raw(\"\\n\");");
144 Ok(out.body)
145}
146
147fn emit_rec_body(
150 ast: &Ast,
151 type_: Id<TypeNode>,
152 out: &mut Dump,
153 path: &str,
154) -> Result<(), ProbeError> {
155 let node = ast.type_(type_).map_err(|error| ProbeError::NoLayout {
156 detail: format!("{error}"),
157 })?;
158 match &node.kind {
159 TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
160 emit_aggregate(ast, type_, "zdv", out, path)
161 }
162 TypeKind::Union { .. } => emit_union(ast, type_, "zdv", out, path),
163 _ => Err(ProbeError::NoLayout {
164 detail: format!("{path}: a type on a cycle is not an aggregate"),
165 }),
166 }
167}
168
169#[allow(clippy::too_many_lines)]
177fn emit(
178 ast: &Ast,
179 type_: Id<TypeNode>,
180 place: &Place,
181 out: &mut Dump,
182 path: &str,
183) -> Result<(), ProbeError> {
184 let node = ast.type_(type_).map_err(|error| ProbeError::NoLayout {
185 detail: format!("{error}"),
186 })?;
187 let here = place.read();
188 match &node.kind {
189 TypeKind::Primitive(PrimitiveKind::Float128) => Err(ProbeError::Unsupported {
192 path: path.to_string(),
193 kind: "a long double, whose Fast DDS bytes differ between x86-64 and aarch64",
194 }),
195 TypeKind::Primitive(kind) => {
196 out.line(&primitive_call(*kind, &here));
197 Ok(())
198 }
199 TypeKind::String {
200 width: CharWidth::Char,
201 ..
202 } => {
203 out.line(&format!("zdj_string({here}.c_str());"));
204 Ok(())
205 }
206 TypeKind::String {
207 width: CharWidth::WChar,
208 ..
209 } => {
210 out.line(&format!("zdj_wstring_cpp({here});"));
216 Ok(())
217 }
218 TypeKind::Enumeration { .. } => {
219 emit_enumeration(ast, type_, &here, out);
220 Ok(())
221 }
222 TypeKind::Bitmask { .. } => {
223 emit_bitmask(ast, type_, &here, out);
224 Ok(())
225 }
226 TypeKind::Bitset { .. } => {
227 out.open("{");
228 out.line("int zd_first = 1;");
229 out.raw("{");
230 for name in contract::bitfield_names(ast, &ast.all_bitfields_of(type_)) {
236 out.line(&format!("zdj_key(&zd_first, \"{name}\");"));
237 out.line(&format!("zdj_u64((unsigned long long)({here}.{name}));"));
238 }
239 out.raw("}");
240 out.close("}");
241 Ok(())
242 }
243 TypeKind::Alias { aliased } => {
244 let target = aliased.resolved().ok_or_else(|| ProbeError::NoLayout {
245 detail: format!("{path}: the typedef target is unresolved"),
246 })?;
247 emit(ast, target, place, out, path)
248 }
249 TypeKind::Structure { .. } | TypeKind::Valuetype { .. } | TypeKind::Union { .. } => {
250 if let Some(position) = out.rec.iter().position(|id| *id == type_) {
253 out.line(&format!("zd_rec::dump_{position}({here});"));
254 return Ok(());
255 }
256 if matches!(&node.kind, TypeKind::Union { .. }) {
257 emit_union(ast, type_, &here, out, path)
258 } else {
259 emit_aggregate(ast, type_, &here, out, path)
260 }
261 }
262 TypeKind::Array { element, dims } => {
263 let element = element.resolved().ok_or_else(|| ProbeError::NoLayout {
264 detail: format!("{path}: the array element is unresolved"),
265 })?;
266 let counts = contract::array_dimensions(ast, dims, path)?;
267 emit_array(ast, element, &here, out, path, &counts)
268 }
269 TypeKind::Sequence { element, .. } => {
270 let element = element.resolved().ok_or_else(|| ProbeError::NoLayout {
271 detail: format!("{path}: the sequence element is unresolved"),
272 })?;
273 let index = out.temporary("i");
274 let count = out.temporary("n");
275 out.open("{");
276 out.line(&format!("const size_t {count} = {here}.size();"));
277 out.raw("[");
278 out.open(&format!(
279 "for (size_t {index} = 0; {index} < {count}; {index}++) {{"
280 ));
281 out.line(&format!("if ({index} > 0) {{ zdj_raw(\",\"); }}"));
282 emit(
283 ast,
284 element,
285 &Place::Element(format!("{here}[{index}]")),
286 out,
287 &format!("{path}[]"),
288 )?;
289 out.close("}");
290 out.raw("]");
291 out.close("}");
292 Ok(())
293 }
294 TypeKind::Map {
295 key, value: entry, ..
296 } => {
297 let key_type = key.resolved().ok_or_else(|| ProbeError::NoLayout {
298 detail: format!("{path}: the map key type is unresolved"),
299 })?;
300 let value_type = entry.resolved().ok_or_else(|| ProbeError::NoLayout {
301 detail: format!("{path}: the map value type is unresolved"),
302 })?;
303 let first = out.temporary("first");
304 let pair = out.temporary("entry");
305 out.open("{");
309 out.line(&format!("int {first} = 1;"));
310 out.raw("[");
311 out.open(&format!("for (const auto &{pair} : {here}) {{"));
312 out.line(&format!("if (!{first}) {{ zdj_raw(\",\"); }} {first} = 0;"));
313 out.raw("[");
314 emit(
315 ast,
316 key_type,
317 &Place::Element(format!("{pair}.first")),
318 out,
319 &format!("{path}[].key"),
320 )?;
321 out.raw(",");
322 emit(
323 ast,
324 value_type,
325 &Place::Element(format!("{pair}.second")),
326 out,
327 &format!("{path}[].value"),
328 )?;
329 out.raw("]");
330 out.close("}");
331 out.raw("]");
332 out.close("}");
333 Ok(())
334 }
335 TypeKind::Fixed { .. } => Err(ProbeError::Unsupported {
336 path: path.to_string(),
337 kind: "a fixed, which fastddsgen rejects in the IDL before any \
338 accessor could be read from it",
339 }),
340 other => Err(ProbeError::Unfilled {
341 path: path.to_string(),
342 kind: other.as_str(),
343 }),
344 }
345}
346
347fn emit_aggregate(
358 ast: &Ast,
359 type_: Id<TypeNode>,
360 here: &str,
361 out: &mut Dump,
362 path: &str,
363) -> Result<(), ProbeError> {
364 out.open("{");
365 out.line("int zd_first = 1;");
366 out.raw("{");
367 for member in contract::members(ast, type_) {
368 let member_type = member.type_;
369 let name = member.name.clone();
370 let member_path = contract::join(path, &name);
371 if is_external(ast, type_, &name) && !member.optional {
377 out.line(&format!("zdj_key(&zd_first, \"{name}\");"));
378 emit(
379 ast,
380 member_type,
381 &Place::Element(format!("(*{here}.{name}())")),
382 out,
383 &member_path,
384 )?;
385 continue;
386 }
387 if member.optional {
391 let engaged = if is_external(ast, type_, &name) {
396 format!("(*{here}.{name}().value())")
397 } else {
398 format!("{here}.{name}().value()")
399 };
400 out.open(&format!("if ({here}.{name}().has_value()) {{"));
401 out.line(&format!("zdj_key(&zd_first, \"{name}\");"));
402 emit(
403 ast,
404 member_type,
405 &Place::Element(engaged),
406 out,
407 &member_path,
408 )?;
409 out.close("}");
410 continue;
411 }
412 out.line(&format!("zdj_key(&zd_first, \"{name}\");"));
413 emit(
414 ast,
415 member_type,
416 &Place::Accessor {
417 object: here.to_string(),
418 name,
419 },
420 out,
421 &member_path,
422 )?;
423 }
424 out.raw("}");
425 out.close("}");
426 Ok(())
427}
428
429pub(crate) fn is_external(ast: &Ast, type_: Id<TypeNode>, name: &str) -> bool {
436 let cases = ast
440 .cases_of(type_)
441 .iter()
442 .filter_map(|case| ast.case(*case).ok().map(|case| case.member));
443 for id in ast.all_members_of(type_).into_iter().chain(cases) {
444 let Ok(member) = ast.member(id) else { continue };
445 if member.name == name {
446 return member
447 .facets
448 .xtypes
449 .as_ref()
450 .is_some_and(|facet| facet.is_external);
451 }
452 }
453 false
454}
455
456fn emit_union(
467 ast: &Ast,
468 type_: Id<TypeNode>,
469 here: &str,
470 out: &mut Dump,
471 path: &str,
472) -> Result<(), ProbeError> {
473 let node = ast.type_(type_).map_err(|error| ProbeError::NoLayout {
474 detail: format!("{error}"),
475 })?;
476 let TypeKind::Union { discriminator, .. } = &node.kind else {
477 return Err(ProbeError::Unsupported {
478 path: path.to_string(),
479 kind: "a union whose kind changed under the emitter",
480 });
481 };
482 let discriminator = discriminator
483 .resolved()
484 .ok_or_else(|| ProbeError::NoLayout {
485 detail: format!("{path}: the discriminator is unresolved"),
486 })?;
487
488 out.open("{");
489 out.line("int zd_first = 1;");
490 out.raw("{");
491 out.line("zdj_key(&zd_first, \"_d\");");
492 emit(
493 ast,
494 discriminator,
495 &Place::Element(format!("{here}._d()")),
496 out,
497 &format!("{path}._d"),
498 )?;
499 let (width, suffix) = contract::discriminator_c_width(ast, discriminator);
506 out.open(&format!("switch (({width})({here}._d())) {{"));
507 for id in ast.cases_of(type_) {
508 let Ok(case) = ast.case(*id) else { continue };
509 let Ok(member) = ast.member(case.member) else {
510 continue;
511 };
512 let Some(member_type) = member.type_.resolved() else {
513 continue;
514 };
515 for label in &case.labels {
516 match label.expr() {
517 Some(expr) => match ast.eval_integral(expr) {
518 Ok(value) => out.line(&format!("case {value}{suffix}:")),
519 Err(error) => {
520 return Err(ProbeError::NoLayout {
521 detail: format!("{path}: a case label does not evaluate: {error}"),
522 });
523 }
524 },
525 None => out.line("default:"),
526 }
527 }
528 out.depth += 1;
529 out.line(&format!("zdj_key(&zd_first, \"{}\");", member.name));
530 let place = if is_external(ast, type_, &member.name) {
532 Place::Element(format!("(*{here}.{}())", member.name))
533 } else {
534 Place::Accessor {
535 object: here.to_string(),
536 name: member.name.clone(),
537 }
538 };
539 emit(
540 ast,
541 member_type,
542 &place,
543 out,
544 &format!("{path}.{}", member.name),
545 )?;
546 out.line("break;");
547 out.depth = out.depth.saturating_sub(1);
548 }
549 out.close("}");
550 out.raw("}");
551 out.close("}");
552 Ok(())
553}
554
555fn emit_array(
559 ast: &Ast,
560 element: Id<TypeNode>,
561 here: &str,
562 out: &mut Dump,
563 path: &str,
564 counts: &[usize],
565) -> Result<(), ProbeError> {
566 let Some((&count, rest)) = counts.split_first() else {
567 return emit(ast, element, &Place::Element(here.to_string()), out, path);
568 };
569 out.raw("[");
570 for index in 0..count {
571 if index > 0 {
572 out.raw(",");
573 }
574 emit_array(
575 ast,
576 element,
577 &format!("{here}[{index}]"),
578 out,
579 &format!("{path}[{index}]"),
580 rest,
581 )?;
582 }
583 out.raw("]");
584 Ok(())
585}
586
587fn emit_enumeration(ast: &Ast, type_: Id<TypeNode>, here: &str, out: &mut Dump) {
592 let scope = cpp_name(ast, type_);
593 out.open(&format!("switch ((long long)({here})) {{"));
594 for id in ast.enumerators_of(type_) {
595 let Ok(node) = ast.enumerator(*id) else {
596 continue;
597 };
598 out.line(&format!("case (long long){scope}::{}:", node.name));
599 out.line(&format!(" zdj_raw(\"\\\"{}\\\"\"); break;", node.name));
600 }
601 out.line(&format!(
602 "default: zdj_enum_unknown((long long)({here})); break;"
603 ));
604 out.close("}");
605}
606
607fn emit_bitmask(ast: &Ast, type_: Id<TypeNode>, here: &str, out: &mut Dump) {
614 let prefix = namespace_prefix(ast, type_);
615 out.open("{");
616 out.line("int zd_first = 1;");
617 out.raw("[");
618 for id in ast.bits_of(type_) {
619 let Ok(node) = ast.bit_value(*id) else {
620 continue;
621 };
622 out.open(&format!(
623 "if (((unsigned long long)({here}) & (unsigned long long){prefix}{}) != 0) {{",
624 node.name
625 ));
626 out.line("if (!zd_first) { zdj_raw(\",\"); }");
627 out.line("zd_first = 0;");
628 out.line(&format!("zdj_raw(\"\\\"{}\\\"\");", node.name));
629 out.close("}");
630 }
631 out.raw("]");
632 out.close("}");
633}
634
635fn primitive_call(kind: PrimitiveKind, here: &str) -> String {
636 match kind {
637 PrimitiveKind::Boolean => format!("zdj_bool({here});"),
638 PrimitiveKind::Octet
639 | PrimitiveKind::UInt8
640 | PrimitiveKind::UInt16
641 | PrimitiveKind::UInt32 => format!("zdj_u64((unsigned long long)({here}));"),
642 PrimitiveKind::Int8 | PrimitiveKind::Int16 | PrimitiveKind::Int32 => {
643 format!("zdj_i64((long long)({here}));")
644 }
645 PrimitiveKind::Char => format!("zdj_char((char)({here}));"),
646 PrimitiveKind::WChar => format!("zdj_wchar((unsigned long)({here}));"),
647 PrimitiveKind::Int64 => format!("zdj_i64_string((long long)({here}));"),
648 PrimitiveKind::UInt64 => format!("zdj_u64_string((unsigned long long)({here}));"),
649 PrimitiveKind::Float32 => format!("zdj_f32((float)({here}));"),
650 PrimitiveKind::Float64 | PrimitiveKind::Float128 => {
651 format!("zdj_f64((double)({here}));")
652 }
653 }
654}
655
656fn namespace_prefix(ast: &Ast, type_: Id<TypeNode>) -> String {
659 let scoped = match ast.any_of_type(type_) {
660 Ok(any) => ast.scoped_name_of(any),
661 Err(_) => None,
662 };
663 let Some(scoped) = scoped else {
664 return String::new();
665 };
666 let segments = scoped.segments();
667 if segments.len() <= 1 {
668 return String::new();
669 }
670 let mut prefix = segments[..segments.len() - 1].join("::");
671 prefix.push_str("::");
672 prefix
673}
674
675#[cfg(test)]
676mod tests {
677 #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
678 use super::*;
679 use crate::encode::model_from_idl;
680
681 fn emitted(idl: &str, type_name: &str) -> String {
682 let ast = model_from_idl(idl, "dump_cpp.idl").expect("the fixture parses");
683 let type_ = ast
684 .find_by_scoped_name(&zerodds_ast_core::ScopedName::absolute(vec![
685 type_name.to_string(),
686 ]))
687 .ok()
688 .and_then(zerodds_ast::AnyId::as_type)
689 .expect("the type is in the model");
690 dump_fastdds(&ast, type_).expect("the type has a C++ shape")
691 }
692
693 #[test]
694 fn a_member_is_read_through_its_getter() {
695 let cpp = emitted("@final struct S { long a; string text; };", "S");
696 assert!(cpp.contains("zdj_i64((long long)(sample.a()));"), "{cpp}");
697 assert!(cpp.contains("zdj_string(sample.text().c_str());"), "{cpp}");
698 }
699
700 #[test]
701 fn a_nested_aggregate_chains_the_getters() {
702 let cpp = emitted(
703 "@final struct Inner { long x; };
704 @final struct S { Inner nested; };",
705 "S",
706 );
707 assert!(cpp.contains("sample.nested().x()"), "{cpp}");
708 }
709
710 #[test]
711 fn an_optional_member_is_read_only_when_it_has_a_value() {
712 let cpp = emitted("@mutable struct S { @optional long b; };", "S");
713 assert!(cpp.contains("sample.b().has_value()"), "{cpp}");
714 assert!(cpp.contains("sample.b().value()"), "{cpp}");
715 }
716
717 #[test]
718 fn a_map_prints_as_an_array_of_pairs() {
719 let cpp = emitted("@final struct S { map<long, double> table; };", "S");
720 assert!(cpp.contains(".first"), "{cpp}");
721 assert!(cpp.contains(".second"), "{cpp}");
722 assert!(!cpp.contains("zdj_key(&zd_first, \"table\");\n zdj_raw(\"{\")"));
723 }
724
725 #[test]
726 fn a_bitset_prints_its_named_fields_and_not_its_padding() {
727 let cpp = emitted(
728 "@bit_bound(16) bitset Packed { bitfield<3> lo; bitfield<2>; bitfield<3> hi; };
729 @final struct S { Packed bits; };",
730 "S",
731 );
732 assert!(cpp.contains("sample.bits().lo"), "{cpp}");
733 assert!(cpp.contains("sample.bits().hi"), "{cpp}");
734 }
735
736 #[test]
737 fn a_fixed_is_refused_because_fastddsgen_rejects_the_idl() {
738 let ast = model_from_idl("@final struct S { fixed<7, 2> amount; };", "dump_cpp.idl")
739 .expect("the fixture parses");
740 let type_ = ast
741 .find_by_scoped_name(&zerodds_ast_core::ScopedName::absolute(vec![
742 "S".to_string(),
743 ]))
744 .ok()
745 .and_then(zerodds_ast::AnyId::as_type)
746 .expect("the type is in the model");
747 assert!(matches!(
748 dump_fastdds(&ast, type_),
749 Err(ProbeError::Unsupported { .. })
750 ));
751 }
752}