Skip to main content

yog_interop/
lib.rs

1//! Yog inter-mod communication — proc-macros for cross-mod function calls.
2//!
3//! # Export (manual registration, auto-generated binding symbol)
4//!
5//! ```ignore
6//! use yog_api::{yog_export, Registry};
7//!
8//! #[yog_export]
9//! pub fn register_pipe(api: *const yog_api::YogApi, json: *const std::ffi::c_char) {
10//!     // ...
11//! }
12//!
13//! fn register(registry: &mut Registry) {
14//!     // Manual export registration (loader scanning coming later)
15//!     registry.interop().export("register_pipe", register_pipe as *const std::ffi::c_void);
16//! }
17//! ```
18//!
19//! # Import (auto-wired by loader)
20//!
21//! ```ignore
22//! yog_api::import! {
23//!     from "yog-pipes" {
24//!         fn register_pipe(api: *const yog_api::YogApi, json: *const std::ffi::c_char);
25//!     }
26//! }
27//!
28//! // Use like a normal function — loader wires it at init time
29//! register_pipe(api, json_ptr);
30//! ```
31
32use proc_macro::TokenStream;
33use quote::{format_ident, quote};
34
35// ── #[yog_export] ─────────────────────────────────────────────────────────
36
37/// Universal export macro — works on both structs and functions.
38///
39/// **On structs**: auto-derives `rkyv::Archive`, `rkyv::Serialize`,
40/// `rkyv::Deserialize`. Equivalent to:
41/// ```ignore
42/// #[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
43/// #[archive(check_bytes)]
44/// struct MyStruct { ... }
45/// ```
46///
47/// **On functions**: generates a C-ABI wrapper using rkyv zero-copy
48/// serialization + auto-registers the export via `export_mod!`.
49///
50/// Supported fn signatures:
51/// - `fn(InputType) -> OutputType`  (single arg)
52/// - `fn() -> OutputType`          (no arg)
53/// - `fn(InputType)`               (no return)
54/// For multi-arg: `fn((A, B)) -> C`.
55///
56/// No manual `registry.interop().export()` needed.
57#[proc_macro_attribute]
58pub fn yog_export(_attr: TokenStream, item: TokenStream) -> TokenStream {
59    // Try parsing as a struct first (more common for types)
60    if let Ok(s) = syn::parse2::<syn::ItemStruct>(proc_macro2::TokenStream::from(item.clone())) {
61        let vis = &s.vis;
62        let ident = &s.ident;
63        let generics = &s.generics;
64        let fields = &s.fields;
65        let attrs = &s.attrs;
66        return TokenStream::from(quote! {
67            #[derive(::yog_api::rkyv::Archive, ::yog_api::rkyv::Serialize, ::yog_api::rkyv::Deserialize)]
68            #[archive(check_bytes)]
69            #[archive_attr(derive(::yog_api::rkyv::bytecheck::CheckBytes))]
70            #(#attrs)*
71            #vis struct #ident #generics #fields
72        });
73    }
74
75    // Fall through to function handling
76    let func = syn::parse_macro_input!(item as syn::ItemFn);
77    let name = &func.sig.ident;
78    let vis = &func.vis;
79    let sig = &func.sig;
80    let block = &func.block;
81    let name_str = name.to_string();
82    let wrap_name = format_ident!("__yog_wrap_{}", name);
83    let init_name = format_ident!("__yog_export_init_{}", name);
84
85    // Extract input and output types
86    let inputs = &func.sig.inputs;
87    let output = match &func.sig.output {
88        syn::ReturnType::Default => quote! { () },
89        syn::ReturnType::Type(_, ty) => quote! { #ty },
90    };
91
92    // Build the wrapper body — handles both arg and no-arg cases
93    let call_fn = if inputs.is_empty() {
94        quote! { #name() }
95    } else {
96        // Multi-arg: serialize as tuple, then deserialize and destructure
97        let arg_names: Vec<_> = inputs.iter().map(|arg| {
98            match arg {
99                syn::FnArg::Typed(pat_type) => &pat_type.pat,
100                syn::FnArg::Receiver(_) => unreachable!(),
101            }
102        }).collect();
103        quote! { #name(#(#arg_names),*) }
104    };
105
106    // Extract the single input type (or unit for no-arg fns)
107    let input_type = if inputs.is_empty() {
108        quote! { () }
109    } else {
110        // Take the first (and should be only) argument type
111        match inputs.first().unwrap() {
112            syn::FnArg::Typed(pat_type) => {
113                let ty = &pat_type.ty;
114                quote! { #ty }
115            }
116            _ => quote! { () },
117        }
118    };
119
120    let expanded = quote! {
121        #vis #sig #block
122
123        // C-ABI wrapper — deserializes input via rkyv, calls fn, serializes output.
124        #[doc(hidden)]
125        #[no_mangle]
126        pub unsafe extern "C" fn #wrap_name(
127            input_ptr: *const u8,
128            input_len: u32,
129            out_data: *mut *mut u8,
130            out_len: *mut u32,
131            out_cap: *mut u32,
132        ) {
133            let input_slice = std::slice::from_raw_parts(input_ptr, input_len as usize);
134            let args: #input_type = ::yog_api::rkyv::from_bytes::<_, ::yog_api::rkyv::rancor::Error>(input_slice)
135                .expect("yog: deserialization failed in export wrapper");
136            let result: #output = #call_fn;
137            let bytes: Vec<u8> = ::yog_api::rkyv::to_bytes::<_, 256>(&result)
138                .unwrap_or_default();
139            *out_data = bytes.as_ptr() as *mut u8;
140            *out_len = bytes.len() as u32;
141            *out_cap = bytes.capacity() as u32;
142            std::mem::forget(bytes); // caller frees via __yog_free_buffer
143        }
144
145        // Static initializer — auto-registers the WRAPPER (not the original).
146        #[doc(hidden)]
147        #[allow(non_upper_case_globals)]
148        static #init_name: u8 = {
149            ::yog_api::__yog_export_registry()
150                .lock()
151                .unwrap()
152                .push((#name_str, #wrap_name as usize));
153            0
154        };
155    };
156
157    TokenStream::from(expanded)
158}
159
160// ── import! macro ────────────────────────────────────────────────────────
161
162/// Declare imported functions from another mod. Generates public wrapper
163/// functions and `__yog_bind_NAME` entry points for the loader.
164///
165/// ```ignore
166/// yog_api::import! {
167///     from "yog-pipes" {
168///         fn register_pipe(api: *const YogApi, json: *const c_char) -> bool;
169///     }
170/// }
171/// register_pipe(api, json_ptr); // auto-wired
172/// ```
173#[proc_macro]
174pub fn import(input: TokenStream) -> TokenStream {
175    let import_block = syn::parse_macro_input!(input as ImportBlock);
176    let mod_name = &import_block.mod_name;
177    let mut out = proc_macro2::TokenStream::new();
178
179    for func in &import_block.funcs {
180        let name = &func.sig.ident;
181        let name_str = name.to_string();
182        let slot_name = format_ident!("__yog_slot_{}", name);
183        let bind_name = format_ident!("__yog_bind_{}", name);
184        let fn_sig = &func.sig;
185        let vis = &func.vis;
186        let inputs = &fn_sig.inputs;
187        let output = &fn_sig.output;
188
189        // Extract input type (single arg or unit)
190        let _input_type = if inputs.is_empty() {
191            quote! { () }
192        } else {
193            match inputs.first().unwrap() {
194                syn::FnArg::Typed(pat_type) => {
195                    let ty = &pat_type.ty;
196                    quote! { #ty }
197                }
198                _ => quote! { () },
199            }
200        };
201
202        let output_type = match output {
203            syn::ReturnType::Default => quote! { () },
204            syn::ReturnType::Type(_, ty) => quote! { #ty },
205        };
206
207        // Build arg forwarding for the public function
208        let arg_names: Vec<_> = inputs.iter().map(|arg| {
209            match arg {
210                syn::FnArg::Typed(pat_type) => &pat_type.pat,
211                syn::FnArg::Receiver(_) => unreachable!("self not allowed in imports"),
212            }
213        }).collect();
214
215        // Serialize the arg (or nothing for no-arg fns)
216        let serialize_block = if inputs.is_empty() {
217            quote! { let input_bytes = Vec::new(); }
218        } else {
219            quote! {
220                let input_bytes = ::yog_api::rkyv::to_bytes::<_, 256>(&(#(#arg_names),*))
221                    .unwrap_or_default();
222            }
223        };
224
225        // C-ABI wrapper type — shared by all exports
226        let wrapper_ty = quote! {
227            unsafe extern "C" fn(
228                input_ptr: *const u8, input_len: u32,
229                out_data: *mut *mut u8, out_len: *mut u32, out_cap: *mut u32,
230            )
231        };
232
233        let wrapper = quote! {
234            #[allow(non_snake_case)]
235            #vis fn #name(#inputs) #output {
236                #serialize_block
237                let f = #slot_name.get()
238                    .expect(concat!("yog: import '", #mod_name, ":", #name_str, "' not bound — is mod '", #mod_name, "' loaded?"));
239                let mut out_data: *mut u8 = std::ptr::null_mut();
240                let mut out_len: u32 = 0;
241                let mut out_cap: u32 = 0;
242                unsafe {
243                    f(
244                        input_bytes.as_ptr(), input_bytes.len() as u32,
245                        &mut out_data, &mut out_len, &mut out_cap,
246                    );
247                    let output_slice = std::slice::from_raw_parts(out_data, out_len as usize);
248                    let result: #output_type = ::yog_api::rkyv::from_bytes::<_, ::yog_api::rkyv::rancor::Error>(output_slice)
249                        .expect(concat!("yog: deserialization failed in import '", #mod_name, ":", #name_str, "'"));
250                    // Free the buffer allocated by the export wrapper
251                    let _ = Vec::from_raw_parts(out_data, out_len as usize, out_cap as usize);
252                    result
253                }
254            }
255
256            static #slot_name: ::std::sync::OnceLock<#wrapper_ty>
257                = ::std::sync::OnceLock::new();
258
259            #[doc(hidden)]
260            #[no_mangle]
261            pub unsafe extern "C" fn #bind_name(ptr: *const ::std::os::raw::c_void) {
262                let f: #wrapper_ty = ::std::mem::transmute(ptr);
263                #slot_name.set(f).ok();
264            }
265        };
266
267        out.extend(wrapper);
268    }
269
270    TokenStream::from(out)
271}
272
273// ── Parse helpers ────────────────────────────────────────────────────────
274
275struct ImportBlock {
276    mod_name: String,
277    funcs: Vec<syn::ItemFn>,
278}
279
280impl syn::parse::Parse for ImportBlock {
281    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
282        let from_kw: syn::Ident = input.parse()?;
283        if from_kw != "from" {
284            return Err(syn::Error::new(from_kw.span(), "expected `from`"));
285        }
286        let lit: syn::LitStr = input.parse()?;
287        let mod_name = lit.value();
288
289        let content;
290        syn::braced!(content in input);
291        let mut funcs = Vec::new();
292        while !content.is_empty() {
293            funcs.push(content.parse::<syn::ItemFn>()?);
294            let _ = content.parse::<syn::Token![;]>();
295        }
296
297        Ok(ImportBlock { mod_name, funcs })
298    }
299}