Skip to main content

yog_interop/
lib.rs

1//! Proc-macros for Yog inter-mod communication.
2//!
3//! `#[yog_export]` on fn: generates a C-ABI wrapper and a `ctor` constructor
4//! that registers it into `yog_api`'s auto-export registry when the mod's
5//! `cdylib` is loaded — `export_mod!` flushes that registry into
6//! `registry.interop().export(...)` automatically, no manual call needed.
7//!
8//! `import!` macro: generates Rust wrapper + binding slot.
9
10use proc_macro::TokenStream;
11use quote::{format_ident, quote};
12
13// ── #[yog_export] ─────────────────────────────────────────────────────────
14
15/// Marks a function for inter-mod export. Generates a C-ABI wrapper plus a
16/// `ctor` constructor that self-registers into `yog_api`'s auto-export
17/// registry — `export_mod!` picks it up automatically, no manual
18/// `registry.interop().export(...)` call needed.
19#[proc_macro_attribute]
20pub fn yog_export(_attr: TokenStream, item: TokenStream) -> TokenStream {
21    // Try parsing as a struct or enum first (auto rkyv derives)
22    if let Ok(s) = syn::parse2::<syn::ItemStruct>(proc_macro2::TokenStream::from(item.clone())) {
23        let vis = &s.vis;
24        let ident = &s.ident;
25        let generics = &s.generics;
26        let fields = &s.fields;
27        let attrs = &s.attrs;
28        return TokenStream::from(quote! {
29            #[derive(::yog_api::rkyv::Archive, ::yog_api::rkyv::Serialize, ::yog_api::rkyv::Deserialize)]
30            #(#attrs)*
31            #vis struct #ident #generics #fields
32        });
33    }
34    if let Ok(e) = syn::parse2::<syn::ItemEnum>(proc_macro2::TokenStream::from(item.clone())) {
35        let vis = &e.vis;
36        let ident = &e.ident;
37        let generics = &e.generics;
38        let variants = &e.variants;
39        let attrs = &e.attrs;
40        return TokenStream::from(quote! {
41            #[derive(::yog_api::rkyv::Archive, ::yog_api::rkyv::Serialize, ::yog_api::rkyv::Deserialize)]
42            #(#attrs)*
43            #vis enum #ident #generics { #variants }
44        });
45    }
46
47    let func = syn::parse_macro_input!(item as syn::ItemFn);
48    let name = &func.sig.ident;
49    let vis = &func.vis;
50    let sig = &func.sig;
51    let block = &func.block;
52    let name_str = name.to_string();
53    let wrap_name = format_ident!("__yog_wrap_{}", name);
54    let ctor_name = format_ident!("__yog_ctor_register_{}", name);
55
56    let output = match &func.sig.output {
57        syn::ReturnType::Default => quote! { () },
58        syn::ReturnType::Type(_, ty) => quote! { #ty },
59    };
60
61    // A leading `&mut Registry`-shaped parameter is never serialized across
62    // the interop boundary — every mod is handed the same global `YogApi`
63    // table, so the exporting mod reconstructs its own `Registry` from the
64    // API pointer it captured during its own `yog_mod_register`, instead of
65    // requiring callers to thread an `api_ptr` field through by hand.
66    fn is_registry_ty(ty: &syn::Type) -> bool {
67        let ty = match ty {
68            syn::Type::Reference(r) => &*r.elem,
69            other => other,
70        };
71        quote! { #ty }
72            .to_string()
73            .replace(' ', "")
74            .ends_with("Registry")
75    }
76
77    let mut data_params: Vec<&syn::PatType> = func
78        .sig
79        .inputs
80        .iter()
81        .filter_map(|arg| match arg {
82            syn::FnArg::Typed(pt) => Some(pt),
83            syn::FnArg::Receiver(_) => None,
84        })
85        .collect();
86
87    let has_registry_param = data_params
88        .first()
89        .map(|pt| is_registry_ty(&pt.ty))
90        .unwrap_or(false);
91    if has_registry_param {
92        data_params.remove(0);
93    }
94
95    let data_types: Vec<&syn::Type> = data_params.iter().map(|pt| &*pt.ty).collect();
96    let arg_idents: Vec<syn::Ident> = (0..data_types.len())
97        .map(|i| format_ident!("__yog_arg{}", i))
98        .collect();
99
100    let data_type = match data_types.len() {
101        0 => quote! { () },
102        1 => {
103            let t = data_types[0];
104            quote! { #t }
105        }
106        _ => quote! { (#(#data_types),*) },
107    };
108
109    let destructure = match arg_idents.len() {
110        0 => quote! {},
111        1 => {
112            let a = &arg_idents[0];
113            quote! { let #a = __yog_data; }
114        }
115        _ => quote! { let (#(#arg_idents),*) = __yog_data; },
116    };
117
118    let registry_setup = if has_registry_param {
119        quote! {
120            let mut __yog_registry = unsafe {
121                ::yog_api::Registry::from_raw(::yog_api::__current_api_ptr() as *const ::yog_api::YogApi)
122            };
123        }
124    } else {
125        quote! {}
126    };
127
128    let mut call_args: Vec<proc_macro2::TokenStream> = Vec::new();
129    if has_registry_param {
130        call_args.push(quote! { &mut __yog_registry });
131    }
132    call_args.extend(arg_idents.iter().map(|a| quote! { #a }));
133    let call_fn = quote! { #name(#(#call_args),*) };
134
135    let expanded = quote! {
136        #vis #sig #block
137
138        #[doc(hidden)]
139        #[unsafe(no_mangle)]
140        pub unsafe extern "C" fn #wrap_name(
141            input_ptr: *const u8, input_len: u32,
142            out_data: *mut *mut u8, out_len: *mut u32, out_cap: *mut u32,
143        ) {
144            let input_slice = unsafe { std::slice::from_raw_parts(input_ptr, input_len as usize) };
145            let __yog_data: #data_type = ::yog_api::rkyv::from_bytes::<_, ::yog_api::rkyv::rancor::Error>(input_slice)
146                .expect("yog: deser failed");
147            #destructure
148            #registry_setup
149            let result: #output = #call_fn;
150            let aligned = ::yog_api::rkyv::to_bytes::<::yog_api::rkyv::rancor::Error>(&result).unwrap_or_default();
151            let bytes: Vec<u8> = aligned.to_vec();
152            unsafe {
153                *out_data = bytes.as_ptr() as *mut u8;
154                *out_len = bytes.len() as u32;
155                *out_cap = bytes.capacity() as u32;
156            }
157            std::mem::forget(bytes);
158        }
159
160        // Runs when this mod's `cdylib` is loaded, before the runtime calls
161        // `yog_mod_register` — populates the auto-export registry so mod
162        // authors don't need to call `registry.interop().export(...)` by hand.
163        #[::yog_api::ctor::ctor(unsafe, crate_path = ::yog_api::ctor)]
164        #[doc(hidden)]
165        fn #ctor_name() {
166            ::yog_api::__yog_export_registry().lock().unwrap().push(
167                ::yog_api::YogExportEntry { name: #name_str, ptr: #wrap_name as usize }
168            );
169        }
170    };
171
172    TokenStream::from(expanded)
173}
174
175// ── #[bench] ─────────────────────────────────────────────────────────────
176
177/// Marks a function in a mod's `benches/*.rs` as a benchmark `yog bench`
178/// runs: `fn name(b: &mut criterion::Bencher)` for one benchmark, or
179/// `fn name(c: &mut criterion::Criterion)` to set up groups. yog generates
180/// the file's `main`; this only makes the function reachable from it.
181#[proc_macro_attribute]
182pub fn bench(_attr: TokenStream, item: TokenStream) -> TokenStream {
183    let mut f = syn::parse_macro_input!(item as syn::ItemFn);
184    if matches!(f.vis, syn::Visibility::Inherited) {
185        f.vis = syn::parse_quote!(pub(crate));
186    }
187    TokenStream::from(quote! { #f })
188}
189
190// ── import! macro ────────────────────────────────────────────────────────
191
192#[proc_macro]
193pub fn import(input: TokenStream) -> TokenStream {
194    let import_block = syn::parse_macro_input!(input as ImportBlock);
195    let mod_name = &import_block.mod_name;
196    let mod_ident = format_ident!("{}", mod_name.replace('-', "_"));
197    let mut items = proc_macro2::TokenStream::new();
198
199    for item in &import_block.items {
200        match item {
201            ImportItem::Func(func) => {
202                items.extend(generate_import_fn(func, mod_name));
203            }
204            ImportItem::Struct(s) => {
205                let vis = &s.vis;
206                let ident = &s.ident;
207                let generics = &s.generics;
208                let fields = &s.fields;
209                let attrs = &s.attrs;
210                items.extend(quote! {
211                    #[derive(::yog_api::rkyv::Archive, ::yog_api::rkyv::Serialize, ::yog_api::rkyv::Deserialize)]
212                    #(#attrs)* #vis struct #ident #generics #fields
213                });
214            }
215        }
216    }
217
218    let out = quote! { pub mod #mod_ident { #items } };
219    TokenStream::from(out)
220}
221
222fn generate_import_fn(func: &syn::ItemFn, mod_name: &str) -> proc_macro2::TokenStream {
223    let name = &func.sig.ident;
224    let name_str = name.to_string();
225    // Statics use SCREAMING_SNAKE_CASE per Rust convention; the bind fn stays
226    // snake_case since it's a function, not a static.
227    let slot_name = format_ident!("__YOG_SLOT_{}", name_str.to_uppercase());
228    let bind_name = format_ident!("__yog_bind_{}", name);
229    let ctor_name = format_ident!("__yog_import_ctor_{}", name);
230    let fn_sig = &func.sig;
231    let vis = &func.vis;
232    let inputs = &fn_sig.inputs;
233    let output = &fn_sig.output;
234
235    let output_type = match output {
236        syn::ReturnType::Default => quote! { () },
237        syn::ReturnType::Type(_, ty) => quote! { #ty },
238    };
239
240    let arg_names: Vec<_> = inputs
241        .iter()
242        .map(|arg| match arg {
243            syn::FnArg::Typed(pat_type) => &pat_type.pat,
244            _ => unreachable!(),
245        })
246        .collect();
247
248    let serialize_block = if inputs.is_empty() {
249        quote! { let input_bytes = Vec::new(); }
250    } else {
251        quote! {
252            let aligned = ::yog_api::rkyv::to_bytes::<::yog_api::rkyv::rancor::Error>(&(#(#arg_names),*)).unwrap_or_default();
253            let input_bytes: Vec<u8> = aligned.to_vec();
254        }
255    };
256
257    let wrapper_ty = quote! {
258        unsafe extern "C" fn(input_ptr: *const u8, input_len: u32,
259            out_data: *mut *mut u8, out_len: *mut u32, out_cap: *mut u32)
260    };
261
262    quote! {
263        #[allow(non_snake_case)]
264        #vis fn #name(#inputs) #output {
265            #serialize_block
266            let f = #slot_name.get()
267                .expect(concat!("yog: import '", #mod_name, ":", #name_str, "' not bound"));
268            let mut out_data: *mut u8 = std::ptr::null_mut();
269            let mut out_len: u32 = 0; let mut out_cap: u32 = 0;
270            unsafe {
271                f(input_bytes.as_ptr(), input_bytes.len() as u32,
272                  &mut out_data, &mut out_len, &mut out_cap);
273                let output_slice = std::slice::from_raw_parts(out_data, out_len as usize);
274                let result: #output_type = ::yog_api::rkyv::from_bytes::<_, ::yog_api::rkyv::rancor::Error>(output_slice)
275                    .expect(concat!("yog: deser failed in import '", #mod_name, ":", #name_str, "'"));
276                let _ = Vec::from_raw_parts(out_data, out_len as usize, out_cap as usize);
277                result
278            }
279        }
280
281        static #slot_name: ::std::sync::OnceLock<#wrapper_ty> = ::std::sync::OnceLock::new();
282
283        #[doc(hidden)] #[unsafe(no_mangle)]
284        pub unsafe extern "C" fn #bind_name(ptr: *const ::std::os::raw::c_void) {
285            let f: #wrapper_ty = unsafe { ::std::mem::transmute(ptr) };
286            #slot_name.set(f).ok();
287        }
288
289        // Queues the binding when this mod's library loads; `export_mod!`
290        // binds it once the exporting mod has exported the function.
291        #[::yog_api::ctor::ctor(unsafe, crate_path = ::yog_api::ctor)]
292        #[doc(hidden)]
293        fn #ctor_name() {
294            ::yog_api::__yog_import_registry().lock().unwrap().push(::yog_api::YogImportEntry {
295                mod_id: #mod_name,
296                symbol: #name_str,
297                bind_fn: #bind_name as *const () as usize,
298            });
299        }
300    }
301}
302
303// ── Parse helpers ────────────────────────────────────────────────────────
304
305struct ImportBlock {
306    mod_name: String,
307    items: Vec<ImportItem>,
308}
309enum ImportItem {
310    Func(syn::ItemFn),
311    Struct(syn::ItemStruct),
312}
313
314impl syn::parse::Parse for ImportBlock {
315    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
316        let from_kw: syn::Ident = input.parse()?;
317        if from_kw != "from" {
318            return Err(syn::Error::new(from_kw.span(), "expected `from`"));
319        }
320        let lit: syn::LitStr = input.parse()?;
321        let mod_name = lit.value();
322        let content;
323        syn::braced!(content in input);
324        let mut items = Vec::new();
325        while !content.is_empty() {
326            if let Ok(f) = content.parse::<syn::ItemFn>() {
327                items.push(ImportItem::Func(f));
328                let _ = content.parse::<syn::Token![;]>();
329            } else if let Ok(s) = content.parse::<syn::ItemStruct>() {
330                items.push(ImportItem::Struct(s));
331                let _ = content.parse::<syn::Token![;]>();
332            } else {
333                break;
334            }
335        }
336        Ok(ImportBlock { mod_name, items })
337    }
338}