Skip to content

Commit a85f1ee

Browse files
committed
Implement mix-in #[methods] blocks
Adds the `#[methods(mixin = "Name")]` syntax for declaring mix-in blocks. Mix-in blocks have a many-to-many relationship with `NativeClass` types. Both `impl Type` and `impl Trait for Type` blocks are accepted. All mix-in blocks have to be manually registered for gdnative v0.11.x. This can be relaxed in the future with a redesign of the `NativeClass` hierarchy of traits.
1 parent dca4816 commit a85f1ee

File tree

14 files changed

+442
-64
lines changed

14 files changed

+442
-64
lines changed

check.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ function findGodot() {
6262
fi
6363
}
6464

65-
features="gdnative/async,gdnative/serde"
65+
features="gdnative/async,gdnative/serde,gdnative/inventory"
6666
cmds=()
6767

6868
for arg in "${args[@]}"; do

gdnative-core/src/export/class_builder.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
use std::any::TypeId;
2+
use std::cell::RefCell;
3+
use std::collections::HashSet;
14
use std::ffi::CString;
25
use std::marker::PhantomData;
36
use std::ptr;
@@ -20,6 +23,7 @@ use crate::private::get_api;
2023
pub struct ClassBuilder<C> {
2124
pub(super) init_handle: *mut libc::c_void,
2225
pub(super) class_name: CString,
26+
mixins: RefCell<HashSet<TypeId, ahash::RandomState>>,
2327
_marker: PhantomData<C>,
2428
}
2529

@@ -28,6 +32,7 @@ impl<C: NativeClass> ClassBuilder<C> {
2832
Self {
2933
init_handle,
3034
class_name,
35+
mixins: RefCell::default(),
3136
_marker: PhantomData,
3237
}
3338
}
@@ -241,4 +246,48 @@ impl<C: NativeClass> ClassBuilder<C> {
241246
);
242247
}
243248
}
249+
250+
/// Add a mixin to the class being registered.
251+
///
252+
/// # Examples
253+
///
254+
/// ```
255+
/// use gdnative::prelude::*;
256+
///
257+
/// #[derive(NativeClass)]
258+
/// #[inherit(Node)]
259+
/// #[register_with(my_register)]
260+
/// #[no_constructor]
261+
/// struct MyType {}
262+
///
263+
/// // This creates a opaque type `MyMixin` in the current scope that implements
264+
/// // the `Mixin` trait. Mixin types have no public interface or stable layout.
265+
/// #[methods(mixin = "MyMixin")]
266+
/// impl MyType {
267+
/// #[method]
268+
/// fn my_method(&self) -> i64 { 42 }
269+
/// }
270+
///
271+
/// fn my_register(builder: &ClassBuilder<MyType>) {
272+
/// builder.mixin::<MyMixin>();
273+
/// }
274+
/// ```
275+
#[inline]
276+
pub fn mixin<M: Mixin<C>>(&self) {
277+
if self.mixins.borrow_mut().insert(TypeId::of::<M>()) {
278+
M::register(self);
279+
}
280+
}
281+
}
282+
283+
/// Trait for mixins, manually registered `#[methods]` blocks that may be applied to multiple types.
284+
///
285+
/// This trait is implemented on generated types by the `#[methods]` proc-macro only, and has no public interface.
286+
/// Use [`ClassBuilder::mixin`] to register mixins to [`NativeClass`] types.
287+
pub trait Mixin<C>: crate::private::mixin::Sealed + 'static
288+
where
289+
C: NativeClass,
290+
{
291+
#[doc(hidden)]
292+
fn register(builder: &ClassBuilder<C>);
244293
}

gdnative-core/src/private.rs

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -202,31 +202,42 @@ pub(crate) fn print_panic_error(err: Box<dyn std::any::Any + Send>) {
202202
}
203203
}
204204

205-
/// Plugin type to be used by macros for auto registration.
205+
/// Plugin type to be used by macros for auto class registration.
206206
pub struct AutoInitPlugin {
207207
pub f: fn(init_handle: crate::init::InitHandle),
208208
}
209209

