rmp_macros/
lib.rs

1use derive_syn_parse::Parse;
2use proc_macro::TokenStream;
3use proc_macro2::TokenStream as TokenStream2;
4use quote::quote;
5use syn::{
6    Ident, Result, Token,
7    parse::{Parse, ParseStream},
8    parse2,
9};
10
11use std::env;
12use std::fmt;
13use std::fs;
14use std::path::Path;
15
16use rmp_utils::{RMPArg, RMPConfig, RMPFunction};
17
18mod keywords {
19    use syn::custom_keyword;
20
21    custom_keyword!(ty);
22    custom_keyword!(class);
23    custom_keyword!(name);
24    custom_keyword!(static_function);
25    custom_keyword!(constructor);
26    custom_keyword!(destructor);
27    custom_keyword!(method);
28    custom_keyword!(property);
29}
30
31#[derive(Clone, Debug, PartialEq, Eq)]
32enum RMPFFIType {
33    StaticFunction,
34    Constructor,
35    Destructor,
36    Method,
37    Property,
38}
39
40impl Parse for RMPFFIType {
41    fn parse(input: ParseStream) -> Result<Self> {
42        let lookahead = input.lookahead1();
43        if lookahead.peek(keywords::static_function) {
44            input.parse::<keywords::static_function>()?;
45            Ok(Self::StaticFunction)
46        } else if lookahead.peek(keywords::constructor) {
47            input.parse::<keywords::constructor>()?;
48            Ok(Self::Constructor)
49        } else if lookahead.peek(keywords::destructor) {
50            input.parse::<keywords::destructor>()?;
51            Ok(Self::Destructor)
52        } else if lookahead.peek(keywords::method) {
53            input.parse::<keywords::method>()?;
54            Ok(Self::Method)
55        } else if lookahead.peek(keywords::property) {
56            input.parse::<keywords::property>()?;
57            Ok(Self::Property)
58        } else {
59            Err(lookahead.error())
60        }
61    }
62}
63
64impl fmt::Display for RMPFFIType {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        match self {
67            RMPFFIType::StaticFunction => write!(f, "static_function"),
68            RMPFFIType::Constructor => write!(f, "constructor"),
69            RMPFFIType::Destructor => write!(f, "destructor"),
70            RMPFFIType::Method => write!(f, "method"),
71            RMPFFIType::Property => write!(f, "property"),
72        }
73    }
74}
75
76#[derive(Parse, Clone)]
77struct RMPFFIAttrArgs {
78    pub _ty_keyword: Option<keywords::ty>,
79    #[parse_if(_ty_keyword.is_some())]
80    pub _eq: Option<Token![=]>,
81    #[parse_if(_ty_keyword.is_some())]
82    pub ty: Option<RMPFFIType>,
83    #[parse_if(_ty_keyword.is_some())]
84    pub _comma: Option<Token![,]>,
85
86    pub _class_keyword: Option<keywords::class>,
87    #[parse_if(_class_keyword.is_some())]
88    pub _eq2: Option<Token![=]>,
89    #[parse_if(_class_keyword.is_some())]
90    pub class: Option<Ident>,
91    #[parse_if(_class_keyword.is_some())]
92    pub _comma2: Option<Token![,]>,
93
94    pub _name_keyword: Option<keywords::name>,
95    #[parse_if(_name_keyword.is_some())]
96    pub _eq3: Option<Token![=]>,
97    #[parse_if(_name_keyword.is_some())]
98    pub name: Option<Ident>,
99}
100
101fn update_or_append_function(functions: &mut Option<Vec<RMPFunction>>, function: RMPFunction) {
102    if let Some(funcs) = functions {
103        if let Some(idx) = funcs.iter().position(|f| f.name == function.name) {
104            funcs[idx] = function;
105        } else {
106            funcs.push(function);
107        }
108    } else {
109        *functions = Some(vec![function]);
110    }
111}
112
113fn process_rmp_ffi(attr: TokenStream2, item: TokenStream2) -> Result<TokenStream2> {
114    let args = parse2::<RMPFFIAttrArgs>(attr)?;
115
116    let func = parse2::<syn::ItemFn>(item.clone())?;
117    let func_name = func.sig.ident.to_string();
118    let func_args = func
119        .sig
120        .inputs
121        .iter()
122        .map(|arg| match arg {
123            syn::FnArg::Typed(syn::PatType { pat, ty, .. }) => (quote!( #pat ), quote!( #ty )),
124            _ => (quote!(), quote!()),
125        })
126        .map(|(name, ty)| RMPArg {
127            name: name.to_string(),
128            ty: ty.to_string(),
129        })
130        .collect::<Vec<RMPArg>>();
131
132    let return_type = func.sig.output;
133    let return_type = match return_type {
134        syn::ReturnType::Type(_, ty) => quote!(#ty).to_string(),
135        _ => "void".to_string(),
136    };
137
138    let class = args.class.unwrap().to_string();
139    let docs = func
140        .attrs
141        .iter()
142        .flat_map(|attr| {
143            if let syn::Meta::NameValue(meta) = &attr.meta {
144                if meta.path.is_ident("doc") {
145                    if let syn::Expr::Lit(syn::ExprLit {
146                        lit: syn::Lit::Str(lit),
147                        ..
148                    }) = &meta.value
149                    {
150                        return Some(lit.value().trim().to_string());
151                    }
152                }
153            }
154            None
155        })
156        .collect::<Vec<_>>();
157    let function = RMPFunction {
158        name: args.name.unwrap().to_string(),
159        rust_name: func_name,
160        args: func_args,
161        return_type,
162        docs,
163    };
164
165    if let Ok(out_dir) = env::var("CARGO_MANIFEST_DIR") {
166        let bindings_dir = Path::new(&out_dir).join("bindings");
167        fs::create_dir_all(&bindings_dir).expect("Failed to create bindings directory");
168        let yaml_file_path = bindings_dir.join(format!("{}.yaml", class));
169
170        let mut config = if yaml_file_path.exists() {
171            match fs::read_to_string(&yaml_file_path) {
172                Ok(contents) => match serde_yaml::from_str(&contents) {
173                    Ok(config) => config,
174                    Err(_) => {
175                        let _ = fs::remove_file(&yaml_file_path);
176                        RMPConfig {
177                            class,
178                            ..Default::default()
179                        }
180                    }
181                },
182                Err(_) => RMPConfig {
183                    class,
184                    ..Default::default()
185                },
186            }
187        } else {
188            RMPConfig {
189                class,
190                ..Default::default()
191            }
192        };
193        match args.ty {
194            Some(RMPFFIType::StaticFunction) => {
195                if let Some(idx) = config
196                    .static_functions
197                    .iter()
198                    .position(|f| f.name == function.name)
199                {
200                    config.static_functions[idx] = function;
201                } else {
202                    config.static_functions.push(function);
203                }
204            }
205            Some(RMPFFIType::Constructor) => {
206                update_or_append_function(&mut config.constructors, function);
207            }
208            Some(RMPFFIType::Destructor) => {
209                config.destructor = Some(function);
210            }
211            Some(RMPFFIType::Method) => {
212                update_or_append_function(&mut config.methods, function);
213            }
214            Some(RMPFFIType::Property) => {
215                update_or_append_function(&mut config.properties, function);
216            }
217            _ => panic!("Invalid FFI type"),
218        }
219        let yaml_output: String =
220            serde_yaml::to_string(&config).expect("Failed to serialize to YAML");
221        fs::write(&yaml_file_path, yaml_output).expect("Failed to write YAML file");
222    } else {
223        panic!("CARGO_WORKSPACE_DIR is not set");
224    }
225
226    Ok(item)
227}
228
229#[proc_macro_attribute]
230pub fn rmp_ffi(attr: TokenStream, item: TokenStream) -> TokenStream {
231    match process_rmp_ffi(attr.into(), item.into()) {
232        Ok(item) => item.into(),
233        Err(e) => e.to_compile_error().into(),
234    }
235}