rpcl2_derive/
lib.rs

1//! This crate provides macros for `ros_pointcloud2`.
2extern crate proc_macro;
3
4use std::collections::HashMap;
5
6use proc_macro::TokenStream;
7use proc_macro2::{Ident, Literal};
8use quote::{quote, ToTokens};
9use syn::{parenthesized, parse_macro_input, Data, DeriveInput, Fields, LitStr};
10
11fn get_allowed_types() -> HashMap<&'static str, usize> {
12    let mut allowed_datatypes = HashMap::<&'static str, usize>::new();
13    allowed_datatypes.insert("f32", std::mem::size_of::<f32>());
14    allowed_datatypes.insert("f64", std::mem::size_of::<f64>());
15    allowed_datatypes.insert("i32", std::mem::size_of::<i32>());
16    allowed_datatypes.insert("u8", std::mem::size_of::<u8>());
17    allowed_datatypes.insert("u16", std::mem::size_of::<u16>());
18    allowed_datatypes.insert("u32", std::mem::size_of::<u32>());
19    allowed_datatypes.insert("i8", std::mem::size_of::<i8>());
20    allowed_datatypes.insert("i16", std::mem::size_of::<i16>());
21    allowed_datatypes
22}
23
24fn struct_field_rename_array(input: &DeriveInput) -> Vec<String> {
25    let fields = match input.data {
26        syn::Data::Struct(ref data) => match data.fields {
27            syn::Fields::Named(ref fields) => &fields.named,
28            _ => panic!("StructNames can only be derived for structs with named fields"),
29        },
30        _ => panic!("StructNames can only be derived for structs"),
31    };
32
33    let mut field_names = Vec::with_capacity(fields.len());
34    for f in fields.iter() {
35        if f.attrs.is_empty() {
36            field_names.push(f.ident.as_ref().unwrap().to_token_stream().to_string());
37        } else {
38            f.attrs.iter().for_each(|attr| {
39                if attr.path().is_ident("ros") {
40                    let res = attr.parse_nested_meta(|meta| {
41                        if meta.path.is_ident("rename") {
42                            let new_name;
43                            parenthesized!(new_name in meta.input);
44                            let lit: LitStr = new_name.parse()?;
45                            field_names.push(lit.value());
46                            Ok(())
47                        } else {
48                            panic!("expected `name` attribute");
49                        }
50                    });
51                    if let Err(e) = res {
52                        panic!("Error parsing attribute: {e}");
53                    }
54                }
55            });
56        }
57    }
58
59    field_names
60}
61
62/// This macro implements the `PointConvertible` trait for your struct so you can use your point for the PointCloud2 conversion.
63///
64/// The struct field names are used in the message if you do not use the `rename` attribute for a custom name.
65///
66/// Note that the repr(C) attribute is required for the struct to work efficiently with C++ PCL.
67/// With Rust layout optimizations, the struct might not work with the PCL library but the message still conforms to the description of PointCloud2.
68/// Furthermore, Rust layout can lead to smaller messages to be send over the network.
69///
70#[proc_macro_derive(PointConvertible, attributes(ros))]
71pub fn ros_point_derive(input: TokenStream) -> TokenStream {
72    let input = parse_macro_input!(input as DeriveInput);
73    let name = input.clone().ident;
74
75    let fields = match input.data {
76        syn::Data::Struct(ref data) => data.fields.clone(),
77        _ => {
78            return syn::Error::new_spanned(input, "Only structs are supported")
79                .to_compile_error()
80                .into()
81        }
82    };
83
84    let allowed_datatypes = get_allowed_types();
85
86    if fields.is_empty() {
87        return syn::Error::new_spanned(input, "No fields found")
88            .to_compile_error()
89            .into();
90    }
91
92    for field in fields.iter() {
93        let ty = field.ty.to_token_stream().to_string();
94        if ty.contains("RGB") || ty.contains("rgb") {
95            return syn::Error::new_spanned(field, "RGB can not be guaranteed to have the correct type or layout. Implement PointConvertible manual or use predefined points instead.")
96                .to_compile_error()
97                .into();
98        }
99        if !allowed_datatypes.contains_key(&ty.as_str()) {
100            return syn::Error::new_spanned(field, "Field type not allowed")
101                .to_compile_error()
102                .into();
103        }
104    }
105
106    let field_len_token: usize = fields.len();
107    let rename_arr = struct_field_rename_array(&input);
108    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
109    let layout = layout_of_type(&name, &input.data);
110
111    let expanded = quote! {
112        unsafe impl #impl_generics ::ros_pointcloud2::PointConvertible<#field_len_token> for #name #ty_generics #where_clause {
113            fn layout() -> ::ros_pointcloud2::LayoutDescription {
114                let mut last_field_end = 0;
115                let mut fields = Vec::new();
116
117                #layout
118
119                let mut rename_idx = 0;
120                let rename_arr = vec![#(#rename_arr),*];
121                let field_info: Vec<::ros_pointcloud2::LayoutField> = fields.into_iter().map(|field| {
122                    match field {
123                        (0, ty, size) => {
124                            rename_idx += 1;
125                            ::ros_pointcloud2::LayoutField::new(rename_arr[rename_idx - 1], ty, size)
126                        },
127                        (1, _, size) => ::ros_pointcloud2::LayoutField::padding(size),
128                        _ => unreachable!(),
129                    }
130                }).collect();
131
132                ::ros_pointcloud2::LayoutDescription::new(field_info.as_slice())
133            }
134        }
135    };
136
137    let field_names_get = fields
138        .iter()
139        .enumerate()
140        .map(|(idx, f)| {
141            let field_name = f.ident.as_ref().unwrap();
142            quote! { #field_name: point[#idx].get() }
143        })
144        .collect::<Vec<_>>();
145
146    let from_my_point = quote! {
147        impl From<ros_pointcloud2::IPoint<#field_len_token>> for #name {
148            fn from(point: ros_pointcloud2::IPoint<#field_len_token>) -> Self {
149                Self {
150                    #(#field_names_get,)*
151                }
152            }
153        }
154    };
155
156    let field_names_into = fields
157        .iter()
158        .map(|f| {
159            let field_name = f.ident.as_ref().unwrap();
160            quote! { point.#field_name.into() }
161        })
162        .collect::<Vec<_>>();
163
164    let from_custom_point = quote! {
165        impl From<#name> for ros_pointcloud2::IPoint<#field_len_token> {
166            fn from(point: #name) -> Self {
167                [ #(#field_names_into,)* ].into()
168            }
169        }
170    };
171
172    TokenStream::from(quote! {
173        #expanded
174        #from_my_point
175        #from_custom_point
176    })
177}
178
179fn layout_of_type(struct_name: &Ident, data: &Data) -> proc_macro2::TokenStream {
180    match data {
181        Data::Struct(data) => match &data.fields {
182            Fields::Named(fields) => {
183                let values = fields.named.iter().map(|field| {
184                    let field_name = field.ident.as_ref().unwrap();
185                    let field_ty = &field.ty;
186                    let field_ty_str = Literal::string(&field_ty.to_token_stream().to_string());
187                    let field_ty = &field.ty;
188
189                    quote! {
190                        let size = ::core::mem::size_of::<#field_ty>();
191                        let offset = ::core::mem::offset_of!(#struct_name, #field_name);
192                        if offset > last_field_end {
193                            fields.push((1, "", offset - last_field_end));
194                        }
195                        fields.push((0, #field_ty_str, size));
196                        last_field_end = offset + size;
197                    }
198                });
199
200                quote! {
201                    #(#values)*
202
203                    let struct_size = ::std::mem::size_of::<#struct_name>();
204                    if struct_size > last_field_end {
205                        fields.push((1, "", struct_size - last_field_end));
206                    }
207                }
208            }
209            Fields::Unnamed(_) => unimplemented!(),
210            Fields::Unit => unimplemented!(),
211        },
212        Data::Enum(_) | Data::Union(_) => unimplemented!("type-layout only supports structs"),
213    }
214}