Skip to main content

rusteron_code_gen/
parser.rs

1use crate::generator::{parse_custom_methods, CBinding, CWrapper, Method};
2use crate::{Arg, ArgProcessing, CHandler};
3use itertools::Itertools;
4use quote::ToTokens;
5use std::collections::{BTreeMap, BTreeSet};
6use std::fs;
7use std::path::PathBuf;
8use syn::{Attribute, Item, ItemForeignMod, ItemStruct, ItemType, Lit, Meta, MetaNameValue};
9
10pub fn parse_bindings(out: &PathBuf) -> CBinding {
11    parse_bindings_with_custom(out, &[])
12}
13
14/// Like [`parse_bindings`], additionally scanning per-crate custom code (appended after the
15/// common `aeron_custom.rs`) so its hand-written methods are skipped during generation.
16pub fn parse_bindings_with_custom(out: &PathBuf, extra_custom_code: &[&str]) -> CBinding {
17    let file_content = fs::read_to_string(out.clone()).expect("Unable to read file");
18    let syntax_tree = syn::parse_file(&file_content).expect("Unable to parse file");
19    let mut wrappers = BTreeMap::new();
20    let mut methods = Vec::new();
21    let mut handlers = Vec::new();
22
23    let items = syntax_tree.items;
24
25    for item in &items {
26        if let Item::Type(ty) = item {
27            process_type(&mut wrappers, &mut handlers, ty);
28        }
29    }
30
31    let handler_names = handlers
32        .iter()
33        .filter(|h| {
34            !["aeron_udp_channel", "aeron_udp_transport"]
35                .iter()
36                .any(|&filter| h.type_name.starts_with(filter))
37        })
38        .map(|handler| handler.type_name.clone())
39        .collect();
40
41    for item in &items {
42        if let Item::Struct(s) = item {
43            process_struct(&mut wrappers, s, &handler_names);
44        }
45    }
46
47    for item in &items {
48        if let Item::ForeignMod(fm) = item {
49            process_c_method(&mut wrappers, &mut methods, fm, &handler_names);
50        }
51    }
52
53    let mut bindings = CBinding {
54        wrappers: wrappers
55            .into_iter()
56            .filter(|(_, wrapper)| {
57                // these are from media driver and do not follow convention
58                ![
59                    "aeron_thread",
60                    "aeron_command",
61                    "aeron_executor",
62                    "aeron_name_resolver",
63                    "aeron_udp_channel_transport", // this one I have issues with handlers
64                    "aeron_udp_transport",         // this one I have issues with handlers
65                ]
66                .iter()
67                .any(|&filter| wrapper.type_name.starts_with(filter))
68            })
69            .collect(),
70        methods,
71        handlers: handlers
72            .into_iter()
73            .filter(|h| {
74                !["aeron_udp_channel", "aeron_udp_transport"]
75                    .iter()
76                    .any(|&filter| h.type_name.starts_with(filter))
77            })
78            .collect(),
79    };
80
81    let mismatched_types = bindings
82        .wrappers
83        .iter()
84        .filter(|(key, w)| key.as_str() != w.type_name)
85        .map(|(a, b)| (a.clone(), b.clone()))
86        .collect_vec();
87    assert_eq!(Vec::<(String, CWrapper)>::new(), mismatched_types);
88
89    let mut custom = parse_custom_methods(crate::CUSTOM_AERON_CODE);
90    for code in extra_custom_code {
91        for (class, methods) in parse_custom_methods(code) {
92            custom.entry(class).or_default().extend(methods);
93        }
94    }
95    for wrapper in bindings.wrappers.values_mut() {
96        if let Some(methods) = custom.get(&wrapper.class_name) {
97            wrapper.skipped_methods = methods.clone();
98        }
99    }
100
101    bindings
102}
103
104fn process_c_method(
105    wrappers: &mut BTreeMap<String, CWrapper>,
106    methods: &mut Vec<Method>,
107    fm: &ItemForeignMod,
108    handler_names: &BTreeSet<String>,
109) {
110    // Extract functions inside extern "C" blocks
111    if fm.abi.name.is_some() && fm.abi.name.as_ref().unwrap().value() == "C" {
112        for foreign_item in &fm.items {
113            if let syn::ForeignItem::Fn(f) = foreign_item {
114                let docs = get_doc_comments(&f.attrs);
115                let fn_name = f.sig.ident.to_string();
116
117                // aeronc.h has a deprecated typo alias:
118                //   aeron_async_add_exclusive_exclusive_publication_get_registration_id
119                // (note the doubled "exclusive"). It attaches to the same wrapper as the
120                // canonical aeron_async_add_exclusive_publication_get_registration_id and
121                // would either collide with it or emit a garbled method name. Upstream
122                // marks it @deprecated, so drop it outright.
123                if fn_name.contains("exclusive_exclusive") {
124                    continue;
125                }
126
127                // Get function arguments and return type as Rust code
128                let args = extract_function_arguments(&f.sig.inputs);
129                let ret = extract_return_type(&f.sig.output);
130
131                let option = if let Some(arg) = args
132                    .iter()
133                    .skip_while(|a| a.is_mut_pointer() && a.is_primitive())
134                    .next()
135                {
136                    let ty = &arg.c_type;
137                    let ty = ty.split(' ').last().map(|t| t.to_string()).unwrap();
138                    if wrappers.contains_key(&ty) {
139                        Some(ty)
140                    } else {
141                        find_closest_wrapper_from_method_name(wrappers, &fn_name)
142                    }
143                } else {
144                    find_closest_wrapper_from_method_name(wrappers, &fn_name)
145                };
146
147                match option {
148                    Some(key) => {
149                        let wrapper = wrappers.get_mut(&key).unwrap();
150                        wrapper.methods.push(Method {
151                            fn_name: fn_name.clone(),
152                            struct_method_name: fn_name
153                                .replace(&wrapper.type_name[..wrapper.type_name.len() - 1], "")
154                                .to_string(),
155                            return_type: Arg {
156                                name: "".to_string(),
157                                c_type: ret.clone(),
158                                processing: ArgProcessing::Default,
159                            },
160                            arguments: process_types(args.clone(), Some(handler_names)),
161                            docs: docs.clone(),
162                        });
163                    }
164                    None => methods.push(Method {
165                        fn_name: fn_name.clone(),
166                        struct_method_name: "".to_string(),
167                        return_type: Arg {
168                            name: "".to_string(),
169                            c_type: ret.clone(),
170                            processing: ArgProcessing::Default,
171                        },
172                        arguments: process_types(args.clone(), Some(handler_names)),
173                        docs: docs.clone(),
174                    }),
175                }
176            }
177        }
178    }
179}
180
181fn find_closest_wrapper_from_method_name(
182    wrappers: &mut BTreeMap<String, CWrapper>,
183    fn_name: &String,
184) -> Option<String> {
185    let type_names = get_possible_wrappers(&fn_name);
186
187    let mut value = None;
188    for ty in type_names {
189        if wrappers.contains_key(&ty) {
190            value = Some(ty);
191            break;
192        }
193    }
194
195    value
196}
197
198pub fn get_possible_wrappers(fn_name: &str) -> Vec<String> {
199    // Try all possible wrapper names by splitting at underscores
200    fn_name
201        .char_indices()
202        .filter(|(_, c)| *c == '_')
203        .map(|(i, _)| format!("{}_t", &fn_name[..i]))
204        .rev()
205        .collect_vec()
206}
207
208fn process_type(wrappers: &mut BTreeMap<String, CWrapper>, handlers: &mut Vec<CHandler>, ty: &ItemType) {
209    // Handle type definitions and get docs
210    let docs = get_doc_comments(&ty.attrs);
211
212    let type_name = ty.ident.to_string();
213    let class_name = snake_to_pascal_case(&type_name);
214
215    if is_struct_typedef(&ty.ty) {
216        wrappers
217            .entry(type_name.clone())
218            .or_insert(CWrapper {
219                class_name,
220                without_name: type_name[..type_name.len() - 2].to_string(),
221                type_name,
222                ..Default::default()
223            })
224            .docs
225            .extend(docs);
226    } else {
227        // Parse the function pointer type -> it is typically used for handlers/callbacks
228        if let syn::Type::Path(type_path) = &*ty.ty {
229            if let Some(segment) = type_path.path.segments.last() {
230                if segment.ident.to_string() == "Option" {
231                    if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
232                        if let Some(syn::GenericArgument::Type(syn::Type::BareFn(bare_fn))) = args.args.first() {
233                            let args: Vec<Arg> = bare_fn
234                                .inputs
235                                .iter()
236                                .map(|arg| {
237                                    let arg_name = match &arg.name {
238                                        Some((ident, _)) => ident.to_string(),
239                                        None => "".to_string(),
240                                    };
241                                    let arg_type = arg.ty.to_token_stream().to_string();
242                                    (arg_name, arg_type)
243                                })
244                                .map(|(field_name, field_type)| Arg {
245                                    name: field_name,
246                                    c_type: field_type,
247                                    processing: ArgProcessing::Default,
248                                })
249                                .collect();
250                            let string = bare_fn.output.to_token_stream().to_string();
251                            let mut return_type = string.trim();
252
253                            if return_type.starts_with("-> ") {
254                                return_type = &return_type[3..];
255                            }
256
257                            if return_type.is_empty() {
258                                return_type = "()";
259                            }
260
261                            if is_handler_typedef(&args) {
262                                let value = CHandler {
263                                    type_name: ty.ident.to_string(),
264                                    args: process_types(args, None),
265                                    return_type: Arg {
266                                        name: "".to_string(),
267                                        c_type: return_type.to_string(),
268                                        processing: ArgProcessing::Default,
269                                    },
270                                    docs: docs.clone(),
271                                    fn_mut_signature: Default::default(),
272                                    closure_type_name: Default::default(),
273                                };
274                                handlers.push(value);
275                            }
276                        }
277                    }
278                }
279            }
280        }
281    }
282}
283
284fn is_handler_typedef(args: &[Arg]) -> bool {
285    args.iter().filter(|arg| arg.is_c_void()).count() == 1
286        || args
287            .first()
288            .map(|arg| arg.is_c_void() && is_client_data_arg(&arg.name))
289            .unwrap_or(false)
290}
291
292fn is_client_data_arg(name: &str) -> bool {
293    name == "clientd"
294        || name == "state"
295        || name == "task_clientd"
296        || name.ends_with("_clientd")
297        || name.ends_with("_state")
298}
299
300fn is_struct_typedef(ty: &syn::Type) -> bool {
301    if let syn::Type::Path(type_path) = ty {
302        if let Some(segment) = type_path.path.segments.last() {
303            return segment.ident.to_string().ends_with("_stct");
304        }
305    }
306
307    false
308}
309
310fn process_struct(wrappers: &mut BTreeMap<String, CWrapper>, s: &ItemStruct, handler_names: &BTreeSet<String>) {
311    // Print the struct name and its doc comments
312    let docs = get_doc_comments(&s.attrs);
313    let type_name = s.ident.to_string().replace("_stct", "_t");
314    let class_name = snake_to_pascal_case(&type_name);
315
316    let fields: Vec<Arg> = s
317        .fields
318        .iter()
319        .map(|f| {
320            let field_name = f.ident.as_ref().unwrap().to_string();
321            let field_type = f.ty.to_token_stream().to_string();
322            (field_name, field_type)
323        })
324        .map(|(field_name, field_type)| Arg {
325            name: field_name,
326            c_type: field_type,
327            processing: ArgProcessing::Default,
328        })
329        .collect();
330
331    let w = wrappers.entry(type_name.to_string()).or_insert(CWrapper {
332        class_name,
333        without_name: type_name[..type_name.len() - 2].to_string(),
334        type_name,
335        ..Default::default()
336    });
337    w.docs.extend(docs);
338    w.fields = process_types(fields, Some(handler_names));
339}
340
341fn process_types(mut name_and_type: Vec<Arg>, handler_names: Option<&BTreeSet<String>>) -> Vec<Arg> {
342    // now mark arguments which can be reduced
343    for i in 1..name_and_type.len() {
344        let param1 = &name_and_type[i - 1];
345        let param2 = &name_and_type[i];
346
347        let is_int = param2.c_type == "usize" || param2.c_type == "i32";
348        // Length fields use varied names across Aeron headers; match the common ones so
349        // (buffer, length) pairs merge into a slice rather than leaking two raw params.
350        let length_field = matches!(param2.name.as_str(), "length" | "len" | "count" | "capacity")
351            || param2.name.ends_with("_length")
352            || param2.name.ends_with("_len")
353            || param2.name.ends_with("_size");
354        if param2.is_c_void()
355            && !param1.is_mut_pointer()
356            && param1.c_type.ends_with("_t")
357            && handler_names
358                .map(|handler_names| handler_names.contains(&param1.c_type))
359                .unwrap_or(false)
360        {
361            // closures
362            //         handler: aeron_on_available_counter_t,
363            //         clientd: *mut ::std::os::raw::c_void,
364            let processing = ArgProcessing::Handler(vec![param1.clone(), param2.clone()]);
365            name_and_type[i - 1].processing = processing.clone();
366            name_and_type[i].processing = processing.clone();
367        } else if param1.is_c_string_any() && is_int && length_field {
368            //     pub stripped_channel: *mut ::std::os::raw::c_char,
369            //     pub stripped_channel_length: usize,
370            // `*const` pairs become `&str` arguments; `*mut` pairs are C fill-buffers
371            // (snprintf-style) and become `&mut [u8]` arguments / `&str` field reads.
372            let processing = ArgProcessing::StringWithLength(vec![param1.clone(), param2.clone()]);
373            name_and_type[i - 1].processing = processing.clone();
374            name_and_type[i].processing = processing.clone();
375        } else if param1.is_byte_array() && is_int && length_field {
376            //         key_buffer: *const u8,
377            //         key_buffer_length: usize,
378            let processing = ArgProcessing::ByteArrayWithLength(vec![param1.clone(), param2.clone()]);
379            name_and_type[i - 1].processing = processing.clone();
380            name_and_type[i].processing = processing.clone();
381        }
382
383        //
384    }
385
386    name_and_type
387}
388
389// Helper function to extract doc comments
390fn get_doc_comments(attrs: &[Attribute]) -> BTreeSet<String> {
391    attrs
392        .iter()
393        .filter_map(|attr| {
394            // Parse the attribute meta to check if it is a `Meta::NameValue`
395            if let Meta::NameValue(MetaNameValue {
396                path,
397                value: syn::Expr::Lit(expr_lit),
398                ..
399            }) = &attr.meta
400            {
401                // Check if the path is "doc"
402                if path.is_ident("doc") {
403                    // Check if the literal is a string and return its value
404                    if let Lit::Str(lit_str) = &expr_lit.lit {
405                        return Some(lit_str.value().trim().to_string());
406                    }
407                }
408            }
409            None
410        })
411        .collect()
412}
413
414pub fn snake_to_pascal_case(mut snake: &str) -> String {
415    if snake.ends_with("_t") {
416        snake = &snake[..snake.len() - 2];
417    }
418    snake
419        .split('_')
420        .filter(|x| *x != "on") // Split the string by underscores
421        .map(|word| {
422            let mut chars = word.chars();
423            // Capitalize the first letter and collect the rest of the letters
424            match chars.next() {
425                Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
426                None => String::new(),
427            }
428        })
429        .collect()
430}
431
432// Helper function to extract function arguments as Rust code
433fn extract_function_arguments(inputs: &syn::punctuated::Punctuated<syn::FnArg, syn::token::Comma>) -> Vec<Arg> {
434    inputs
435        .iter()
436        .map(|arg| match arg {
437            syn::FnArg::Receiver(_) => "self".to_string(), // Handle self receiver
438            syn::FnArg::Typed(pat_type) => pat_type.to_token_stream().to_string(), // Convert the pattern and type to Rust code
439        })
440        .map(|arg| {
441            arg.splitn(2, ':')
442                .map(|s| s.trim().to_string())
443                .collect_tuple()
444                .unwrap()
445        })
446        .map(|(name, ty)| Arg {
447            name,
448            c_type: ty,
449            processing: ArgProcessing::Default,
450        })
451        .collect_vec()
452}
453
454// Helper function to extract return type as Rust code
455fn extract_return_type(output: &syn::ReturnType) -> String {
456    match output {
457        syn::ReturnType::Default => "()".to_string(), // No return type, equivalent to ()
458        syn::ReturnType::Type(_, ty) => ty.to_token_stream().to_string(), // Convert the type to Rust code
459    }
460}
461
462#[cfg(test)]
463mod tests {
464    use crate::parser::parse_bindings;
465    use crate::ArgProcessing;
466    use std::path::PathBuf;
467
468    fn running_under_valgrind() -> bool {
469        std::env::var_os("RUSTERON_VALGRIND").is_some()
470    }
471
472    #[test]
473    fn media_driver() {
474        if running_under_valgrind() {
475            return;
476        }
477
478        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
479            .join("bindings")
480            .join("media-driver.rs");
481        let bindings = parse_bindings(&path);
482        assert_eq!(
483            "AeronImageFragmentAssembler",
484            bindings
485                .wrappers
486                .get("aeron_image_fragment_assembler_t")
487                .unwrap()
488                .class_name
489        );
490    }
491    #[test]
492    fn client() {
493        if running_under_valgrind() {
494            return;
495        }
496
497        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
498            .join("bindings")
499            .join("client.rs");
500        let bindings = parse_bindings(&path);
501        assert_eq!(
502            "AeronImageFragmentAssembler",
503            bindings
504                .wrappers
505                .get("aeron_image_fragment_assembler_t")
506                .unwrap()
507                .class_name
508        );
509        assert!(bindings.handlers.len() > 1);
510    }
511
512    /// `(buffer, frame_length)` must merge into one slice; old heuristic missed `frame_length`.
513    #[test]
514    fn reserved_value_supplier_buffer_merges_with_frame_length() {
515        if running_under_valgrind() {
516            return;
517        }
518
519        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
520            .join("bindings")
521            .join("client.rs");
522        let bindings = parse_bindings(&path);
523        let handler = bindings
524            .handlers
525            .iter()
526            .find(|h| h.type_name == "aeron_reserved_value_supplier_t")
527            .expect("reserved value supplier handler missing");
528        let buffer = handler.args.iter().find(|a| a.name == "buffer").unwrap();
529        assert!(
530            matches!(buffer.processing, ArgProcessing::ByteArrayWithLength(_)),
531            "buffer must merge with frame_length into a slice, got {:?}",
532            buffer.processing
533        );
534    }
535
536    /// `*mut c_char` fill-buffers must merge so the wrapper takes `&mut [u8]`.
537    #[test]
538    fn mut_string_fill_buffer_merges() {
539        if running_under_valgrind() {
540            return;
541        }
542
543        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
544            .join("bindings")
545            .join("client.rs");
546        let bindings = parse_bindings(&path);
547        let reader = bindings.wrappers.get("aeron_counters_reader_t").unwrap();
548        let method = reader
549            .methods
550            .iter()
551            .find(|m| m.fn_name == "aeron_counters_reader_counter_label")
552            .expect("counter_label method missing");
553        let buffer = method.arguments.iter().find(|a| a.name == "buffer").unwrap();
554        assert!(
555            matches!(buffer.processing, ArgProcessing::StringWithLength(_)),
556            "mut string buffer must merge with its length, got {:?}",
557            buffer.processing
558        );
559    }
560}