diff --git a/godot-macros/src/derive/data_models/godot_convert.rs b/godot-macros/src/derive/data_models/godot_convert.rs index 8de589e9e..b6e2a2076 100644 --- a/godot-macros/src/derive/data_models/godot_convert.rs +++ b/godot-macros/src/derive/data_models/godot_convert.rs @@ -7,6 +7,7 @@ use proc_macro2::{Ident, TokenStream}; use quote::ToTokens; +use venial::{GenericParamList, WhereClause}; use super::c_style_enum::CStyleEnum; use super::godot_attribute::{GodotAttribute, ViaType}; @@ -18,23 +19,41 @@ use crate::util::bail; pub struct GodotConvert { /// The name of the type we're deriving for. pub ty_name: Ident, + pub where_clause: Option, + pub generic_params: Option, /// The data from the type and `godot` attribute. pub convert_type: ConvertType, } impl GodotConvert { pub fn parse_declaration(item: venial::Item) -> ParseResult { - let (name, where_clause, generic_params) = match &item { - venial::Item::Struct(struct_) => ( - struct_.name.clone(), - &struct_.where_clause, - &struct_.generic_params, - ), - venial::Item::Enum(enum_) => ( - enum_.name.clone(), - &enum_.where_clause, - &enum_.generic_params, - ), + let data = ConvertType::parse_declaration(&item)?; + + let (name, where_clause, generic_params) = match item { + venial::Item::Struct(struct_) => { + (struct_.name, struct_.where_clause, struct_.generic_params) + } + venial::Item::Enum(enum_) => { + // We only have C-style enums, so Rust would already complain that generics are unused. + // This provides clearer error messages though. + if let Some(generic_params) = &enum_.generic_params { + return bail!( + generic_params, + "#[derive(GodotConvert)] does not support lifetimes or generic parameters on enums" + ); + } + + // Is this check even necessary? What's the use case of where clauses without generics? + // For traits, one can imagine `Self: SomeBound`, but for structs/enums? + if let Some(where_clause) = &enum_.where_clause { + return bail!( + where_clause, + "#[derive(GodotConvert)] does not support where clauses" + ); + } + + (enum_.name, enum_.where_clause, enum_.generic_params) + } other => { return bail!( other, @@ -43,26 +62,10 @@ impl GodotConvert { } }; - if let Some(generic_params) = generic_params { - return bail!( - generic_params, - "#[derive(GodotConvert)] does not support lifetimes or generic parameters" - ); - } - - // Is this check even necessary? What's the use case of where clauses without generics? - // For traits, one can imagine `Self: SomeBound`, but for structs/enums? - if let Some(where_clause) = where_clause { - return bail!( - where_clause, - "#[derive(GodotConvert)] does not support where clauses" - ); - } - - let data = ConvertType::parse_declaration(item)?; - Ok(Self { ty_name: name, + where_clause: where_clause, + generic_params: generic_params, convert_type: data, }) } @@ -77,10 +80,10 @@ pub enum ConvertType { } impl ConvertType { - pub fn parse_declaration(item: venial::Item) -> ParseResult { - let attribute = GodotAttribute::parse_attribute(&item)?; + pub fn parse_declaration(item: &venial::Item) -> ParseResult { + let attribute = GodotAttribute::parse_attribute(item)?; - match &item { + match item { venial::Item::Struct(struct_) => { let GodotAttribute::Transparent { .. } = attribute else { return bail!( diff --git a/godot-macros/src/derive/data_models/newtype.rs b/godot-macros/src/derive/data_models/newtype.rs index 9ed13cab5..e12061a0e 100644 --- a/godot-macros/src/derive/data_models/newtype.rs +++ b/godot-macros/src/derive/data_models/newtype.rs @@ -5,18 +5,34 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use proc_macro2::{Ident, TokenStream}; +use proc_macro2::{Ident, Literal, Span, TokenStream}; use quote::quote; +use venial::{NamedField, TupleField}; use crate::ParseResult; use crate::util::bail; +pub enum FieldType { + Tuple(Literal), + Named(Ident), +} + +pub enum FieldsType { + Tuple(Vec), + Named(Vec), +} + /// Stores info from the field of a newtype struct for use in deriving `GodotConvert` and other related traits. +/// +/// Here, a newtype struct must have exactly 1 non-ZST field, and can have an arbitrary amount of ZST fields. pub struct NewtypeStruct { /// The name of the field. /// /// If `None`, then this represents a tuple-struct with one field. - pub name: Option, + pub name: FieldType, + + /// The names of the phantom fields. + pub phantom_names: FieldsType, /// The type of the field. pub ty: venial::TypeExpr, @@ -33,34 +49,99 @@ impl NewtypeStruct { "GodotConvert expects a struct with a single field, unit structs are currently not supported" ), venial::Fields::Tuple(fields) => { - if fields.fields.len() != 1 { + fn phantom_predicate(field: &TupleField) -> bool { + // Some types we don't care about are not paths, like references + if let Some(path) = field.ty.as_path() { + // This unwrap only fails if the field had no type specified, which isn't valid code anyways. + return path.segments.last().unwrap().ident + == Ident::new("PhantomData", Span::mixed_site()); + } + false + } + + let mut non_phantom_fields = fields + .fields + .items() + .enumerate() + .filter(|(_, field)| !phantom_predicate(field)); + + let maybe_field = non_phantom_fields.next(); + + let total_count = if maybe_field.is_none() { + 0 + } else { + non_phantom_fields.count() + 1 + }; + + if total_count != 1 { return bail!( &fields.fields, - "GodotConvert expects a struct with a single field, not {} fields", - fields.fields.len() + "GodotConvert expects a struct with a single non-PhantomData field, not {} fields", + total_count ); } - let (field, _) = fields.fields[0].clone(); + let (field_num, field) = maybe_field.unwrap(); + + let phantom_nums = (0..field_num) + .chain(field_num + 1..fields.fields.len()) + .map(Literal::usize_unsuffixed) + .collect(); Ok(NewtypeStruct { - name: None, - ty: field.ty, + name: FieldType::Tuple(Literal::usize_unsuffixed(field_num)), + phantom_names: FieldsType::Tuple(phantom_nums), + ty: field.ty.clone(), }) } venial::Fields::Named(fields) => { - if fields.fields.len() != 1 { + fn phantom_predicate(field: &NamedField) -> bool { + // Some types we don't care about are not paths, like references + if let Some(path) = field.ty.as_path() { + // This unwrap only fails if the field had no type specified, which isn't valid code anyways. + return path.segments.last().unwrap().ident + == Ident::new("PhantomData", Span::mixed_site()); + } + false + } + + let mut non_phantom_fields = fields + .fields + .items() + .filter(|field| !phantom_predicate(field)); + + let maybe_field = non_phantom_fields.next(); + + let total_count = if maybe_field.is_none() { + 0 + } else { + non_phantom_fields.count() + 1 + }; + + if total_count != 1 { return bail!( &fields.fields, - "GodotConvert expects a struct with a single field, not {} fields", - fields.fields.len() + "GodotConvert expects a struct with a single non-PhantomData field, not {} fields", + total_count ); } - let (field, _) = fields.fields[0].clone(); + let field = maybe_field.unwrap().clone(); + + let phantom_names = fields + .fields + .items() + .filter_map(|field| { + if phantom_predicate(field) { + return Some(field.name.clone()); + } + None + }) + .collect(); Ok(NewtypeStruct { - name: Some(field.name), + name: FieldType::Named(field.name), + phantom_names: FieldsType::Named(phantom_names), ty: field.ty, }) } @@ -69,7 +150,7 @@ impl NewtypeStruct { /// Gets the field name. /// - /// If this represents a tuple-struct, then it will return `0`. This can be used just like it was a named field with the name `0`. + /// If this represents a tuple-struct, then it will return a number. This can be used just like it was a named field. /// For instance: /// ``` /// struct Foo(i64); @@ -80,8 +161,18 @@ impl NewtypeStruct { /// ``` pub fn field_name(&self) -> TokenStream { match &self.name { - Some(name) => quote! { #name }, - None => quote! { 0 }, + FieldType::Named(name) => quote! { #name }, + FieldType::Tuple(num) => quote! { #num }, + } + } + + /// Gets the phantom field names. + /// + /// If this represents a tuple-struct, then it will return numbers. See `Self::field_name` + pub fn phantom_field_names(&self) -> Vec { + match &self.phantom_names { + FieldsType::Named(vec) => vec.iter().map(|ident| quote! {#ident}).collect(), + FieldsType::Tuple(vec) => vec.iter().map(|ident| quote! {#ident}).collect(), } } } diff --git a/godot-macros/src/derive/derive_from_godot.rs b/godot-macros/src/derive/derive_from_godot.rs index 53d8184f9..cbc5bd9ba 100644 --- a/godot-macros/src/derive/derive_from_godot.rs +++ b/godot-macros/src/derive/derive_from_godot.rs @@ -19,10 +19,11 @@ pub fn make_fromgodot(convert: &GodotConvert, cache: &mut EnumeratorExprCache) - let GodotConvert { ty_name: name, convert_type: data, + .. } = convert; match data { - ConvertType::NewType { field } => make_fromgodot_for_newtype_struct(name, field), + ConvertType::NewType { field } => make_fromgodot_for_newtype_struct(convert, field), ConvertType::Enum { variants, @@ -37,16 +38,29 @@ pub fn make_fromgodot(convert: &GodotConvert, cache: &mut EnumeratorExprCache) - } /// Derives `FromGodot` for newtype structs. -fn make_fromgodot_for_newtype_struct(name: &Ident, field: &NewtypeStruct) -> TokenStream { +fn make_fromgodot_for_newtype_struct(convert: &GodotConvert, field: &NewtypeStruct) -> TokenStream { // For tuple structs this ends up using the alternate tuple-struct constructor syntax of // TupleStruct { 0: value } + let GodotConvert { + ty_name: name, + generic_params, + where_clause, + .. + } = convert; + let generic_args = generic_params + .as_ref() + .map(|params| params.as_inline_args()); let field_name = field.field_name(); + let phantom_field_names = field.phantom_field_names(); let via_type = &field.ty; quote! { - impl ::godot::meta::FromGodot for #name { + impl #generic_params ::godot::meta::FromGodot for #name #generic_args #where_clause { fn try_from_godot(via: #via_type) -> ::std::result::Result { - Ok(Self { #field_name: via }) + Ok(Self { + #field_name: via, + #(#phantom_field_names: ::std::marker::PhantomData),* + }) } } } diff --git a/godot-macros/src/derive/derive_godot_convert.rs b/godot-macros/src/derive/derive_godot_convert.rs index 1db83751b..570f91dce 100644 --- a/godot-macros/src/derive/derive_godot_convert.rs +++ b/godot-macros/src/derive/derive_godot_convert.rs @@ -7,8 +7,9 @@ use std::collections::HashMap; -use proc_macro2::{Ident, TokenStream, TokenTree}; +use proc_macro2::{Ident, Punct, Spacing, TokenStream, TokenTree}; use quote::quote; +use venial::GenericBound; use crate::ParseResult; use crate::derive::data_models::{ConvertType, GodotConvert, ViaType}; @@ -20,7 +21,42 @@ use crate::derive::{make_fromgodot, make_togodot}; pub fn derive_godot_convert(item: venial::Item) -> ParseResult { let convert = GodotConvert::parse_declaration(item)?; - let name = &convert.ty_name; + let GodotConvert { + ty_name: name, + generic_params, + where_clause, + .. + } = &convert; + + let generic_args = generic_params + .as_ref() + .map(|params| params.as_inline_args()); + let element_generic_params = { + let mut maybe_params = generic_params.clone(); + + if let Some(params) = &mut maybe_params { + for (param, _) in params.params.iter_mut() { + if param.is_ty() || param.is_lifetime() { + if let Some(bound) = &mut param.bound { + // We have at least 1 bound, and rust doesn't error if the bound is already 'static, i.e. `T: 'static + 'static` works. + // If it were to error, we would have to inspect the bounds to make sure the tokens `+ 'static` are valid. It feels a + // little hacky, but there's not really a reason to inspect all of the bound's tokens if rust doesn't really care what + // they are. + bound + .tokens + .append(&mut quote! {+ 'static}.into_iter().collect()); + } else { + param.bound = Some(GenericBound { + tk_colon: Punct::new(':', Spacing::Alone), + tokens: quote! {'static}.into_iter().collect(), + }); + } + } + } + } + maybe_params + }; + let via_type = convert.convert_type.via_type(); let mut cache = EnumeratorExprCache::default(); @@ -30,7 +66,7 @@ pub fn derive_godot_convert(item: venial::Item) -> ParseResult { let shape_override = make_shape_override(&convert.convert_type, &mut cache); Ok(quote! { - impl ::godot::meta::GodotConvert for #name { + impl #generic_params ::godot::meta::GodotConvert for #name #generic_args #where_clause { type Via = #via_type; #shape_override } @@ -39,7 +75,7 @@ pub fn derive_godot_convert(item: venial::Item) -> ParseResult { #from_godot_impl // Marker impl: defaults derive element metadata from shape(). - impl ::godot::meta::Element for #name {} + impl #element_generic_params ::godot::meta::Element for #name #generic_args #where_clause {} }) } diff --git a/godot-macros/src/derive/derive_to_godot.rs b/godot-macros/src/derive/derive_to_godot.rs index 616287f96..36b3d7d5a 100644 --- a/godot-macros/src/derive/derive_to_godot.rs +++ b/godot-macros/src/derive/derive_to_godot.rs @@ -18,10 +18,11 @@ pub fn make_togodot(convert: &GodotConvert, cache: &mut EnumeratorExprCache) -> let GodotConvert { ty_name: name, convert_type: data, + .. } = convert; match data { - ConvertType::NewType { field } => make_togodot_for_newtype_struct(name, field), + ConvertType::NewType { field } => make_togodot_for_newtype_struct(convert, field), ConvertType::Enum { variants, @@ -36,12 +37,22 @@ pub fn make_togodot(convert: &GodotConvert, cache: &mut EnumeratorExprCache) -> } /// Derives `ToGodot` for newtype structs. -fn make_togodot_for_newtype_struct(name: &Ident, field: &NewtypeStruct) -> TokenStream { +fn make_togodot_for_newtype_struct(convert: &GodotConvert, field: &NewtypeStruct) -> TokenStream { + let GodotConvert { + ty_name: name, + generic_params, + where_clause, + .. + } = convert; + + let generic_args = generic_params + .as_ref() + .map(|params| params.as_inline_args()); let field_name = field.field_name(); let via_type = &field.ty; quote! { - impl ::godot::meta::ToGodot for #name { + impl #generic_params ::godot::meta::ToGodot for #name #generic_args #where_clause { type Pass = <#via_type as ::godot::meta::ToGodot>::Pass; fn to_godot(&self) -> ::godot::meta::ToArg<'_, Self::Via, Self::Pass> { diff --git a/godot-macros/src/lib.rs b/godot-macros/src/lib.rs index 0d9ef6a2e..d273a76d4 100644 --- a/godot-macros/src/lib.rs +++ b/godot-macros/src/lib.rs @@ -1366,15 +1366,36 @@ pub fn godot_dyn(_meta: TokenStream, input: TokenStream) -> TokenStream { /// assert_eq!(obj.to_godot(), &GString::from("hello!")); /// ``` /// -/// However, it will not work for structs with more than one field, even if that field is zero sized: +/// It will not work for structs with more than one field, unless the extra fields are `PhantomData` /// ```compile_fail /// use godot::prelude::*; +/// use std::marker::PhantomData; /// +/// // This will not compile /// #[derive(GodotConvert)] /// #[godot(transparent)] -/// struct SomeNewtype { +/// struct SomeNewtype1 { /// int: i64, -/// zst: (), +/// bool: bool, +/// } +/// +/// // This will compile +/// #[derive(GodotConvert)] +/// #[godot(transparent)] +/// struct SomeNewtype2 { +/// int: i64, +/// _marker: PhantomData, +/// } +/// ``` +/// +/// This is useful for cases where you want to have generics in Rust, but you still want to use that struct from Godot. For example, you have a +/// key `Key` to a registry `Registry` that contains `T`. +/// ```rs +/// #[derive(GodotConvert)] +/// #[godot(transparent)] +/// struct Key { +/// id: u32, +/// _marker: PhantomData, /// } /// ``` /// diff --git a/itest/rust/src/register_tests/derive_godotconvert_test.rs b/itest/rust/src/register_tests/derive_godotconvert_test.rs index be81c2db5..44eafb321 100644 --- a/itest/rust/src/register_tests/derive_godotconvert_test.rs +++ b/itest/rust/src/register_tests/derive_godotconvert_test.rs @@ -6,6 +6,7 @@ */ use std::fmt::Debug; +use std::marker::PhantomData; use godot::builtin::{GString, Vector2, array, dict}; use godot::meta::{GodotConvert, ToGodot}; @@ -16,16 +17,31 @@ use crate::framework::itest; // ---------------------------------------------------------------------------------------------------------------------------------------------- // General FromGodot/ToGodot derive tests +// #[derive(PartialEq, Debug)] #[derive(GodotConvert, PartialEq, Debug)] #[godot(transparent)] struct TupleNewtype(GString); +#[derive(GodotConvert, PartialEq, Debug)] +#[godot(transparent)] +struct TuplePhantomNewtype( + PhantomData, + godot::prelude::Array, +); + #[derive(GodotConvert, PartialEq, Debug)] #[godot(transparent)] struct NamedNewtype { field1: Vector2, } +#[derive(GodotConvert, PartialEq, Debug)] +#[godot(transparent)] +struct NamedPhantomNewtype { + field1: Vector2, + _marker: std::marker::PhantomData, +} + #[derive(GodotConvert, Clone, PartialEq, Debug)] #[godot(via = GString)] enum EnumStringy { @@ -58,6 +74,7 @@ enum EnumIntyWithExprs { #[itest] fn newtype_tuple_struct() { roundtrip(TupleNewtype(GString::from("hello!"))); + roundtrip(TuplePhantomNewtype::(PhantomData, array![])) } #[itest] @@ -65,6 +82,10 @@ fn newtype_named_struct() { roundtrip(NamedNewtype { field1: Vector2::new(10.0, 25.0), }); + roundtrip(NamedPhantomNewtype:: { + field1: Vector2::new(10.0, 25.0), + _marker: PhantomData, + }); } #[itest]