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/// Marks a function for inter-mod export.
38///
39/// Generates:
40/// 1. A static initializer that auto-registers the export (no manual
41///    `registry.interop().export()` needed — `export_mod!` handles it).
42/// 2. A `__yog_export_get_NAME` symbol for future loader-side scanning.
43#[proc_macro_attribute]
44pub fn yog_export(_attr: TokenStream, item: TokenStream) -> TokenStream {
45    let func = syn::parse_macro_input!(item as syn::ItemFn);
46    let name = &func.sig.ident;
47    let vis = &func.vis;
48    let sig = &func.sig;
49    let block = &func.block;
50    let name_str = name.to_string();
51    let getter_name = format_ident!("__yog_export_get_{}", name);
52    let init_name = format_ident!("__yog_export_init_{}", name);
53
54    let expanded = quote! {
55        #vis #sig #block
56
57        // Static initializer — runs before main(), pushes to export registry.
58        #[doc(hidden)]
59        #[allow(non_upper_case_globals)]
60        static #init_name: u8 = {
61            ::yog_api::__yog_export_registry()
62                .lock()
63                .unwrap()
64                .push((#name_str, #name as usize));
65            0
66        };
67
68        // Getter symbol for future loader-side scanning.
69        #[doc(hidden)]
70        #[no_mangle]
71        pub unsafe extern "C" fn #getter_name(
72            out_name: *mut *const ::std::os::raw::c_char,
73            out_ptr: *mut *const ::std::os::raw::c_void,
74        ) {
75            if !out_name.is_null() {
76                *out_name = concat!(#name_str, "\0").as_ptr() as *const ::std::os::raw::c_char;
77            }
78            if !out_ptr.is_null() {
79                *out_ptr = #name as *const ::std::os::raw::c_void;
80            }
81        }
82    };
83
84    TokenStream::from(expanded)
85}
86
87// ── import! macro ────────────────────────────────────────────────────────
88
89/// Declare imported functions from another mod. Generates public wrapper
90/// functions and `__yog_bind_NAME` entry points for the loader.
91///
92/// ```ignore
93/// yog_api::import! {
94///     from "yog-pipes" {
95///         fn register_pipe(api: *const YogApi, json: *const c_char) -> bool;
96///     }
97/// }
98/// register_pipe(api, json_ptr); // auto-wired
99/// ```
100#[proc_macro]
101pub fn import(input: TokenStream) -> TokenStream {
102    let import_block = syn::parse_macro_input!(input as ImportBlock);
103    let mod_name = &import_block.mod_name;
104    let mut out = proc_macro2::TokenStream::new();
105
106    for func in &import_block.funcs {
107        let name = &func.sig.ident;
108        let name_str = name.to_string();
109        let slot_name = format_ident!("__yog_slot_{}", name);
110        let bind_name = format_ident!("__yog_bind_{}", name);
111        let fn_sig = &func.sig;
112        let vis = &func.vis;
113        let inputs = &fn_sig.inputs;
114        let output = &fn_sig.output;
115
116        let arg_names: Vec<_> = inputs.iter().map(|arg| {
117            match arg {
118                syn::FnArg::Typed(pat_type) => &pat_type.pat,
119                syn::FnArg::Receiver(_) => unreachable!("self not allowed in imports"),
120            }
121        }).collect();
122
123        let wrapper = quote! {
124            #[allow(non_snake_case)]
125            #vis fn #name(#inputs) #output {
126                let f = #slot_name.get()
127                    .expect(concat!("yog: import '", #mod_name, ":", #name_str, "' not bound — is mod '", #mod_name, "' loaded?"));
128                unsafe { f(#(#arg_names),*) }
129            }
130
131            static #slot_name: ::std::sync::OnceLock<unsafe extern "C" fn(#inputs) #output>
132                = ::std::sync::OnceLock::new();
133
134            #[doc(hidden)]
135            #[no_mangle]
136            pub unsafe extern "C" fn #bind_name(ptr: *const ::std::os::raw::c_void) {
137                let f: unsafe extern "C" fn(#inputs) #output = ::std::mem::transmute(ptr);
138                #slot_name.set(f).ok();
139            }
140        };
141
142        out.extend(wrapper);
143    }
144
145    TokenStream::from(out)
146}
147
148// ── Parse helpers ────────────────────────────────────────────────────────
149
150struct ImportBlock {
151    mod_name: String,
152    funcs: Vec<syn::ItemFn>,
153}
154
155impl syn::parse::Parse for ImportBlock {
156    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
157        let from_kw: syn::Ident = input.parse()?;
158        if from_kw != "from" {
159            return Err(syn::Error::new(from_kw.span(), "expected `from`"));
160        }
161        let lit: syn::LitStr = input.parse()?;
162        let mod_name = lit.value();
163
164        let content;
165        syn::braced!(content in input);
166        let mut funcs = Vec::new();
167        while !content.is_empty() {
168            funcs.push(content.parse::<syn::ItemFn>()?);
169            let _ = content.parse::<syn::Token![;]>();
170        }
171
172        Ok(ImportBlock { mod_name, funcs })
173    }
174}