210210
#[cfg(feature = "inventory")]
211-
pub use inventory::submit as inventory_submit;
211+
pub mod inventory {
212+
pub use inventory::{collect, submit};
212213

213-
#[cfg(feature = "inventory")]
214-
inventory::collect!(AutoInitPlugin);
215-
216-
#[cfg(not(feature = "inventory"))]
217-
pub use crate::_gdnative_inventory_submit as inventory_submit;
214+
inventory::collect!(super::AutoInitPlugin);
215+
}
218216

219217
#[cfg(not(feature = "inventory"))]
220-
#[macro_export]
221-
#[doc(hidden)]
222-
macro_rules! _gdnative_inventory_submit {
223-
($($tt:tt)*) => {};
218+
pub mod inventory {
219+
pub use crate::_inventory_discard as submit;
220+
pub use crate::_inventory_discard as collect;
221+
222+
#[macro_export]
223+
#[doc(hidden)]
224+
macro_rules! _inventory_discard {
225+
($($tt:tt)*) => {};
226+
}
224227
}
225228

226229
pub mod godot_object {
227230
pub trait Sealed {}
228231
}
229232

233+
pub mod mixin {
234+
pub trait Sealed {}
235+
236+
pub struct Opaque {
237+
_private: (),
238+
}
239+
}
240+
230241
pub(crate) struct ManuallyManagedClassPlaceholder;
231242

232243
unsafe impl crate::object::GodotObject for ManuallyManagedClassPlaceholder {

gdnative-derive/src/lib.rs

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ extern crate quote;
88
use proc_macro::TokenStream;
99
use proc_macro2::TokenStream as TokenStream2;
1010
use quote::ToTokens;
11-
use syn::{AttributeArgs, DeriveInput, ItemFn, ItemImpl, ItemType};
11+
use syn::{parse::Parser, AttributeArgs, DeriveInput, ItemFn, ItemImpl, ItemType};
1212

13-
mod extend_bounds;
1413
mod methods;
1514
mod native_script;
1615
mod profiled;
16+
mod utils;
1717
mod varargs;
1818
mod variant;
1919

@@ -44,13 +44,13 @@ mod variant;
4444
/// ```
4545
#[proc_macro_attribute]
4646
pub fn methods(meta: TokenStream, input: TokenStream) -> TokenStream {
47-
if syn::parse::<syn::parse::Nothing>(meta.clone()).is_err() {
48-
let err = syn::Error::new_spanned(
49-
TokenStream2::from(meta),
50-
"#[methods] does not take parameters.",
51-
);
52-
return error_with_input(input, err);
53-
}
47+
let args =
48+
match syn::punctuated::Punctuated::<syn::NestedMeta, syn::Token![,]>::parse_terminated
49+
.parse(meta)
50+
{
51+
Ok(args) => args.into_iter().collect::<Vec<_>>(),
52+
Err(err) => return error_with_input(input, err),
53+
};
5454

5555
let impl_block = match syn::parse::<ItemImpl>(input.clone()) {
5656
Ok(impl_block) => impl_block,
@@ -63,7 +63,10 @@ pub fn methods(meta: TokenStream, input: TokenStream) -> TokenStream {
6363
err
6464
}
6565

66-
TokenStream::from(methods::derive_methods(impl_block))
66+
match methods::derive_methods(args, impl_block) {
67+
Ok(ts) => ts.into(),
68+
Err(err) => error_with_input(input, err),
69+
}
6770
}
6871

6972
/// Makes a function profiled in Godot's built-in profiler. This macro automatically
@@ -593,7 +596,16 @@ fn crate_gdnative_core() -> proc_macro2::TokenStream {
593596
.expect("crate not found");
594597

595598
match found_crate {
596-
proc_macro_crate::FoundCrate::Itself => quote!(crate),
599+
proc_macro_crate::FoundCrate::Itself => {
600+
// Workaround: `proc-macro-crate` returns `Itself` in doc-tests, and refuses to use unstable env
601+
// variables for detection.
602+
// See https://github.com/bkchr/proc-macro-crate/issues/11
603+
if std::env::var_os("UNSTABLE_RUSTDOC_TEST_PATH").is_some() {
604+
quote!(gdnative_core)
605+
} else {
606+
quote!(crate)
607+
}
608+
}
597609
proc_macro_crate::FoundCrate::Name(name) => {
598610
let ident = proc_macro2::Ident::new(&name, proc_macro2::Span::call_site());
599611
ident.to_token_stream()
@@ -625,6 +637,29 @@ fn crate_gdnative_async() -> proc_macro2::TokenStream {
625637
}
626638
}
627639

640+
/// Returns the (possibly renamed or imported as `gdnative`) identifier of the `gdnative_bindings` crate.
641+
fn crate_gdnative_bindings() -> proc_macro2::TokenStream {
642+
if let Ok(found_crate) = proc_macro_crate::crate_name("gdnative-bindings") {
643+
return match found_crate {
644+
proc_macro_crate::FoundCrate::Itself => quote!(crate),
645+
proc_macro_crate::FoundCrate::Name(name) => {
646+
let ident = proc_macro2::Ident::new(&name, proc_macro2::Span::call_site());
647+
ident.to_token_stream()
648+
}
649+
};
650+
}
651+
652+
let found_crate = proc_macro_crate::crate_name("gdnative").expect("crate not found");
653+
654+
match found_crate {
655+
proc_macro_crate::FoundCrate::Itself => quote!(crate::api),
656+
proc_macro_crate::FoundCrate::Name(name) => {
657+
let ident = proc_macro2::Ident::new(&name, proc_macro2::Span::call_site());
658+
quote!( #ident::api )
659+
}
660+
}
661+
}
662+
628663
/// Hack to emit a warning in expression position through `deprecated`.
629664
/// This is because there is no way to emit warnings from macros in stable Rust.
630665
fn emit_warning<S: std::fmt::Display>(

gdnative-derive/src/methods.rs

Lines changed: 110 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
1-
use syn::{spanned::Spanned, FnArg, Generics, ImplItem, ItemImpl, Pat, PatIdent, Signature, Type};
1+
use syn::{
2+
spanned::Spanned, visit::Visit, FnArg, Generics, ImplItem, ItemImpl, Meta, NestedMeta, Pat,
3+
PatIdent, Signature, Type,
4+
};
25

36
use proc_macro2::TokenStream as TokenStream2;
47
use quote::{quote, ToTokens};
58
use std::boxed::Box;
69

10+
use crate::utils::find_non_concrete;
11+
12+
use self::mixin_args::{MixinArgsBuilder, MixinKind};
13+
14+
mod mixin_args;
15+
716
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
817
pub enum RpcMode {
918
Disabled,
@@ -322,15 +331,57 @@ pub(crate) struct ExportArgs {
322331
pub(crate) is_async: bool,
323332
}
324333

325-
pub(crate) fn derive_methods(item_impl: ItemImpl) -> TokenStream2 {
334+
pub(crate) fn derive_methods(
335+
args: Vec<NestedMeta>,
336+
item_impl: ItemImpl,
337+
) -> Result<TokenStream2, syn::Error> {
326338
let derived = crate::automatically_derived();
339+
let gdnative_core = crate::crate_gdnative_core();
327340
let (impl_block, export) = impl_gdnative_expose(item_impl);
328341
let (impl_generics, _, where_clause) = impl_block.generics.split_for_impl();
329342

330343
let class_name = export.class_ty;
331344

332345
let builder = syn::Ident::new("builder", proc_macro2::Span::call_site());
333346

347+
let args = {
348+
let mut attr_args_builder = MixinArgsBuilder::new();
349+
350+
for arg in args {
351+
if let NestedMeta::Meta(Meta::NameValue(ref pair)) = arg {
352+
attr_args_builder.add_pair(pair)?;
353+
} else if let NestedMeta::Meta(Meta::Path(ref path)) = arg {
354+
attr_args_builder.add_path(path)?;
355+
} else {
356+
let msg = format!("Unexpected argument: {arg:?}");
357+
return Err(syn::Error::new(arg.span(), msg));
358+
}
359+
}
360+
361+
attr_args_builder.done()?
362+
};
363+
364+
let non_concrete = find_non_concrete::with_visitor(&impl_block.generics, |v| {
365+
v.visit_type(&impl_block.self_ty)
366+
});
367+
368+
let non_concrete = if non_concrete.is_empty() {
369+
None
370+
} else if non_concrete.len() == 1 {
371+
Some(non_concrete[0])
372+
} else {
373+
Some(impl_block.self_ty.span())
374+
};
375+
376+
if let Some(span) = non_concrete {
377+
if matches!(args.mixin, Some(MixinKind::Auto(_))) {
378+
return Err(syn::Error::new(
379+
span,
380+
"non-concrete mixins must be named and manually registered",
381+
));
382+
}
383+
}
384+
334385
let methods = export
335386
.methods
336387
.into_iter()
@@ -402,19 +453,66 @@ pub(crate) fn derive_methods(item_impl: ItemImpl) -> TokenStream2 {
402453
})
403454
.collect::<Vec<_>>();
404455

405-
quote::quote!(
406-
#impl_block
456+
match args.mixin {
457+
Some(mixin_kind) => {
458+
let vis = args.pub_.then(|| quote!(pub));
459+
460+
let mixin_name = match &mixin_kind {
461+
MixinKind::Named(ident) => ident.clone(),
462+
MixinKind::Auto(span) => {
463+
return Err(syn::Error::new(
464+
*span,
465+
"mixins must be named in gdnative v0.11.x",
466+
))
467+
}
468+
};
407469

408-
#derived
409-
impl #impl_generics gdnative::export::NativeClassMethods for #class_name #where_clause {
410-
fn nativeclass_register(#builder: &::gdnative::export::ClassBuilder<Self>) {
411-
use gdnative::export::*;
470+
let body = quote! {
471+
#derived
472+
#vis struct #mixin_name {
473+
_opaque: #gdnative_core::private::mixin::Opaque,
474+
}
412475

413-
#(#methods)*
414-
}
476+
#derived
477+
impl #gdnative_core::private::mixin::Sealed for #mixin_name {}
478+
#derived
479+
impl #impl_generics #gdnative_core::export::Mixin<#class_name> for #mixin_name #where_clause {
480+
fn register(#builder: &#gdnative_core::export::ClassBuilder<#class_name>) {
481+
use #gdnative_core::export::*;
482+
483+
#(#methods)*
484+
}
485+
}
486+
};
487+
488+
let body = match &mixin_kind {
489+
MixinKind::Named(_) => body,
490+
MixinKind::Auto(_) => quote! {
491+
const _: () = {
492+
#body
493+
}
494+
},
495+
};
496+
497+
Ok(quote::quote!(
498+
#impl_block
499+
#body
500+
))
415501
}
502+
None => Ok(quote::quote!(
503+
#impl_block
416504

417-
)
505+
#derived
506+
impl #impl_generics #gdnative_core::export::NativeClassMethods for #class_name #where_clause {
507+
fn nativeclass_register(#builder: &#gdnative_core::export::ClassBuilder<Self>) {
508+
use #gdnative_core::export::*;
509+
510+
#(#methods)*
511+
}
512+
}
513+
514+
)),
515+
}
418516
}
419517

420518
/// Extract the data to export from the impl block.
@@ -464,7 +562,7 @@ fn impl_gdnative_expose(ast: ItemImpl) -> (ItemImpl, ClassMethodExport) {
464562
};
465563

466564
if is_export {
467-
use syn::{punctuated::Punctuated, Lit, Meta, NestedMeta};
565+
use syn::{punctuated::Punctuated, Lit};
468566
let mut export_args =
469567
export_args.get_or_insert_with(ExportArgs::default);
470568
export_args.is_old_syntax = is_old_syntax;

0 commit comments

Comments
 (0)