Skip to main content

hdds_codegen/
lib.rs

1// SPDX-License-Identifier: Apache-2.0 OR MIT
2// Copyright (c) 2025-2026 naskel.com
3
4extern crate proc_macro;
5
6use proc_macro::TokenStream;
7use quote::quote;
8use syn::{parse_macro_input, Data, DeriveInput, Fields, GenericArgument, PathArguments, Type};
9
10/// Field kind for code generation
11#[derive(Clone)]
12enum FieldKind {
13    /// Fixed-size primitive type (u8, i32, f64, etc.)
14    Primitive {
15        size: usize,
16        alignment: usize,
17        kind_tokens: proc_macro2::TokenStream,
18    },
19    /// String type (variable size)
20    String,
21    /// Vec<u8> type (variable size byte array)
22    ByteVec,
23}
24
25/// `#[derive(DDS)]` macro: generates `TypeDescriptor` + encode/decode impl
26///
27/// Supports:
28/// - Primitive types: i8, i16, i32, i64, u8, u16, u32, u64, f32, f64, bool
29/// - String type: variable-length UTF-8 string
30/// - Vec<u8>: variable-length byte array
31///
32/// # Panics
33///
34/// Panics if struct contains unsupported field types or unnamed fields
35///
36/// Example:
37/// ```ignore
38/// use hdds_codegen::DDS;
39///
40/// #[derive(DDS)]
41/// struct ImageMeta {
42///     image_id: u32,
43///     width: u16,
44///     height: u16,
45///     format: String,      // Variable-length string
46///     data: Vec<u8>,       // Variable-length byte array
47/// }
48/// ```
49#[proc_macro_derive(DDS, attributes(dds))]
50#[allow(clippy::too_many_lines)]
51pub fn derive_dds(input: TokenStream) -> TokenStream {
52    let input = parse_macro_input!(input as DeriveInput);
53
54    let name = &input.ident;
55    let type_name = name.to_string();
56    let type_id = compute_fnv1a_hash(&type_name);
57
58    // Parse struct fields
59    let fields = match &input.data {
60        Data::Struct(data) => match &data.fields {
61            Fields::Named(f) => &f.named,
62            _ => {
63                return syn::Error::new_spanned(&input, "Only named fields are supported")
64                    .to_compile_error()
65                    .into()
66            }
67        },
68        _ => {
69            return syn::Error::new_spanned(&input, "Only structs are supported")
70                .to_compile_error()
71                .into()
72        }
73    };
74
75    // Generate field info with proper CDR2 alignment
76    struct FieldInfo {
77        name: syn::Ident,
78        ty: syn::Type,
79        kind: FieldKind,
80        offset: usize, // Only valid for fixed-size fields
81    }
82
83    let mut field_infos = Vec::new();
84    let mut current_offset = 0usize;
85    let mut max_alignment = 1usize;
86    let mut has_variable_size = false;
87
88    for field in fields {
89        let Some(field_name) = field.ident.as_ref() else {
90            return syn::Error::new_spanned(field, "Field must have a name")
91                .to_compile_error()
92                .into();
93        };
94        let field_type = &field.ty;
95
96        let Some(kind) = get_field_kind(field_type) else {
97            return syn::Error::new_spanned(
98                field_type,
99                format!("Unsupported type: {field_type:?}. Supported types: primitives, String, Vec<u8>."),
100            )
101            .to_compile_error()
102            .into();
103        };
104
105        let (size, alignment) = match &kind {
106            FieldKind::Primitive {
107                size, alignment, ..
108            } => (*size, *alignment),
109            FieldKind::String | FieldKind::ByteVec => {
110                has_variable_size = true;
111                (0, 4) // Length prefix is u32, aligned to 4
112            }
113        };
114
115        // Align offset to field alignment (CDR2 requirement)
116        current_offset = align_to(current_offset, alignment);
117        max_alignment = max_alignment.max(alignment);
118
119        field_infos.push(FieldInfo {
120            name: field_name.clone(),
121            ty: field_type.clone(),
122            kind,
123            offset: current_offset,
124        });
125
126        if !has_variable_size {
127            current_offset += size;
128        }
129    }
130
131    let total_size = if has_variable_size {
132        0xFFFF_FFFF_u32 // Variable size marker
133    } else {
134        align_to(current_offset, max_alignment) as u32
135    };
136
137    // Generate TypeDescriptor fields array
138    let field_layouts: Vec<_> = field_infos
139        .iter()
140        .map(|f| {
141            let name_str = f.name.to_string();
142            let offset = f.offset as u32;
143
144            match &f.kind {
145                FieldKind::Primitive {
146                    size,
147                    alignment,
148                    kind_tokens,
149                } => {
150                    let size = *size as u32;
151                    let alignment = *alignment as u8;
152                    quote! {
153                        ::hdds::core::types::FieldLayout {
154                            name: #name_str,
155                            offset_bytes: #offset,
156                            field_type: ::hdds::core::types::FieldType::Primitive(#kind_tokens),
157                            alignment: #alignment,
158                            size_bytes: #size,
159                            element_type: None,
160                        }
161                    }
162                }
163                FieldKind::String => {
164                    quote! {
165                        ::hdds::core::types::FieldLayout {
166                            name: #name_str,
167                            offset_bytes: #offset,
168                            field_type: ::hdds::core::types::FieldType::String,
169                            alignment: 4,
170                            size_bytes: 0xFFFF_FFFF, // Variable
171                            element_type: None,
172                        }
173                    }
174                }
175                FieldKind::ByteVec => {
176                    quote! {
177                        ::hdds::core::types::FieldLayout {
178                            name: #name_str,
179                            offset_bytes: #offset,
180                            field_type: ::hdds::core::types::FieldType::Sequence,
181                            alignment: 4,
182                            size_bytes: 0xFFFF_FFFF, // Variable
183                            element_type: None,
184                        }
185                    }
186                }
187            }
188        })
189        .collect();
190
191    // Generate encode_cdr2 implementation
192    let encode_fields: Vec<_> = field_infos
193        .iter()
194        .map(|f| {
195            let field_name = &f.name;
196
197            match &f.kind {
198                FieldKind::Primitive { alignment, .. } => {
199                    quote! {
200                        // Align cursor to field alignment
201                        while cursor.offset() % #alignment != 0 {
202                            cursor.write_u8(0)?;
203                        }
204                        // Write field value as little-endian
205                        cursor.write_bytes(&self.#field_name.to_le_bytes())?;
206                    }
207                }
208                FieldKind::String => {
209                    quote! {
210                        // Align to 4 bytes for length prefix
211                        while cursor.offset() % 4 != 0 {
212                            cursor.write_u8(0)?;
213                        }
214                        // Write string: length (u32) + bytes + null terminator
215                        let str_bytes = self.#field_name.as_bytes();
216                        let str_len = (str_bytes.len() + 1) as u32; // Include null terminator
217                        cursor.write_bytes(&str_len.to_le_bytes())?;
218                        cursor.write_bytes(str_bytes)?;
219                        cursor.write_u8(0)?; // Null terminator
220                    }
221                }
222                FieldKind::ByteVec => {
223                    quote! {
224                        // Align to 4 bytes for length prefix
225                        while cursor.offset() % 4 != 0 {
226                            cursor.write_u8(0)?;
227                        }
228                        // Write Vec<u8>: length (u32) + bytes
229                        let vec_len = self.#field_name.len() as u32;
230                        cursor.write_bytes(&vec_len.to_le_bytes())?;
231                        cursor.write_bytes(&self.#field_name)?;
232                    }
233                }
234            }
235        })
236        .collect();
237
238    // Generate decode_cdr2 implementation
239    let decode_fields: Vec<_> = field_infos
240        .iter()
241        .map(|f| {
242            let field_name = &f.name;
243            let field_type = &f.ty;
244
245            match &f.kind {
246                FieldKind::Primitive { size, alignment, .. } => {
247                    quote! {
248                        // Align cursor to field alignment
249                        while cursor.offset() % #alignment != 0 {
250                            let _ = cursor.read_u8()?;
251                        }
252                        // Read field value as little-endian
253                        let #field_name = {
254                            let bytes_slice = cursor.read_bytes(#size)?;
255                            let mut bytes = [0u8; #size];
256                            bytes.copy_from_slice(bytes_slice);
257                            <#field_type>::from_le_bytes(bytes)
258                        };
259                    }
260                }
261                FieldKind::String => {
262                    quote! {
263                        // Align to 4 bytes for length prefix
264                        while cursor.offset() % 4 != 0 {
265                            let _ = cursor.read_u8()?;
266                        }
267                        // Read string: length (u32) + bytes + null terminator
268                        let #field_name = {
269                            let len_bytes = cursor.read_bytes(4)?;
270                            let str_len = u32::from_le_bytes([len_bytes[0], len_bytes[1], len_bytes[2], len_bytes[3]]) as usize;
271                            if str_len == 0 {
272                                String::new()
273                            } else {
274                                let str_bytes = cursor.read_bytes(str_len - 1)?; // Exclude null terminator
275                                let _ = cursor.read_u8()?; // Skip null terminator
276                                String::from_utf8(str_bytes.to_vec())
277                                    .map_err(|_| ::hdds::dds::Error::SerializationError)?
278                            }
279                        };
280                    }
281                }
282                FieldKind::ByteVec => {
283                    quote! {
284                        // Align to 4 bytes for length prefix
285                        while cursor.offset() % 4 != 0 {
286                            let _ = cursor.read_u8()?;
287                        }
288                        // Read Vec<u8>: length (u32) + bytes
289                        let #field_name = {
290                            let len_bytes = cursor.read_bytes(4)?;
291                            let vec_len = u32::from_le_bytes([len_bytes[0], len_bytes[1], len_bytes[2], len_bytes[3]]) as usize;
292                            let data = cursor.read_bytes(vec_len)?;
293                            data.to_vec()
294                        };
295                    }
296                }
297            }
298        })
299        .collect();
300
301    let field_names: Vec<_> = field_infos.iter().map(|f| &f.name).collect();
302
303    // Generate CompleteStructMembers for TypeObject (Phase 8b)
304    let type_object_members: Vec<_> = field_infos
305        .iter()
306        .enumerate()
307        .map(|(idx, f)| {
308            let Ok(member_id) = u32::try_from(idx) else {
309                return syn::Error::new_spanned(
310                    &f.name,
311                    format!("Struct has too many fields (index {idx} exceeds u32::MAX)"),
312                )
313                .to_compile_error();
314            };
315            let name_str = f.name.to_string();
316            let type_id_const = get_type_identifier_for_kind(&f.kind);
317
318            quote! {
319                ::hdds::xtypes::CompleteStructMember {
320                    common: ::hdds::xtypes::CommonStructMember {
321                        member_id: #member_id,
322                        member_flags: ::hdds::xtypes::MemberFlag::empty(),
323                        member_type_id: #type_id_const,
324                    },
325                    detail: ::hdds::xtypes::CompleteMemberDetail::new(#name_str),
326                }
327            }
328        })
329        .collect();
330
331    let max_alignment_u8 = max_alignment as u8;
332
333    let expanded = quote! {
334        impl ::hdds::api::DDS for #name {
335            fn type_descriptor() -> &'static ::hdds::core::types::TypeDescriptor {
336                static DESCRIPTOR: ::hdds::core::types::TypeDescriptor = ::hdds::core::types::TypeDescriptor {
337                    type_id: #type_id,
338                    type_name: #type_name,
339                    size_bytes: #total_size,
340                    alignment: #max_alignment_u8,
341                    is_variable_size: #has_variable_size,
342                    fields: &[#(#field_layouts),*],
343                };
344                &DESCRIPTOR
345            }
346
347            fn encode_cdr2(&self, buf: &mut [u8]) -> ::hdds::api::Result<usize> {
348                use ::hdds::core::ser::cursor::CursorMut;
349
350                let mut cursor = CursorMut::new(buf);
351
352                // Encode each field with proper alignment
353                #(#encode_fields)*
354
355                Ok(cursor.offset())
356            }
357
358            fn decode_cdr2(buf: &[u8]) -> ::hdds::api::Result<Self> {
359                use ::hdds::core::ser::cursor::Cursor;
360
361                let mut cursor = Cursor::new(buf);
362
363                // Decode each field with proper alignment
364                #(#decode_fields)*
365
366                Ok(Self {
367                    #(#field_names),*
368                })
369            }
370
371            /// Get XTypes v1.3 TypeObject for this type
372            ///
373            /// Auto-generated by #[derive(DDS)] proc-macro (Phase 8b).
374            /// Returns CompleteTypeObject::Struct with all field metadata.
375            ///
376            /// # XTypes v1.3 Integration
377            ///
378            /// This enables:
379            /// - Runtime type discovery via SEDP announcements
380            /// - Structural type equivalence checking (EquivalenceHash)
381            /// - Multi-vendor interoperability (FastDDS, RTI, etc.)
382            ///
383            /// # Generated Structure
384            ///
385            /// - Extensibility: IS_FINAL (MVP, future: @appendable/@mutable)
386            /// - Members: Sequential member_id assignment (0, 1, 2, ...)
387            /// - Member flags: Empty (future: @key, @optional, @must_understand)
388            /// - Type IDs: Primitive TypeIdentifier constants (TK_INT32, TK_FLOAT32, etc.)
389            fn get_type_object() -> Option<::hdds::xtypes::CompleteTypeObject> {
390                Some(::hdds::xtypes::CompleteTypeObject::Struct(
391                    ::hdds::xtypes::CompleteStructType {
392                        struct_flags: ::hdds::xtypes::StructTypeFlag::IS_FINAL,
393                        header: ::hdds::xtypes::CompleteStructHeader {
394                            base_type: None, // No inheritance (Phase 8b MVP)
395                            detail: ::hdds::xtypes::CompleteTypeDetail::new(#type_name),
396                        },
397                        member_seq: vec![
398                            #(#type_object_members),*
399                        ],
400                    }
401                ))
402            }
403        }
404    };
405
406    TokenStream::from(expanded)
407}
408
409/// Get field kind for a Rust type
410///
411/// Supports:
412/// - Primitive types: i8, i16, i32, i64, u8, u16, u32, u64, f32, f64, bool
413/// - String: variable-length UTF-8 string
414/// - Vec<u8>: variable-length byte array
415fn get_field_kind(ty: &syn::Type) -> Option<FieldKind> {
416    if let Type::Path(type_path) = ty {
417        let segment = type_path.path.segments.last()?;
418        let ident_str = segment.ident.to_string();
419
420        // Check for primitive types
421        match ident_str.as_str() {
422            "i8" => {
423                return Some(FieldKind::Primitive {
424                    size: 1,
425                    alignment: 1,
426                    kind_tokens: quote! { ::hdds::core::types::PrimitiveKind::I8 },
427                })
428            }
429            "i16" => {
430                return Some(FieldKind::Primitive {
431                    size: 2,
432                    alignment: 2,
433                    kind_tokens: quote! { ::hdds::core::types::PrimitiveKind::I16 },
434                })
435            }
436            "i32" => {
437                return Some(FieldKind::Primitive {
438                    size: 4,
439                    alignment: 4,
440                    kind_tokens: quote! { ::hdds::core::types::PrimitiveKind::I32 },
441                })
442            }
443            "i64" => {
444                return Some(FieldKind::Primitive {
445                    size: 8,
446                    alignment: 8,
447                    kind_tokens: quote! { ::hdds::core::types::PrimitiveKind::I64 },
448                })
449            }
450            "u8" => {
451                return Some(FieldKind::Primitive {
452                    size: 1,
453                    alignment: 1,
454                    kind_tokens: quote! { ::hdds::core::types::PrimitiveKind::U8 },
455                })
456            }
457            "u16" => {
458                return Some(FieldKind::Primitive {
459                    size: 2,
460                    alignment: 2,
461                    kind_tokens: quote! { ::hdds::core::types::PrimitiveKind::U16 },
462                })
463            }
464            "u32" => {
465                return Some(FieldKind::Primitive {
466                    size: 4,
467                    alignment: 4,
468                    kind_tokens: quote! { ::hdds::core::types::PrimitiveKind::U32 },
469                })
470            }
471            "u64" => {
472                return Some(FieldKind::Primitive {
473                    size: 8,
474                    alignment: 8,
475                    kind_tokens: quote! { ::hdds::core::types::PrimitiveKind::U64 },
476                })
477            }
478            "f32" => {
479                return Some(FieldKind::Primitive {
480                    size: 4,
481                    alignment: 4,
482                    kind_tokens: quote! { ::hdds::core::types::PrimitiveKind::F32 },
483                })
484            }
485            "f64" => {
486                return Some(FieldKind::Primitive {
487                    size: 8,
488                    alignment: 8,
489                    kind_tokens: quote! { ::hdds::core::types::PrimitiveKind::F64 },
490                })
491            }
492            "bool" | "boolean" => {
493                return Some(FieldKind::Primitive {
494                    size: 1,
495                    alignment: 1,
496                    kind_tokens: quote! { ::hdds::core::types::PrimitiveKind::Bool },
497                })
498            }
499            "String" => return Some(FieldKind::String),
500            "Vec" => {
501                // Check if it's Vec<u8>
502                if let PathArguments::AngleBracketed(args) = &segment.arguments {
503                    if let Some(GenericArgument::Type(Type::Path(inner_path))) = args.args.first() {
504                        if let Some(inner_segment) = inner_path.path.segments.last() {
505                            if inner_segment.ident == "u8" {
506                                return Some(FieldKind::ByteVec);
507                            }
508                        }
509                    }
510                }
511                return None; // Vec<T> where T != u8 is not supported
512            }
513            _ => return None,
514        }
515    }
516    None
517}
518
519/// Align offset to the specified alignment (round up to next multiple)
520#[allow(clippy::integer_division_remainder_used, clippy::integer_division)]
521const fn align_to(offset: usize, alignment: usize) -> usize {
522    // div_ceil is not const stable yet (1.73+), manual implementation for older MSRV
523    #[allow(clippy::arithmetic_side_effects)] // Alignment is always > 0
524    #[allow(clippy::manual_div_ceil)] // div_ceil not const stable yet
525    {
526        (offset + alignment - 1) / alignment * alignment
527    }
528}
529
530/// Compute FNV-1a hash (32-bit) for type ID
531fn compute_fnv1a_hash(s: &str) -> u32 {
532    let mut hash = 2_166_136_261_u32;
533    for byte in s.bytes() {
534        hash ^= u32::from(byte);
535        hash = hash.wrapping_mul(16_777_619);
536    }
537    hash
538}
539
540/// Map `FieldKind` to `TypeIdentifier` constant for XTypes (Phase 8b)
541///
542/// # Mapping
543/// - Primitives: I8 -> TK_INT8, etc.
544/// - String -> TK_STRING8
545/// - ByteVec -> TK_SEQUENCE (of u8)
546fn get_type_identifier_for_kind(kind: &FieldKind) -> proc_macro2::TokenStream {
547    match kind {
548        FieldKind::Primitive { kind_tokens, .. } => {
549            let prim_str = kind_tokens.to_string();
550            let variant = prim_str.split("::").last().unwrap_or("").trim();
551
552            match variant {
553                "I8" => quote! { ::hdds::xtypes::TypeIdentifier::TK_INT8 },
554                "I16" => quote! { ::hdds::xtypes::TypeIdentifier::TK_INT16 },
555                "I32" => quote! { ::hdds::xtypes::TypeIdentifier::TK_INT32 },
556                "I64" => quote! { ::hdds::xtypes::TypeIdentifier::TK_INT64 },
557                "U8" => quote! { ::hdds::xtypes::TypeIdentifier::TK_UINT8 },
558                "U16" => quote! { ::hdds::xtypes::TypeIdentifier::TK_UINT16 },
559                "U32" => quote! { ::hdds::xtypes::TypeIdentifier::TK_UINT32 },
560                "U64" => quote! { ::hdds::xtypes::TypeIdentifier::TK_UINT64 },
561                "F32" => quote! { ::hdds::xtypes::TypeIdentifier::TK_FLOAT32 },
562                "F64" => quote! { ::hdds::xtypes::TypeIdentifier::TK_FLOAT64 },
563                "Bool" => quote! { ::hdds::xtypes::TypeIdentifier::TK_BOOLEAN },
564                _ => quote! {
565                    compile_error!(concat!("Internal error: unsupported primitive kind: ", #variant))
566                },
567            }
568        }
569        FieldKind::String => {
570            quote! { ::hdds::xtypes::TypeIdentifier::TK_STRING8 }
571        }
572        FieldKind::ByteVec => {
573            // Sequence of u8 - use primitive TK_SEQUENCE marker
574            // Note: Full XTypes would encode element type in a PlainSequence/Sequence variant
575            quote! { ::hdds::xtypes::TypeIdentifier::Primitive(::hdds::xtypes::TypeKind::TK_SEQUENCE) }
576        }
577    }
578}