byondapi_macros/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::Ident;
3use quote::quote;
4use syn::{spanned::Spanned, Lit};
5
6fn extract_args(a: &syn::FnArg) -> &syn::PatType {
7    match a {
8        syn::FnArg::Typed(p) => p,
9        _ => panic!("Not supported on types with `self`!"),
10    }
11}
12//this is an example, mr clippy
13#[allow(clippy::test_attr_in_doctest)]
14/// Macro for generating byond binds
15/// Usage:
16/// ```ignore
17/// use byondapi::prelude::*;
18/// #[byondapi::bind]
19/// fn example() {Ok(ByondValue::null())}
20///
21/// #[byondapi::bind("/datum/example/proc/other_example")]
22/// fn example_other(_: ByondValue, _: ByondValue) {Ok(ByondValue::null())}
23///
24/// ```
25/// Then generate the bindings.dm file with
26/// ```ignore
27/// #[test]
28/// fn generate_binds() {
29///     byondapi::byondapi_macros::generate_bindings(env!("CARGO_CRATE_NAME"));
30/// }
31/// ```
32/// and run cargo test to actually create the file
33///
34#[proc_macro_attribute]
35pub fn bind(attr: TokenStream, item: TokenStream) -> TokenStream {
36    let input = syn::parse_macro_input!(item as syn::ItemFn);
37    let proc = syn::parse_macro_input!(attr as Option<syn::Lit>);
38
39    let func_name = &input.sig.ident;
40    let func_name_disp = quote!(#func_name).to_string();
41
42    let func_name_ffi = format!("{func_name_disp}_ffi");
43    let func_name_ffi = Ident::new(&func_name_ffi, func_name.span());
44    let func_name_ffi_disp = quote!(#func_name_ffi).to_string();
45
46    let args = &input.sig.inputs;
47
48    let all_docs = input
49        .attrs
50        .iter()
51        .filter(|attr| matches!(attr.style, syn::AttrStyle::Outer))
52        .filter_map(|attr| match &attr.meta {
53            syn::Meta::NameValue(nameval) => {
54                let ident = nameval.path.get_ident()?;
55                if ident.to_string() == "doc".to_string() {
56                    match &nameval.value {
57                        syn::Expr::Lit(literal) => match &literal.lit {
58                            syn::Lit::Str(docstring) => {
59                                Some(format!("///{}\n", docstring.value(),))
60                            }
61                            _ => None,
62                        },
63                        _ => None,
64                    }
65                } else {
66                    None
67                }
68            }
69            _ => None,
70        })
71        .collect::<String>();
72
73    //Check for returns
74    let func_return = match &input.sig.output {
75        syn::ReturnType::Default => {
76            return syn::Error::new(
77                input.span(),
78                "Empty returns are not allowed, please return a Result",
79            )
80            .to_compile_error()
81            .into()
82        }
83
84        syn::ReturnType::Type(_, ty) => match ty.as_ref() {
85            &syn::Type::Path(_) => &input.sig.output,
86            _ => {
87                return syn::Error::new(input.span(), "Invalid return type, please return a Result")
88                    .to_compile_error()
89                    .into()
90            }
91        },
92    };
93
94    let signature = quote! {
95        #[no_mangle]
96        pub unsafe extern "C-unwind" fn #func_name_ffi (
97            __argc: ::byondapi::sys::u4c,
98            __argv: *mut ::byondapi::value::ByondValue
99        ) -> ::byondapi::value::ByondValue
100    };
101
102    let body = &input.block;
103    let mut arg_names: syn::punctuated::Punctuated<syn::Ident, syn::Token![,]> =
104        syn::punctuated::Punctuated::new();
105    let mut proc_arg_unpacker: syn::punctuated::Punctuated<
106        proc_macro2::TokenStream,
107        syn::Token![,],
108    > = syn::punctuated::Punctuated::new();
109
110    for arg in args.iter().map(extract_args) {
111        if let syn::Pat::Ident(p) = &*arg.pat {
112            arg_names.push(p.ident.clone());
113            let index = arg_names.len() - 1;
114            proc_arg_unpacker.push(quote! {
115                args.get(#index).map(::byondapi::value::ByondValue::clone).unwrap_or_default()
116            });
117        }
118    }
119
120    let arg_names_disp = quote!(#arg_names).to_string();
121
122    //Submit to inventory
123    let cthook_prelude = match &proc {
124        Some(Lit::Str(p)) => {
125            quote! {
126                ::byondapi::inventory::submit!({
127                    ::byondapi::binds::Bind {
128                        proc_path: #p,
129                        func_name: #func_name_ffi_disp,
130                        func_arguments: #arg_names_disp,
131                        docs: #all_docs,
132                        is_variadic: false,
133                    }
134                });
135            }
136        }
137        Some(other_literal) => {
138            return syn::Error::new(
139                other_literal.span(),
140                "Bind attributes must be a string literal or empty",
141            )
142            .to_compile_error()
143            .into()
144        }
145        None => {
146            let mut func_name_disp = func_name_disp.clone();
147            func_name_disp.insert_str(0, "/proc/");
148            quote! {
149                ::byondapi::inventory::submit!({
150                    ::byondapi::binds::Bind{
151                        proc_path: #func_name_disp,
152                        func_name: #func_name_ffi_disp,
153                        func_arguments: #arg_names_disp,
154                        docs: #all_docs,
155                        is_variadic: false,
156                    }
157                });
158            }
159        }
160    };
161
162    let crash_syntax = if cfg!(feature = "old-crash-workaround") {
163        quote! {
164            let error_string = ::byondapi::value::ByondValue::try_from(error_string).unwrap();
165            ::byondapi::global_call::call_global_id({
166                static STACK_TRACE: ::std::sync::OnceLock<u32> = ::std::sync::OnceLock::new();
167                *STACK_TRACE.get_or_init(|| ::byondapi::byond_string::str_id_of("byondapi_stack_trace")
168                    .expect("byondapi-rs implicitly expects byondapi_stack_trace to exist as a proc for error reporting purposes, this proc doesn't exist!")
169                )
170            }
171            ,&[error_string]).unwrap();
172        }
173    } else {
174        quote! {
175            ::byondapi::runtime::byond_runtime(error_string);
176        }
177    };
178
179    let result = quote! {
180        #cthook_prelude
181        #signature {
182            let args = unsafe { ::byondapi::parse_args(__argc, __argv) };
183            match #func_name(#proc_arg_unpacker) {
184                Ok(val) => val,
185                Err(e) => {
186                    let error_string = ::std::format!("{e:?}");
187                    #crash_syntax
188                    ::byondapi::value::ByondValue::null()
189                }
190            }
191
192        }
193        fn #func_name(#args) #func_return
194        #body
195    };
196    result.into()
197}
198
199/// Same as [`bind`] but accepts variable amount of args, with src in the beginning if there's a src
200/// The args are just a variable named `args` in the macro'd function
201#[proc_macro_attribute]
202pub fn bind_raw_args(attr: TokenStream, item: TokenStream) -> TokenStream {
203    let input = syn::parse_macro_input!(item as syn::ItemFn);
204    let proc = syn::parse_macro_input!(attr as Option<syn::Lit>);
205
206    let func_name = &input.sig.ident;
207    let func_name_disp = quote!(#func_name).to_string();
208
209    let func_name_ffi = format!("{func_name_disp}_ffi");
210    let func_name_ffi = Ident::new(&func_name_ffi, func_name.span());
211    let func_name_ffi_disp = quote!(#func_name_ffi).to_string();
212
213    let all_docs = input
214        .attrs
215        .iter()
216        .filter(|attr| matches!(attr.style, syn::AttrStyle::Outer))
217        .filter_map(|attr| match &attr.meta {
218            syn::Meta::NameValue(nameval) => {
219                let ident = nameval.path.get_ident()?;
220                if ident.to_string() == "doc".to_string() {
221                    match &nameval.value {
222                        syn::Expr::Lit(literal) => match &literal.lit {
223                            syn::Lit::Str(docstring) => {
224                                Some(format!("///{}\n", docstring.value(),))
225                            }
226                            _ => None,
227                        },
228                        _ => None,
229                    }
230                } else {
231                    None
232                }
233            }
234            _ => None,
235        })
236        .collect::<String>();
237
238    //Check for returns
239    let func_return = match &input.sig.output {
240        syn::ReturnType::Default => {
241            return syn::Error::new(
242                input.span(),
243                "Empty returns are not allowed, please return a Result",
244            )
245            .to_compile_error()
246            .into()
247        }
248
249        syn::ReturnType::Type(_, ty) => match ty.as_ref() {
250            &syn::Type::Path(_) => &input.sig.output,
251            _ => {
252                return syn::Error::new(input.span(), "Invalid return type, please return a Result")
253                    .to_compile_error()
254                    .into()
255            }
256        },
257    };
258
259    if !input.sig.inputs.is_empty() {
260        return syn::Error::new(
261            input.sig.inputs.span(),
262            "Do not specify arguments for raw arg binds",
263        )
264        .to_compile_error()
265        .into();
266    }
267
268    let signature = quote! {
269        #[no_mangle]
270        pub unsafe extern "C-unwind" fn #func_name_ffi (
271            __argc: ::byondapi::sys::u4c,
272            __argv: *mut ::byondapi::value::ByondValue
273        ) -> ::byondapi::value::ByondValue
274    };
275
276    let body = &input.block;
277
278    //Submit to inventory
279    let cthook_prelude = match proc {
280        Some(Lit::Str(p)) => {
281            quote! {
282                ::byondapi::inventory::submit!({
283                    ::byondapi::binds::Bind {
284                        proc_path: #p,
285                        func_name: #func_name_ffi_disp,
286                        func_arguments: "",
287                        docs: #all_docs,
288                        is_variadic: true,
289                    }
290                });
291            }
292        }
293        Some(other_literal) => {
294            return syn::Error::new(
295                other_literal.span(),
296                "Bind attributes must be a string literal or empty",
297            )
298            .to_compile_error()
299            .into()
300        }
301        None => {
302            let mut func_name_disp = func_name_disp.clone();
303            func_name_disp.insert_str(0, "/proc/");
304            quote! {
305                    ::byondapi::inventory::submit!({
306                        ::byondapi::binds::Bind{
307                            proc_path: #func_name_disp,
308                            func_name: #func_name_ffi_disp,
309                            func_arguments: "",
310                            docs: #all_docs,
311                            is_variadic: true,
312                        }
313                    });
314            }
315        }
316    };
317    let crash_syntax = if cfg!(feature = "old-crash-workaround") {
318        quote! {
319            let error_string = ::byondapi::value::ByondValue::try_from(error_string).unwrap();
320            ::byondapi::global_call::call_global_id({
321                static STACK_TRACE: ::std::sync::OnceLock<u32> = ::std::sync::OnceLock::new();
322                *STACK_TRACE.get_or_init(|| ::byondapi::byond_string::str_id_of("byondapi_stack_trace")
323                    .expect("byondapi-rs implicitly expects byondapi_stack_trace to exist as a proc for error reporting purposes, this proc doesn't exist!")
324                )
325            }
326            ,&[error_string]).unwrap();
327        }
328    } else {
329        quote! {
330            ::byondapi::runtime::byond_runtime(error_string);
331        }
332    };
333
334    let result = quote! {
335        #cthook_prelude
336        #signature {
337            let mut args = unsafe { ::byondapi::parse_args(__argc, __argv) };
338            match #func_name(args) {
339                Ok(val) => val,
340                Err(e) => {
341                    let error_string = ::std::format!("{e:?}");
342                    #crash_syntax
343                    ::byondapi::value::ByondValue::null()
344                }
345            }
346        }
347        fn #func_name(args: &mut [::byondapi::value::ByondValue]) #func_return
348        #body
349    };
350    result.into()
351}
352
353#[proc_macro_attribute]
354pub fn init(_: TokenStream, item: TokenStream) -> TokenStream {
355    let input = syn::parse_macro_input!(item as syn::ItemFn);
356    let func_name = &input.sig.ident;
357    quote! {
358        #input
359        ::byondapi::inventory::submit!({::byondapi::InitFunc(#func_name)});
360    }
361    .into()
362}