Skip to main content

yog_interop/
lib.rs

1//! Proc-macros for mods' exports (`docs/design/bus.md`, stage 4).
2//!
3//! `#[yog_export]` on a fn makes it a bus service at `mod_id:name` (its
4//! `ctor` registers it when the mod's library loads; `export_mod!`
5//! provides it); on a struct or enum it derives the Yog Data schema the
6//! calls carry it with. `import!` writes the calling side.
7
8use proc_macro::TokenStream;
9use quote::{format_ident, quote};
10
11mod inject;
12mod mirror;
13mod nanite;
14
15// ── #[yog_export] ─────────────────────────────────────────────────────────
16
17/// The derives an exported type carries: its Yog Data schema, through
18/// `yog_api`'s re-export (a mod depends on `yog_api` only).
19fn schema_derive() -> proc_macro2::TokenStream {
20    quote! {
21        #[derive(::yog_api::yog_data::Schema)]
22        #[data(crate = "::yog_api::yog_data")]
23    }
24}
25
26/// An exported enum with data is tagged (`#[data(tag = "type")]`) unless
27/// it names its own tag: the mod writes the enum, not its format.
28fn enum_tag(e: &syn::ItemEnum) -> proc_macro2::TokenStream {
29    use yog_ids::data_attr::{DATA, TAG};
30    let has_data = e
31        .variants
32        .iter()
33        .any(|v| !matches!(v.fields, syn::Fields::Unit));
34    let tagged = e.attrs.iter().filter(|a| a.path().is_ident(DATA)).any(|a| {
35        let mut tag = false;
36        // Every `key = value` read past; the tag noted.
37        let _ = a.parse_nested_meta(|m| {
38            tag |= m.path.is_ident(TAG);
39            if m.input.peek(syn::Token![=]) {
40                m.value()?.parse::<syn::Expr>()?;
41            }
42            Ok(())
43        });
44        tag
45    });
46    if !has_data || tagged {
47        return quote! {};
48    }
49    let tag = yog_ids::word::TYPE;
50    let (data, tag_key) = (format_ident!("{DATA}"), format_ident!("{TAG}"));
51    quote! { #[#data(#tag_key = #tag)] }
52}
53
54/// Whether `ty` is a `Registry` (a leading `&mut Registry` is the
55/// exporting mod's own, never part of a call).
56fn is_registry_ty(ty: &syn::Type) -> bool {
57    let ty = match ty {
58        syn::Type::Reference(r) => &*r.elem,
59        other => other,
60    };
61    quote! { #ty }
62        .to_string()
63        .replace(' ', "")
64        .ends_with("Registry")
65}
66
67/// A function's call parameters (a leading `Registry` left out), each a
68/// plain name — `__yog_argN` for a pattern that isn't one.
69fn call_params(sig: &syn::Signature) -> (bool, Vec<(syn::Ident, syn::Type)>) {
70    let mut typed: Vec<&syn::PatType> = sig
71        .inputs
72        .iter()
73        .filter_map(|a| match a {
74            syn::FnArg::Typed(pt) => Some(pt),
75            syn::FnArg::Receiver(_) => None,
76        })
77        .collect();
78    let registry = typed.first().is_some_and(|pt| is_registry_ty(&pt.ty));
79    if registry {
80        typed.remove(0);
81    }
82    let params = typed
83        .iter()
84        .enumerate()
85        .map(|(i, pt)| {
86            let name = match &*pt.pat {
87                syn::Pat::Ident(p) => p.ident.clone(),
88                _ => format_ident!("__yog_arg{}", i),
89            };
90            (name, (*pt.ty).clone())
91        })
92        .collect();
93    (registry, params)
94}
95
96/// How a call's arguments cross the bus: a lone parameter as itself (so
97/// a caller needs only its type — handlers, callbacks), several as a struct
98/// of them by name (`__YogArgs_name`).
99struct Args {
100    /// The type carried.
101    ty: proc_macro2::TokenStream,
102    /// That struct's definition (nothing for a lone parameter).
103    def: proc_macro2::TokenStream,
104    /// The value carried, made of the parameters (in scope by name).
105    pack: proc_macro2::TokenStream,
106    /// The parameters again, out of the value carried (`a`).
107    unpack: proc_macro2::TokenStream,
108}
109
110fn args_of(name: &syn::Ident, params: &[(syn::Ident, syn::Type)]) -> Args {
111    if let [(param, ty)] = params {
112        return Args {
113            ty: quote! { #ty },
114            def: quote! {},
115            pack: quote! { &#param },
116            unpack: quote! { a },
117        };
118    }
119    let args = format_ident!("__YogArgs_{}", name);
120    let fields = params.iter().map(|(n, t)| quote! { #n: #t });
121    let names: Vec<&syn::Ident> = params.iter().map(|(n, _)| n).collect();
122    let derive = schema_derive();
123    Args {
124        ty: quote! { #args },
125        def: quote! {
126            #[doc(hidden)]
127            // Named after the function (`__YogArgs_give_item`), which is snake case.
128            #[allow(non_camel_case_types)]
129            #derive
130            struct #args { #(#fields),* }
131        },
132        pack: quote! { &#args { #(#names),* } },
133        unpack: quote! { #(a.#names),* },
134    }
135}
136
137fn output_type(sig: &syn::Signature) -> proc_macro2::TokenStream {
138    match &sig.output {
139        syn::ReturnType::Default => quote! { () },
140        syn::ReturnType::Type(_, ty) => quote! { #ty },
141    }
142}
143
144/// Marks a function, struct or enum for other mods: a function becomes the
145/// service `mod_id:name`, a type gets the schema its values cross with.
146/// A function's doc comment, its lines joined.
147fn doc_of(attrs: &[syn::Attribute]) -> String {
148    let lines: Vec<String> = attrs
149        .iter()
150        .filter(|a| a.path().is_ident("doc"))
151        .filter_map(|a| match &a.meta {
152            syn::Meta::NameValue(nv) => match &nv.value {
153                syn::Expr::Lit(syn::ExprLit {
154                    lit: syn::Lit::Str(s),
155                    ..
156                }) => Some(s.value().trim().to_owned()),
157                _ => None,
158            },
159            _ => None,
160        })
161        .collect();
162    lines.join(" ")
163}
164
165/// The interfaces an export says it implements: `implements = "a", implements = "b"`.
166fn implements_of(attr: TokenStream) -> syn::Result<Vec<String>> {
167    let mut out = Vec::new();
168    let parser = syn::meta::parser(|meta| {
169        if meta.path.is_ident("implements") {
170            out.push(meta.value()?.parse::<syn::LitStr>()?.value());
171            Ok(())
172        } else {
173            Err(meta.error("unknown `yog_export` argument (implements)"))
174        }
175    });
176    syn::parse::Parser::parse(parser, attr)?;
177    Ok(out)
178}
179
180#[proc_macro_attribute]
181pub fn yog_export(attr: TokenStream, item: TokenStream) -> TokenStream {
182    let implements = match implements_of(attr) {
183        Ok(i) => i,
184        Err(e) => return e.to_compile_error().into(),
185    };
186    let tokens = proc_macro2::TokenStream::from(item.clone());
187    if syn::parse2::<syn::ItemStruct>(tokens.clone()).is_ok() {
188        let derive = schema_derive();
189        return TokenStream::from(quote! { #derive #tokens });
190    }
191    if let Ok(e) = syn::parse2::<syn::ItemEnum>(tokens.clone()) {
192        let derive = schema_derive();
193        let tag = enum_tag(&e);
194        return TokenStream::from(quote! { #derive #tag #tokens });
195    }
196    let func = syn::parse_macro_input!(item as syn::ItemFn);
197    let name = &func.sig.ident;
198    let name_str = name.to_string();
199    let serve_name = format_ident!("__yog_serve_{}", name);
200    let ctor_name = format_ident!("__yog_ctor_register_{}", name);
201    let (registry, params) = call_params(&func.sig);
202    let Args {
203        ty: args,
204        def: args_def,
205        unpack,
206        ..
207    } = args_of(name, &params);
208    let output = output_type(&func.sig);
209    let doc = doc_of(&func.attrs);
210    // The exporting mod's own `Registry`, from the API it captured at load.
211    let (setup, registry_arg) = if registry {
212        (
213            quote! { let mut __yog_registry = unsafe { ::yog_api::Registry::from_raw(::yog_api::__current_api_ptr() as *const ::yog_api::YogApi) }; },
214            quote! { &mut __yog_registry, },
215        )
216    } else {
217        (quote! {}, quote! {})
218    };
219    TokenStream::from(quote! {
220        #func
221
222        #args_def
223
224        #[doc(hidden)]
225        unsafe extern "C" fn #serve_name(
226            _ud: *mut ::std::os::raw::c_void,
227            payload: ::yog_api::YogBytes,
228            reply: *const ::yog_api::YogReply,
229        ) -> ::yog_api::YogStatus {
230            unsafe {
231                ::yog_api::__yog_serve::<#args, #output>(payload, reply, |a| {
232                    #setup
233                    #name(#registry_arg #unpack)
234                })
235            }
236        }
237
238        // Runs when this mod's library loads, before `yog_mod_register`.
239        #[::yog_api::ctor::ctor(unsafe, crate_path = ::yog_api::ctor)]
240        #[doc(hidden)]
241        fn #ctor_name() {
242            ::yog_api::__yog_export_registry().lock().push(
243                ::yog_api::YogExportEntry {
244                    name: #name_str,
245                    handler: #serve_name,
246                    doc: #doc,
247                    shapes: || (<#args as ::yog_api::yog_data::Schema>::shape(), <#output as ::yog_api::yog_data::Schema>::shape()),
248                    implements: &[#(#implements),*],
249                }
250            );
251        }
252    })
253}
254
255// ── #[yog_mirror] ─────────────────────────────────────────────────────────
256
257/// Makes a struct a mirror's row (see `mirror.rs`): `#[yog_mirror(space =
258/// …, owner = "mod")]`, a field's fact named by `#[fact(…)]`.
259#[proc_macro_attribute]
260pub fn yog_mirror(attr: TokenStream, item: TokenStream) -> TokenStream {
261    mirror::expand(attr.into(), item.into())
262        .unwrap_or_else(syn::Error::into_compile_error)
263        .into()
264}
265
266// ── #[nanite] ─────────────────────────────────────────────────────────────
267
268/// A Java class reached by a mod (see `nanite.rs`): on a unit struct
269/// `#[nanite("net.minecraft…")]`, on its `impl` `#[nanite]` (body-less
270/// `fn`s become calls; `#[field]`, `#[new]`).
271#[proc_macro_attribute]
272pub fn nanite(attr: TokenStream, item: TokenStream) -> TokenStream {
273    let item = proc_macro2::TokenStream::from(item);
274    let out = if attr.is_empty() {
275        nanite::members(item)
276    } else {
277        nanite::class(attr.into(), item)
278    };
279    out.unwrap_or_else(syn::Error::into_compile_error).into()
280}
281
282/// The nanite macros (see `inject.rs`), each a kind with its defaults.
283macro_rules! nanites {
284    ($($(#[$doc:meta])* $name:ident => $kind:ident, $at:ident, $cancellable:literal;)*) => {$(
285        $(#[$doc])*
286        #[proc_macro_attribute]
287        pub fn $name(attr: TokenStream, item: TokenStream) -> TokenStream {
288            let kind = inject::Kind {
289                kind: stringify!($kind),
290                at: stringify!($at),
291                cancellable: $cancellable,
292            };
293            inject::expand(&kind, attr.into(), item.into())
294                .unwrap_or_else(syn::Error::into_compile_error)
295                .into()
296        }
297    )*};
298}
299
300nanites! {
301    /// `@Inject`: `#[inject(LivingEntity::HURT, at = HEAD, cancellable)]`.
302    inject => INJECT, HEAD, false;
303    /// `@ModifyReturnValue`: an inject at RETURN whose handler sets the value.
304    modify_return => INJECT, RETURN, true;
305    /// `@Overwrite`: an inject at HEAD whose handler always answers.
306    overwrite => INJECT, HEAD, true;
307    /// `@Redirect`: `#[redirect(M, target = "Lowner;name(desc)R")]`.
308    redirect => REDIRECT, INVOKE, false;
309    /// `@WrapOperation`: a redirect whose handler calls the original (`Hook::call_original`).
310    wrap => REDIRECT, INVOKE, false;
311    /// `@ModifyArg`: `#[modify_arg(M, target = "…", index = 1)]`.
312    modify_arg => MODIFY_ARG, INVOKE, false;
313    /// `@ModifyVariable`: `#[modify_variable(M, at = STORE, ty = "F", index = 3)]`.
314    modify_variable => MODIFY_VARIABLE, STORE, false;
315    /// `@ModifyConstant`: `#[modify_constant(M, constant = "float:0.5")]`.
316    modify_constant => MODIFY_CONSTANT, HEAD, false;
317    /// A method added to a class: `#[add_method(LivingEntity, name = "yogCharge", desc = "()I")]`
318    /// (`target`: the interface whose method it is, for its run-time name).
319    add_method => ADD_METHOD, HEAD, false;
320}
321
322// ── #[bench] ─────────────────────────────────────────────────────────────
323
324/// Marks a function in a mod's `benches/*.rs` as a benchmark `yog bench`
325/// runs: `fn name(b: &mut criterion::Bencher)` for one benchmark, or
326/// `fn name(c: &mut criterion::Criterion)` to set up groups. yog generates
327/// the file's `main`; this only makes the function reachable from it.
328#[proc_macro_attribute]
329pub fn bench(_attr: TokenStream, item: TokenStream) -> TokenStream {
330    let mut f = syn::parse_macro_input!(item as syn::ItemFn);
331    if matches!(f.vis, syn::Visibility::Inherited) {
332        f.vis = syn::parse_quote!(pub(crate));
333    }
334    TokenStream::from(quote! { #f })
335}
336
337// ── import! macro ────────────────────────────────────────────────────────
338
339/// Another mod's exports, callable here: `import! { from "mod_id" { pub fn
340/// name(a: A) -> R; pub struct T { .. } } }` — each fn calls `mod_id:name`
341/// on the bus and answers `Option<R>` (`None`: the mod isn't loaded, or it
342/// failed); each type gets the schema its values cross with.
343#[proc_macro]
344pub fn import(input: TokenStream) -> TokenStream {
345    let import_block = syn::parse_macro_input!(input as ImportBlock);
346    let mod_name = &import_block.mod_name;
347    let mod_ident = format_ident!("{}", mod_name.replace('-', "_"));
348    let mut items = proc_macro2::TokenStream::new();
349    for item in &import_block.items {
350        match item {
351            ImportItem::Func(func) => items.extend(generate_import_fn(func, mod_name)),
352            ImportItem::Type(t) => {
353                let derive = schema_derive();
354                items.extend(quote! { #derive #t });
355            }
356        }
357    }
358    TokenStream::from(quote! { pub mod #mod_ident { use super::*; #items } })
359}
360
361/// An imported function: `pub fn name(args) -> Out;` (a body, if written, is ignored).
362struct ImportFn {
363    vis: syn::Visibility,
364    sig: syn::Signature,
365}
366
367fn generate_import_fn(func: &ImportFn, mod_name: &str) -> proc_macro2::TokenStream {
368    let name = &func.sig.ident;
369    let name_str = name.to_string();
370    let vis = &func.vis;
371    let (_, params) = call_params(&func.sig);
372    let Args {
373        ty: args,
374        def: args_def,
375        pack,
376        ..
377    } = args_of(name, &params);
378    let output = output_type(&func.sig);
379    let names: Vec<&syn::Ident> = params.iter().map(|(n, _)| n).collect();
380    let types: Vec<&syn::Type> = params.iter().map(|(_, t)| t).collect();
381    quote! {
382        #args_def
383
384        /// The other mod's answer; `None` when it isn't loaded or failed (see its log).
385        #vis fn #name(#(#names: #types),*) -> ::std::option::Option<#output> {
386            ::yog_api::call_export::<#args, #output>(#mod_name, #name_str, #pack)
387        }
388    }
389}
390
391// ── Parse helpers ────────────────────────────────────────────────────────
392
393struct ImportBlock {
394    mod_name: String,
395    items: Vec<ImportItem>,
396}
397enum ImportItem {
398    Func(Box<ImportFn>),
399    /// A struct or enum, as written.
400    Type(proc_macro2::TokenStream),
401}
402
403impl syn::parse::Parse for ImportBlock {
404    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
405        let from_kw: syn::Ident = input.parse()?;
406        if from_kw != "from" {
407            return Err(syn::Error::new(from_kw.span(), "expected `from`"));
408        }
409        let lit: syn::LitStr = input.parse()?;
410        let mod_name = lit.value();
411        let content;
412        syn::braced!(content in input);
413        let mut items = Vec::new();
414        while !content.is_empty() {
415            let attrs = content.call(syn::Attribute::parse_outer)?;
416            let vis: syn::Visibility = content.parse()?;
417            if content.peek(syn::Token![fn]) {
418                let sig: syn::Signature = content.parse()?;
419                if content.peek(syn::token::Brace) {
420                    let _body: syn::Block = content.parse()?;
421                } else {
422                    content.parse::<syn::Token![;]>()?;
423                }
424                items.push(ImportItem::Func(Box::new(ImportFn { vis, sig })));
425            } else if content.peek(syn::Token![enum]) {
426                let mut e: syn::ItemEnum = content.parse()?;
427                e.attrs = attrs;
428                e.vis = vis;
429                items.push(ImportItem::Type(quote! { #e }));
430            } else {
431                let mut s: syn::ItemStruct = content.parse()?;
432                s.attrs = attrs;
433                s.vis = vis;
434                items.push(ImportItem::Type(quote! { #s }));
435                let _ = content.parse::<syn::Token![;]>();
436            }
437        }
438        Ok(ImportBlock { mod_name, items })
439    }
440}