diff --git a/gdnative-core/src/export/property/hint.rs b/gdnative-core/src/export/property/hint.rs index 8dbd28365..41d6cc8a7 100644 --- a/gdnative-core/src/export/property/hint.rs +++ b/gdnative-core/src/export/property/hint.rs @@ -1,6 +1,6 @@ //! Strongly typed property hints. -use std::fmt::{self, Write}; +use std::fmt::{self, Display, Write}; use std::ops::RangeInclusive; use crate::core_types::GodotString; @@ -116,20 +116,26 @@ where /// ``` #[derive(Clone, Eq, PartialEq, Debug, Default)] pub struct EnumHint { - values: Vec, + entries: Vec, } impl EnumHint { #[inline] - pub fn new(values: Vec) -> Self { - EnumHint { values } + pub fn new(keys: Vec) -> Self { + let entries = keys.into_iter().map(EnumHintEntry::new).collect(); + EnumHint { entries } + } + + #[inline] + pub fn with_entries(entries: Vec) -> Self { + EnumHint { entries } } /// Formats the hint as a Godot hint string. fn to_godot_hint_string(&self) -> GodotString { let mut s = String::new(); - let mut iter = self.values.iter(); + let mut iter = self.entries.iter(); if let Some(first) = iter.next() { write!(s, "{first}").unwrap(); @@ -143,6 +149,38 @@ impl EnumHint { } } +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct EnumHintEntry { + key: String, + value: Option, +} + +impl EnumHintEntry { + #[inline] + pub fn new(key: String) -> Self { + Self { key, value: None } + } + + #[inline] + pub fn with_value(key: String, value: i64) -> Self { + Self { + key, + value: Some(value), + } + } +} + +impl Display for EnumHintEntry { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.key)?; + if let Some(value) = self.value { + write!(f, ":{}", value)?; + } + Ok(()) + } +} + /// Possible hints for integers. #[derive(Clone, Debug)] #[non_exhaustive] @@ -469,3 +507,16 @@ impl ArrayHint { } } } + +godot_test!(test_enum_hint_without_mapping { + let hint = EnumHint::new(vec!["Foo".into(), "Bar".into()]); + assert_eq!(hint.to_godot_hint_string().to_string(), "Foo,Bar".to_string(),); +}); + +godot_test!(test_enum_hint_with_mapping { + let hint = EnumHint::with_entries(vec![ + EnumHintEntry::with_value("Foo".to_string(), 42), + EnumHintEntry::with_value("Bar".to_string(), 67), + ]); + assert_eq!(hint.to_godot_hint_string().to_string(), "Foo:42,Bar:67".to_string(),); +}); diff --git a/gdnative-derive/src/export.rs b/gdnative-derive/src/export.rs new file mode 100644 index 000000000..537586f6a --- /dev/null +++ b/gdnative-derive/src/export.rs @@ -0,0 +1,177 @@ +use crate::crate_gdnative_core; +use proc_macro2::{Ident, Span, TokenStream as TokenStream2}; +use quote::ToTokens; +use syn::spanned::Spanned; +use syn::{DeriveInput, Fields, Meta}; + +#[derive(Copy, Clone, Debug)] +enum Kind { + Enum, +} + +#[derive(Debug)] +struct DeriveData { + kind: Kind, + ident: Ident, + data: syn::Data, +} + +fn parse_derive_input(input: DeriveInput) -> syn::Result { + let DeriveInput { + ident, data, attrs, .. + } = input.clone(); + + let (kind, errors) = attrs + .iter() + .filter(|attr| attr.path.is_ident("export")) + .fold((None, vec![]), |(mut kind, mut errors), attr| { + let list = match attr.parse_meta() { + Ok(meta) => match meta { + Meta::List(list) => list, + Meta::Path(path) => { + errors.push(syn::Error::new( + path.span(), + "missing macro arguments. expected #[export(...)]", + )); + return (kind, errors); + } + Meta::NameValue(pair) => { + errors.push(syn::Error::new( + pair.span(), + "missing macro arguments. expected #[export(...)]", + )); + return (kind, errors); + } + }, + Err(e) => { + errors.push(syn::Error::new( + e.span(), + format!("unknown attribute format. expected #[export(...)]: {e}"), + )); + return (kind, errors); + } + }; + + for meta in list.nested.into_iter() { + let syn::NestedMeta::Meta(Meta::NameValue(pair)) = meta else { + errors.push(syn::Error::new( + meta.span(), + "invalid syntax. expected #[export(key = \"value\")]", + )); + continue; + }; + + if !pair.path.is_ident("kind") { + errors.push(syn::Error::new( + pair.span(), + format!("found {}, expected kind", pair.path.into_token_stream()), + )); + continue; + } + + let syn::Lit::Str(str) = pair.lit else { + errors.push(syn::Error::new( + pair.lit.span(), + "string literal expected, wrap with double quotes", + )); + continue; + }; + + match str.value().as_str() { + "enum" => { + if kind.is_some() { + errors.push(syn::Error::new(str.span(), "kind already set")); + } else { + kind = Some(Kind::Enum); + } + } + _ => { + errors.push(syn::Error::new(str.span(), "unknown kind, expected enum")); + } + } + } + + (kind, errors) + }); + + if let Some(err) = errors.into_iter().reduce(|mut acc, err| { + acc.combine(err); + acc + }) { + return Err(err); + } + + match kind { + Some(kind) => Ok(DeriveData { ident, kind, data }), + None => Err(syn::Error::new(Span::call_site(), "kind not found")), + } +} + +fn err_only_supports_fieldless_enums(span: Span) -> syn::Error { + syn::Error::new(span, "#[derive(Export)] only supports fieldless enums") +} + +pub(crate) fn derive_export(input: DeriveInput) -> syn::Result { + let derive_data = parse_derive_input(input)?; + + match derive_data.kind { + Kind::Enum => { + let derived_enum = match derive_data.data { + syn::Data::Enum(data) => data, + syn::Data::Struct(data) => { + return Err(err_only_supports_fieldless_enums(data.struct_token.span())); + } + syn::Data::Union(data) => { + return Err(err_only_supports_fieldless_enums(data.union_token.span())); + } + }; + let export_impl = impl_export(&derive_data.ident, &derived_enum)?; + Ok(export_impl) + } + } +} + +fn impl_export(enum_ty: &syn::Ident, data: &syn::DataEnum) -> syn::Result { + let err = data + .variants + .iter() + .filter(|variant| !matches!(variant.fields, Fields::Unit)) + .map(|variant| err_only_supports_fieldless_enums(variant.ident.span())) + .reduce(|mut acc, err| { + acc.combine(err); + acc + }); + if let Some(err) = err { + return Err(err); + } + + let gdnative_core = crate_gdnative_core(); + let mappings = data + .variants + .iter() + .map(|variant| { + let key = &variant.ident; + let val = quote! { #enum_ty::#key as i64 }; + quote! { #gdnative_core::export::hint::EnumHintEntry::with_value(stringify!(#key).to_string(), #val) } + }) + .collect::>(); + + let impl_block = quote! { + const _: () = { + pub enum NoHint {} + + impl #gdnative_core::export::Export for #enum_ty { + type Hint = NoHint; + + #[inline] + fn export_info(_hint: Option) -> #gdnative_core::export::ExportInfo { + let mappings = vec![ #(#mappings),* ]; + let enum_hint = #gdnative_core::export::hint::EnumHint::with_entries(mappings); + return #gdnative_core::export::hint::IntHint::::Enum(enum_hint).export_info(); + } + } + }; + }; + + Ok(impl_block) +} diff --git a/gdnative-derive/src/lib.rs b/gdnative-derive/src/lib.rs index a71625381..2d4138879 100644 --- a/gdnative-derive/src/lib.rs +++ b/gdnative-derive/src/lib.rs @@ -10,6 +10,7 @@ use proc_macro2::TokenStream as TokenStream2; use quote::ToTokens; use syn::{parse::Parser, AttributeArgs, DeriveInput, ItemFn, ItemImpl, ItemType}; +mod export; mod init; mod methods; mod native_script; @@ -663,6 +664,64 @@ pub fn godot_wrap_method(input: TokenStream) -> TokenStream { } } +/// Make a rust `enum` has drop-down list in Godot editor. +/// Note that the derived `enum` should also implements `Copy` trait. +/// +/// Take the following example, you will see a drop-down list for the `dir` +/// property, and `Up` and `Down` converts to `1` and `-1` in the GDScript +/// side. +/// +/// ``` +/// use gdnative::prelude::*; +/// +/// #[derive(Debug, PartialEq, Clone, Copy, Export, ToVariant, FromVariant)] +/// #[variant(enum = "repr")] +/// #[export(kind = "enum")] +/// #[repr(i32)] +/// enum Dir { +/// Up = 1, +/// Down = -1, +/// } +/// +/// #[derive(NativeClass)] +/// #[no_constructor] +/// struct Move { +/// #[property] +/// pub dir: Dir, +/// } +/// ``` +/// +/// You can't derive `Export` on `enum` that has non-unit variant. +/// +/// ```compile_fail +/// use gdnative::prelude::*; +/// +/// #[derive(Debug, PartialEq, Clone, Copy, Export)] +/// enum Action { +/// Move((f32, f32, f32)), +/// Attack(u64), +/// } +/// ``` +/// +/// You can't derive `Export` on `struct` or `union`. +/// +/// ```compile_fail +/// use gdnative::prelude::*; +/// +/// #[derive(Export)] +/// struct Foo { +/// f1: i32 +/// } +/// ``` +#[proc_macro_derive(Export, attributes(export))] +pub fn derive_export(input: TokenStream) -> TokenStream { + let derive_input = syn::parse_macro_input!(input as syn::DeriveInput); + match export::derive_export(derive_input) { + Ok(stream) => stream.into(), + Err(err) => err.to_compile_error().into(), + } +} + /// Returns a standard header for derived implementations. /// /// Adds the `automatically_derived` attribute and prevents common lints from triggering diff --git a/gdnative/tests/ui.rs b/gdnative/tests/ui.rs index 859d569c9..bfd7ef401 100644 --- a/gdnative/tests/ui.rs +++ b/gdnative/tests/ui.rs @@ -40,6 +40,10 @@ fn ui_tests() { t.compile_fail("tests/ui/from_variant_fail_07.rs"); t.compile_fail("tests/ui/from_variant_fail_08.rs"); t.compile_fail("tests/ui/from_variant_fail_09.rs"); + + // Export + t.pass("tests/ui/export_pass.rs"); + t.compile_fail("tests/ui/export_fail_*.rs"); } // FIXME(rust/issues/54725): Full path spans are only available on nightly as of now diff --git a/gdnative/tests/ui/export_fail_01.rs b/gdnative/tests/ui/export_fail_01.rs new file mode 100644 index 000000000..0a2ea147a --- /dev/null +++ b/gdnative/tests/ui/export_fail_01.rs @@ -0,0 +1,10 @@ +use gdnative::prelude::*; + +#[derive(Export, ToVariant)] +#[export(kind = "enum")] +pub enum Foo { + Bar(String), + Baz { a: i32, b: u32 }, +} + +fn main() {} diff --git a/gdnative/tests/ui/export_fail_01.stderr b/gdnative/tests/ui/export_fail_01.stderr new file mode 100644 index 000000000..2848a43c4 --- /dev/null +++ b/gdnative/tests/ui/export_fail_01.stderr @@ -0,0 +1,11 @@ +error: #[derive(Export)] only supports fieldless enums + --> tests/ui/export_fail_01.rs:6:5 + | +6 | Bar(String), + | ^^^ + +error: #[derive(Export)] only supports fieldless enums + --> tests/ui/export_fail_01.rs:7:5 + | +7 | Baz { a: i32, b: u32 }, + | ^^^ diff --git a/gdnative/tests/ui/export_fail_02.rs b/gdnative/tests/ui/export_fail_02.rs new file mode 100644 index 000000000..fa1c82498 --- /dev/null +++ b/gdnative/tests/ui/export_fail_02.rs @@ -0,0 +1,9 @@ +use gdnative::prelude::*; + +#[derive(Export, ToVariant)] +#[export(kind = "enum")] +pub struct Foo { + bar: i32, +} + +fn main() {} diff --git a/gdnative/tests/ui/export_fail_02.stderr b/gdnative/tests/ui/export_fail_02.stderr new file mode 100644 index 000000000..64b5e1656 --- /dev/null +++ b/gdnative/tests/ui/export_fail_02.stderr @@ -0,0 +1,5 @@ +error: #[derive(Export)] only supports fieldless enums + --> tests/ui/export_fail_02.rs:5:5 + | +5 | pub struct Foo { + | ^^^^^^ diff --git a/gdnative/tests/ui/export_fail_03.rs b/gdnative/tests/ui/export_fail_03.rs new file mode 100644 index 000000000..4641ffe2e --- /dev/null +++ b/gdnative/tests/ui/export_fail_03.rs @@ -0,0 +1,9 @@ +use gdnative::prelude::*; + +#[derive(Export, ToVariant)] +#[export(kind = "enum")] +pub union Foo { + bar: i32, +} + +fn main() {} diff --git a/gdnative/tests/ui/export_fail_03.stderr b/gdnative/tests/ui/export_fail_03.stderr new file mode 100644 index 000000000..b9184f792 --- /dev/null +++ b/gdnative/tests/ui/export_fail_03.stderr @@ -0,0 +1,11 @@ +error: #[derive(Export)] only supports fieldless enums + --> tests/ui/export_fail_03.rs:5:5 + | +5 | pub union Foo { + | ^^^^^ + +error: Variant conversion derive macro does not work on unions. + --> tests/ui/export_fail_03.rs:4:1 + | +4 | #[export(kind = "enum")] + | ^ diff --git a/gdnative/tests/ui/export_fail_04.rs b/gdnative/tests/ui/export_fail_04.rs new file mode 100644 index 000000000..885285277 --- /dev/null +++ b/gdnative/tests/ui/export_fail_04.rs @@ -0,0 +1,21 @@ +use gdnative::prelude::*; + +#[derive(Export, ToVariant)] +#[export] +pub enum Foo { + Bar, +} + +#[derive(Export, ToVariant)] +#[export = "foo"] +pub enum Bar { + Foo, +} + +#[derive(Export, ToVariant)] +#[export(weird format a => b)] +pub enum Baz { + Quux, +} + +fn main() {} diff --git a/gdnative/tests/ui/export_fail_04.stderr b/gdnative/tests/ui/export_fail_04.stderr new file mode 100644 index 000000000..caa03b0d2 --- /dev/null +++ b/gdnative/tests/ui/export_fail_04.stderr @@ -0,0 +1,17 @@ +error: missing macro arguments. expected #[export(...)] + --> tests/ui/export_fail_04.rs:4:3 + | +4 | #[export] + | ^^^^^^ + +error: missing macro arguments. expected #[export(...)] + --> tests/ui/export_fail_04.rs:10:3 + | +10 | #[export = "foo"] + | ^^^^^^ + +error: unknown attribute format. expected #[export(...)]: expected `,` + --> tests/ui/export_fail_04.rs:16:16 + | +16 | #[export(weird format a => b)] + | ^^^^^^ diff --git a/gdnative/tests/ui/export_fail_05.rs b/gdnative/tests/ui/export_fail_05.rs new file mode 100644 index 000000000..ff3a5f39d --- /dev/null +++ b/gdnative/tests/ui/export_fail_05.rs @@ -0,0 +1,9 @@ +use gdnative::prelude::*; + +#[derive(Export, ToVariant)] +#[export(kind)] +pub enum Foo { + Bar, +} + +fn main() {} diff --git a/gdnative/tests/ui/export_fail_05.stderr b/gdnative/tests/ui/export_fail_05.stderr new file mode 100644 index 000000000..752be2a32 --- /dev/null +++ b/gdnative/tests/ui/export_fail_05.stderr @@ -0,0 +1,5 @@ +error: invalid syntax. expected #[export(key = "value")] + --> tests/ui/export_fail_05.rs:4:10 + | +4 | #[export(kind)] + | ^^^^ diff --git a/gdnative/tests/ui/export_fail_06.rs b/gdnative/tests/ui/export_fail_06.rs new file mode 100644 index 000000000..0413ea008 --- /dev/null +++ b/gdnative/tests/ui/export_fail_06.rs @@ -0,0 +1,9 @@ +use gdnative::prelude::*; + +#[derive(Export, ToVariant)] +#[export(kinb = "enum")] +pub enum Foo { + Bar, +} + +fn main() {} diff --git a/gdnative/tests/ui/export_fail_06.stderr b/gdnative/tests/ui/export_fail_06.stderr new file mode 100644 index 000000000..1105998c5 --- /dev/null +++ b/gdnative/tests/ui/export_fail_06.stderr @@ -0,0 +1,5 @@ +error: found kinb, expected kind + --> tests/ui/export_fail_06.rs:4:10 + | +4 | #[export(kinb = "enum")] + | ^^^^ diff --git a/gdnative/tests/ui/export_fail_07.rs b/gdnative/tests/ui/export_fail_07.rs new file mode 100644 index 000000000..954c3a3ac --- /dev/null +++ b/gdnative/tests/ui/export_fail_07.rs @@ -0,0 +1,9 @@ +use gdnative::prelude::*; + +#[derive(Export, ToVariant)] +#[export(kind = 123)] +pub enum Foo { + Bar, +} + +fn main() {} diff --git a/gdnative/tests/ui/export_fail_07.stderr b/gdnative/tests/ui/export_fail_07.stderr new file mode 100644 index 000000000..ebf80930a --- /dev/null +++ b/gdnative/tests/ui/export_fail_07.stderr @@ -0,0 +1,5 @@ +error: string literal expected, wrap with double quotes + --> tests/ui/export_fail_07.rs:4:17 + | +4 | #[export(kind = 123)] + | ^^^ diff --git a/gdnative/tests/ui/export_fail_08.rs b/gdnative/tests/ui/export_fail_08.rs new file mode 100644 index 000000000..dda42bc8a --- /dev/null +++ b/gdnative/tests/ui/export_fail_08.rs @@ -0,0 +1,9 @@ +use gdnative::prelude::*; + +#[derive(Export, ToVariant)] +#[export(kind = "foo")] +pub enum Foo { + Bar, +} + +fn main() {} diff --git a/gdnative/tests/ui/export_fail_08.stderr b/gdnative/tests/ui/export_fail_08.stderr new file mode 100644 index 000000000..ce38e6529 --- /dev/null +++ b/gdnative/tests/ui/export_fail_08.stderr @@ -0,0 +1,5 @@ +error: unknown kind, expected enum + --> tests/ui/export_fail_08.rs:4:17 + | +4 | #[export(kind = "foo")] + | ^^^^^ diff --git a/gdnative/tests/ui/export_fail_09.rs b/gdnative/tests/ui/export_fail_09.rs new file mode 100644 index 000000000..6077c5b90 --- /dev/null +++ b/gdnative/tests/ui/export_fail_09.rs @@ -0,0 +1,10 @@ +use gdnative::prelude::*; + +#[derive(Export, ToVariant)] +#[export(kind = "enum")] +#[export(kind = "enum")] +pub enum Foo { + Bar, +} + +fn main() {} diff --git a/gdnative/tests/ui/export_fail_09.stderr b/gdnative/tests/ui/export_fail_09.stderr new file mode 100644 index 000000000..73bd44a7c --- /dev/null +++ b/gdnative/tests/ui/export_fail_09.stderr @@ -0,0 +1,5 @@ +error: kind already set + --> tests/ui/export_fail_09.rs:5:17 + | +5 | #[export(kind = "enum")] + | ^^^^^^ diff --git a/gdnative/tests/ui/export_pass.rs b/gdnative/tests/ui/export_pass.rs new file mode 100644 index 000000000..d417a105a --- /dev/null +++ b/gdnative/tests/ui/export_pass.rs @@ -0,0 +1,12 @@ +use gdnative::prelude::*; + +#[derive(Export, ToVariant, Clone, Copy)] +#[variant(enum = "repr")] +#[export(kind = "enum")] +#[repr(i32)] +pub enum Foo { + Bar, + Baz, +} + +fn main() {} diff --git a/test/src/lib.rs b/test/src/lib.rs index 92a0fbd2a..4858da0d4 100644 --- a/test/src/lib.rs +++ b/test/src/lib.rs @@ -28,6 +28,9 @@ pub extern "C" fn run_tests( status &= gdnative::core_types::test_core_types(); + status &= gdnative::export::hint::test_enum_hint_without_mapping(); + status &= gdnative::export::hint::test_enum_hint_with_mapping(); + status &= test_from_instance_id(); status &= test_nil_object_return_value(); status &= test_rust_class_construction();