diff --git a/codegen/src/custom_type.rs b/codegen/src/custom_type.rs index 757c2fd51..46bdb07bd 100644 --- a/codegen/src/custom_type.rs +++ b/codegen/src/custom_type.rs @@ -1,9 +1,34 @@ use proc_macro2::{Span, TokenStream}; use quote::quote; -use syn::{DeriveInput, Fields}; +use syn::{spanned::Spanned, DeriveInput, Fields}; + +const ATTR_NAME: &str = "rhai_type_name"; +const ATTR_SKIP: &str = "rhai_type_skip"; +const ATTR_GET: &str = "rhai_type_get"; +const ATTR_GET_MUT: &str = "rhai_type_get_mut"; +const ATTR_SET: &str = "rhai_type_set"; +const ATTR_READONLY: &str = "rhai_type_readonly"; +const ATTR_EXTRA: &str = "rhai_type_extra"; pub fn derive_custom_type_impl(input: DeriveInput) -> TokenStream { let type_name = input.ident; + let mut pretty_print_name = quote! { stringify!(#type_name) }; + let mut extras = Vec::new(); + + for attr in input.attrs.iter() { + if attr.path().is_ident(ATTR_NAME) { + // Type name + match attr.parse_args::() { + Ok(name) => pretty_print_name = quote! { #name }, + Err(e) => return e.into_compile_error(), + } + } else if attr.path().is_ident(ATTR_EXTRA) { + extras.push( + attr.parse_args() + .unwrap_or_else(syn::Error::into_compile_error), + ); + } + } let accessors = match input.data { // struct Foo; @@ -23,28 +48,51 @@ pub fn derive_custom_type_impl(input: DeriveInput) -> TokenStream { let iter = fields.enumerate().map(|(i, field)| { let mut name = None; let mut get_fn = None; + let mut get_mut_fn = None; let mut set_fn = None; let mut readonly = false; let mut skip = false; for attr in field.attrs.iter() { - if attr.path().is_ident("rhai_custom_type_skip") { + if attr.path().is_ident(ATTR_SKIP) { skip = true; - } else if attr.path().is_ident("rhai_custom_type_name") { + + if get_fn.is_some() + || get_mut_fn.is_some() + || set_fn.is_some() + || name.is_some() + || readonly + { + return syn::Error::new( + attr.path().span(), + format!("cannot use '{ATTR_SKIP}' with other attributes"), + ) + .into_compile_error(); + } + + continue; + } + + if attr.path().is_ident(ATTR_NAME) { name = Some( attr.parse_args() .unwrap_or_else(syn::Error::into_compile_error), ); - } else if attr.path().is_ident("rhai_custom_type_get") { + } else if attr.path().is_ident(ATTR_GET) { get_fn = Some( attr.parse_args() .unwrap_or_else(syn::Error::into_compile_error), ); - } else if attr.path().is_ident("rhai_custom_type_set") { + } else if attr.path().is_ident(ATTR_GET_MUT) { + get_mut_fn = Some( + attr.parse_args() + .unwrap_or_else(syn::Error::into_compile_error), + ); + } else if attr.path().is_ident(ATTR_SET) { if readonly { return syn::Error::new( - Span::call_site(), - "cannot use 'rhai_custom_type_set' with 'rhai_custom_type_readonly'", + attr.path().span(), + format!("cannot use '{ATTR_SET}' with '{ATTR_READONLY}'"), ) .into_compile_error(); } @@ -52,24 +100,25 @@ pub fn derive_custom_type_impl(input: DeriveInput) -> TokenStream { attr.parse_args() .unwrap_or_else(syn::Error::into_compile_error), ); - } else if attr.path().is_ident("rhai_custom_type_readonly") { + } else if attr.path().is_ident(ATTR_READONLY) { if set_fn.is_some() { return syn::Error::new( - Span::call_site(), - "cannot use 'rhai_custom_type_readonly' with 'rhai_custom_type_set'", + attr.path().span(), + format!("cannot use '{ATTR_READONLY}' with '{ATTR_SET}'"), ) .into_compile_error(); } readonly = true; } - } - if skip && (get_fn.is_some() || set_fn.is_some() || name.is_some() || readonly) { - return syn::Error::new( - Span::call_site(), - "cannot use 'rhai_custom_type_skip' with other attributes", - ) - .into_compile_error(); + if skip { + let attr_name = attr.path().get_ident().unwrap(); + return syn::Error::new( + attr.path().span(), + format!("cannot use '{}' with '{ATTR_SKIP}'", attr_name), + ) + .into_compile_error(); + } } if !skip { @@ -84,7 +133,7 @@ pub fn derive_custom_type_impl(input: DeriveInput) -> TokenStream { quote! { #index } }; - generate_accessor_fns(field_name, name, get_fn, set_fn, readonly) + generate_accessor_fns(field_name, name, get_fn, get_mut_fn, set_fn, readonly) } else { quote! {} } @@ -94,18 +143,21 @@ pub fn derive_custom_type_impl(input: DeriveInput) -> TokenStream { } syn::Data::Enum(_) => { - syn::Error::new(Span::call_site(), "enums are not yet implemented").into_compile_error() + return syn::Error::new(Span::call_site(), "enums are not yet implemented") + .into_compile_error() } syn::Data::Union(_) => { - syn::Error::new(Span::call_site(), "unions are not supported").into_compile_error() + return syn::Error::new(Span::call_site(), "unions are not yet supported") + .into_compile_error() } }; quote! { impl CustomType for #type_name { fn build(mut builder: TypeBuilder) { - builder.with_name(stringify!(#type_name)); + builder.with_name(#pretty_print_name); #accessors; + #(#extras(&mut builder);)* } } } @@ -115,13 +167,16 @@ fn generate_accessor_fns( field: TokenStream, name: Option, get: Option, + get_mut: Option, set: Option, readonly: bool, ) -> proc_macro2::TokenStream { - let get = get.map_or_else( - || quote! { |obj: &mut Self| obj.#field.clone() }, - |func| quote! { #func }, - ); + let get = match (get_mut, get) { + (Some(func), _) => quote! { #func }, + (None, Some(func)) => quote! { |obj: &mut Self| #func(&*obj) }, + (None, None) => quote! { |obj: &mut Self| obj.#field.clone() }, + }; + let set = set.map_or_else( || quote! { |obj: &mut Self, val| obj.#field = val }, |func| quote! { #func }, diff --git a/codegen/src/lib.rs b/codegen/src/lib.rs index c72c05bff..3a6645653 100644 --- a/codegen/src/lib.rs +++ b/codegen/src/lib.rs @@ -413,11 +413,12 @@ pub fn set_exported_global_fn(args: proc_macro::TokenStream) -> proc_macro::Toke #[proc_macro_derive( CustomType, attributes( - rhai_custom_type_name, - rhai_custom_type_get, - rhai_custom_type_set, - rhai_custom_type_readonly, - rhai_custom_type_skip + rhai_type_name, + rhai_type_get, + rhai_type_set, + rhai_type_readonly, + rhai_type_skip, + rhai_type_extra ) )] pub fn derive_custom_type(input: proc_macro::TokenStream) -> proc_macro::TokenStream { diff --git a/codegen/tests/test_derive.rs b/codegen/tests/test_derive.rs index e720a7a26..c09dbc224 100644 --- a/codegen/tests/test_derive.rs +++ b/codegen/tests/test_derive.rs @@ -1,30 +1,31 @@ -use rhai::{CustomType, TypeBuilder, FLOAT, INT}; +use rhai::{CustomType, Engine, TypeBuilder, FLOAT, INT}; // Sanity check to make sure everything compiles #[derive(Clone, CustomType)] pub struct Bar( #[cfg(not(feature = "no_float"))] // check other attributes - #[rhai_custom_type_skip] + #[rhai_type_skip] FLOAT, INT, - #[rhai_custom_type_name("boo")] - #[rhai_custom_type_readonly] + #[rhai_type_name("boo")] + #[rhai_type_readonly] String, Vec, ); -#[derive(Clone, CustomType)] +#[derive(Clone, Default, CustomType)] +#[rhai_type_name("MyFoo")] +#[rhai_type_extra(Self::build_extra)] pub struct Foo { - #[cfg(not(feature = "no_float"))] // check other attributes - #[rhai_custom_type_skip] + #[rhai_type_skip] _dummy: FLOAT, - #[rhai_custom_type_get(get_bar)] + #[rhai_type_get(get_bar)] pub bar: INT, - #[rhai_custom_type_name("boo")] - #[rhai_custom_type_readonly] + #[rhai_type_name("boo")] + #[rhai_type_readonly] pub(crate) baz: String, - #[rhai_custom_type_set(Self::set_qux)] + #[rhai_type_set(Self::set_qux)] pub qux: Vec, } @@ -32,11 +33,20 @@ impl Foo { pub fn set_qux(&mut self, value: Vec) { self.qux = value; } + + fn build_extra(builder: &mut TypeBuilder) { + builder.with_fn("new_foo", || Self::default()); + } } -fn get_bar(_this: &mut Foo) -> INT { +fn get_bar(_this: &Foo) -> INT { 42 } #[test] -fn test() {} +fn test() { + let mut engine = Engine::new(); + engine.build_type::().build_type::(); + + engine.run("new_foo()").unwrap(); +} diff --git a/src/api/build_type.rs b/src/api/build_type.rs index 6d1e37a8a..53ba2bdf1 100644 --- a/src/api/build_type.rs +++ b/src/api/build_type.rs @@ -144,7 +144,7 @@ impl<'s, T: Variant + Clone> TypeBuilder<'_, 's, T> { self } - /// Register a custom function. + /// Register a custom method. #[inline(always)] pub fn with_fn( &mut self, diff --git a/tests/build_type.rs b/tests/build_type.rs index f1f0ebe4b..ac4b26e97 100644 --- a/tests/build_type.rs +++ b/tests/build_type.rs @@ -1,5 +1,5 @@ #![cfg(not(feature = "no_object"))] -use rhai::{CustomType, Engine, EvalAltResult, Position, Scope, TypeBuilder, INT}; +use rhai::{CustomType, Engine, EvalAltResult, Position, TypeBuilder, INT}; #[test] fn test_build_type() { @@ -160,18 +160,28 @@ fn test_build_type() { #[test] fn test_build_type_macro() { #[derive(Debug, Clone, Eq, PartialEq, CustomType)] + #[rhai_type_name("MyFoo")] + #[rhai_type_extra(Self::build_extra)] struct Foo { - #[rhai_custom_type_skip] + #[rhai_type_skip] dummy: i64, - #[rhai_custom_type_readonly] + #[rhai_type_readonly] bar: i64, - #[rhai_custom_type_name("emphasize")] + #[rhai_type_name("emphasize")] baz: bool, - #[rhai_custom_type_set(Self::set_hello)] + #[rhai_type_set(Self::set_hello)] hello: String, } impl Foo { + pub fn new() -> Self { + Self { + dummy: 0, + bar: 5, + baz: false, + hello: "hey".to_string(), + } + } pub fn set_hello(&mut self, value: String) { self.hello = if self.baz { let mut s = self.hello.clone(); @@ -184,27 +194,19 @@ fn test_build_type_macro() { value }; } + fn build_extra(builder: &mut TypeBuilder) { + builder.with_fn("new_foo", || Self::new()); + } } let mut engine = Engine::new(); engine.build_type::(); - let mut scope = Scope::new(); - scope.push( - "foo", - Foo { - dummy: 0, - bar: 5, - baz: false, - hello: "hey".to_string(), - }, - ); - assert_eq!( engine - .eval_with_scope::( - &mut scope, + .eval::( r#" + let foo = new_foo(); foo.hello = "this should not be seen"; foo.hello = "world!"; foo.emphasize = true;