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 inputs = &func.sig.inputs;
57    let output = match &func.sig.output {
58        syn::ReturnType::Default => quote! { () },
59        syn::ReturnType::Type(_, ty) => quote! { #ty },
60    };
61
62    // NOTE: only the first parameter is (de)serialized (see `input_type`
63    // below) — `#[yog_export]` fns take a single args struct by convention,
64    // so the call must use the wrapper's deserialized `args` binding, not
65    // the original parameter name (which isn't in scope in the wrapper).
66    let call_fn = if inputs.is_empty() {
67        quote! { #name() }
68    } else {
69        quote! { #name(args) }
70    };
71
72    let input_type = if inputs.is_empty() {
73        quote! { () }
74    } else {
75        match inputs.first().unwrap() {
76            syn::FnArg::Typed(pat_type) => { let ty = &pat_type.ty; quote! { #ty } }
77            _ => quote! { () },
78        }
79    };
80
81    let expanded = quote! {
82        #vis #sig #block
83
84        #[doc(hidden)]
85        #[no_mangle]
86        pub unsafe extern "C" fn #wrap_name(
87            input_ptr: *const u8, input_len: u32,
88            out_data: *mut *mut u8, out_len: *mut u32, out_cap: *mut u32,
89        ) {
90            let input_slice = std::slice::from_raw_parts(input_ptr, input_len as usize);
91            let args: #input_type = ::yog_api::rkyv::from_bytes::<_, ::yog_api::rkyv::rancor::Error>(input_slice)
92                .expect("yog: deser failed");
93            let result: #output = #call_fn;
94            let aligned = ::yog_api::rkyv::to_bytes::<::yog_api::rkyv::rancor::Error>(&result).unwrap_or_default();
95            let bytes: Vec<u8> = aligned.to_vec();
96            *out_data = bytes.as_ptr() as *mut u8;
97            *out_len = bytes.len() as u32;
98            *out_cap = bytes.capacity() as u32;
99            std::mem::forget(bytes);
100        }
101
102        // Runs when this mod's `cdylib` is loaded, before the runtime calls
103        // `yog_mod_register` — populates the auto-export registry so mod
104        // authors don't need to call `registry.interop().export(...)` by hand.
105        #[::yog_api::ctor::ctor(unsafe, crate_path = ::yog_api::ctor)]
106        #[doc(hidden)]
107        fn #ctor_name() {
108            ::yog_api::__yog_export_registry().lock().unwrap().push(
109                ::yog_api::YogExportEntry { name: #name_str, ptr: #wrap_name as usize }
110            );
111        }
112    };
113
114    TokenStream::from(expanded)
115}
116
117// ── import! macro ────────────────────────────────────────────────────────
118
119#[proc_macro]
120pub fn import(input: TokenStream) -> TokenStream {
121    let import_block = syn::parse_macro_input!(input as ImportBlock);
122    let mod_name = &import_block.mod_name;
123    let mod_ident = format_ident!("{}", mod_name.replace('-', "_"));
124    let mut items = proc_macro2::TokenStream::new();
125
126    for item in &import_block.items {
127        match item {
128            ImportItem::Func(func) => { items.extend(generate_import_fn(func, mod_name)); }
129            ImportItem::Struct(s) => {
130                let vis = &s.vis; let ident = &s.ident; let generics = &s.generics;
131                let fields = &s.fields; let attrs = &s.attrs;
132                items.extend(quote! {
133                    #[derive(::yog_api::rkyv::Archive, ::yog_api::rkyv::Serialize, ::yog_api::rkyv::Deserialize)]
134                    #(#attrs)* #vis struct #ident #generics #fields
135                });
136            }
137        }
138    }
139
140    let out = quote! { pub mod #mod_ident { #items } };
141    TokenStream::from(out)
142}
143
144fn generate_import_fn(func: &syn::ItemFn, mod_name: &str) -> proc_macro2::TokenStream {
145    let name = &func.sig.ident;
146    let name_str = name.to_string();
147    // Statics use SCREAMING_SNAKE_CASE per Rust convention; the bind fn stays
148    // snake_case since it's a function, not a static.
149    let slot_name = format_ident!("__YOG_SLOT_{}", name_str.to_uppercase());
150    let bind_name = format_ident!("__yog_bind_{}", name);
151    let fn_sig = &func.sig;
152    let vis = &func.vis;
153    let inputs = &fn_sig.inputs;
154    let output = &fn_sig.output;
155
156    let output_type = match output {
157        syn::ReturnType::Default => quote! { () },
158        syn::ReturnType::Type(_, ty) => quote! { #ty },
159    };
160
161    let arg_names: Vec<_> = inputs.iter().map(|arg| {
162        match arg { syn::FnArg::Typed(pat_type) => &pat_type.pat, _ => unreachable!() }
163    }).collect();
164
165    let serialize_block = if inputs.is_empty() {
166        quote! { let input_bytes = Vec::new(); }
167    } else {
168        quote! {
169            let aligned = ::yog_api::rkyv::to_bytes::<::yog_api::rkyv::rancor::Error>(&(#(#arg_names),*)).unwrap_or_default();
170            let input_bytes: Vec<u8> = aligned.to_vec();
171        }
172    };
173
174    let wrapper_ty = quote! {
175        unsafe extern "C" fn(input_ptr: *const u8, input_len: u32,
176            out_data: *mut *mut u8, out_len: *mut u32, out_cap: *mut u32)
177    };
178
179    quote! {
180        #[allow(non_snake_case)]
181        #vis fn #name(#inputs) #output {
182            #serialize_block
183            let f = #slot_name.get()
184                .expect(concat!("yog: import '", #mod_name, ":", #name_str, "' not bound"));
185            let mut out_data: *mut u8 = std::ptr::null_mut();
186            let mut out_len: u32 = 0; let mut out_cap: u32 = 0;
187            unsafe {
188                f(input_bytes.as_ptr(), input_bytes.len() as u32,
189                  &mut out_data, &mut out_len, &mut out_cap);
190                let output_slice = std::slice::from_raw_parts(out_data, out_len as usize);
191                let result: #output_type = ::yog_api::rkyv::from_bytes::<_, ::yog_api::rkyv::rancor::Error>(output_slice)
192                    .expect(concat!("yog: deser failed in import '", #mod_name, ":", #name_str, "'"));
193                let _ = Vec::from_raw_parts(out_data, out_len as usize, out_cap as usize);
194                result
195            }
196        }
197
198        static #slot_name: ::std::sync::OnceLock<#wrapper_ty> = ::std::sync::OnceLock::new();
199
200        #[doc(hidden)] #[no_mangle]
201        pub unsafe extern "C" fn #bind_name(ptr: *const ::std::os::raw::c_void) {
202            let f: #wrapper_ty = ::std::mem::transmute(ptr);
203            #slot_name.set(f).ok();
204        }
205    }
206}
207
208// ── Parse helpers ────────────────────────────────────────────────────────
209
210struct ImportBlock { mod_name: String, items: Vec<ImportItem> }
211enum ImportItem { Func(syn::ItemFn), Struct(syn::ItemStruct) }
212
213impl syn::parse::Parse for ImportBlock {
214    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
215        let from_kw: syn::Ident = input.parse()?;
216        if from_kw != "from" { return Err(syn::Error::new(from_kw.span(), "expected `from`")); }
217        let lit: syn::LitStr = input.parse()?;
218        let mod_name = lit.value();
219        let content; syn::braced!(content in input);
220        let mut items = Vec::new();
221        while !content.is_empty() {
222            if let Ok(f) = content.parse::<syn::ItemFn>() { items.push(ImportItem::Func(f)); let _ = content.parse::<syn::Token![;]>(); }
223            else if let Ok(s) = content.parse::<syn::ItemStruct>() { items.push(ImportItem::Struct(s)); let _ = content.parse::<syn::Token![;]>(); }
224            else { break; }
225        }
226        Ok(ImportBlock { mod_name, items })
227    }
228}