Skip to content

Commit 1460960

Browse files
committed
More complete #[export] implementation
- Rename arguments `getter` and `setter` to `get` and `set` to stay closer to GDScript and save keystrokes. - Allow `get` and `set` without arguments to generate the corresponding function. - Assume generated `get` and `set` if neither has been given. - Check at compile time that the referenced getter and setter actually exist (otherwise Godot gives a cryptic "invalid get/set index" error). - Remove unused `property` argument. - Add documentation for `GodotClass` derive macro.
1 parent 6bfe53e commit 1460960

File tree

6 files changed

+374
-86
lines changed

6 files changed

+374
-86
lines changed

godot-core/src/obj/traits.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ use godot_ffi as sys;
1111
/// Makes `T` eligible to be managed by Godot and stored in [`Gd<T>`][crate::obj::Gd] pointers.
1212
///
1313
/// The behavior of types implementing this trait is influenced by the associated types; check their documentation for information.
14+
///
15+
/// You wouldn't usually implement this trait yourself; use the [`GodotClass`](godot_macros::GodotClass) derive macro instead.
1416
pub trait GodotClass: 'static
1517
where
1618
Self: Sized,

godot-macros/src/derive_godot_class.rs

Lines changed: 152 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
55
*/
66

7-
use crate::util::{bail, ident, KvParser};
7+
use crate::util::{bail, ident, string_lit_contents, KvParser, KvValue};
88
use crate::ParseResult;
99
use proc_macro2::{Ident, Punct, TokenStream};
1010
use quote::{format_ident, quote};
@@ -68,7 +68,7 @@ fn parse_struct_attributes(class: &Struct) -> ParseResult<ClassAttributes> {
6868
let mut base_ty = ident("RefCounted");
6969
let mut has_generated_init = false;
7070

71-
// #[func] attribute on struct
71+
// #[class] attribute on struct
7272
if let Some(mut parser) = KvParser::parse(&class.attributes, "class")? {
7373
if let Some(base) = parser.handle_ident("base")? {
7474
base_ty = base;
@@ -174,11 +174,45 @@ impl Field {
174174

175175
struct ExportedField {
176176
field: Field,
177-
getter: String,
178-
setter: String,
177+
getter: GetterSetter,
178+
setter: GetterSetter,
179179
hint: Option<ExportHint>,
180180
}
181181

182+
#[derive(Clone, Debug, Eq, PartialEq)]
183+
enum GetterSetter {
184+
/// Getter/setter should be omitted, field is write/read only.
185+
Omitted,
186+
/// Trivial getter/setter should be autogenerated.
187+
Generated,
188+
/// Getter/setter is hand-written by the user, and here is its name.
189+
Custom(String),
190+
}
191+
192+
impl GetterSetter {
193+
fn parse(parser: &mut KvParser, key: &str) -> ParseResult<Self> {
194+
Ok(match parser.handle_any(key) {
195+
// No `get` argument
196+
None => GetterSetter::Omitted,
197+
// `get` without value
198+
Some(KvValue::None) => GetterSetter::Generated,
199+
// `get = literal`
200+
Some(KvValue::Lit(name_lit)) => {
201+
let Some(name) = string_lit_contents(&name_lit) else {
202+
return bail(format!("argument to {key} must be a string literal, got: {name_lit}"), parser.span());
203+
};
204+
GetterSetter::Custom(name)
205+
}
206+
Some(KvValue::Ident(ident)) => {
207+
return bail(
208+
format!("argument to {key} must be a string, got: {ident}"),
209+
parser.span(),
210+
);
211+
}
212+
})
213+
}
214+
}
215+
182216
#[derive(Clone)]
183217
struct ExportHint {
184218
hint_type: Ident,
@@ -196,8 +230,12 @@ impl ExportHint {
196230

197231
impl ExportedField {
198232
pub fn new_from_kv(field: Field, parser: &mut KvParser) -> ParseResult<ExportedField> {
199-
let getter = parser.handle_lit_required("getter")?;
200-
let setter = parser.handle_lit_required("setter")?;
233+
let mut getter = GetterSetter::parse(parser, "get")?;
234+
let mut setter = GetterSetter::parse(parser, "set")?;
235+
if getter == GetterSetter::Omitted && setter == GetterSetter::Omitted {
236+
getter = GetterSetter::Generated;
237+
setter = GetterSetter::Generated;
238+
}
201239

202240
let hint = parser
203241
.handle_ident("hint")?
@@ -265,51 +303,109 @@ fn make_deref_impl(class_name: &Ident, fields: &Fields) -> TokenStream {
265303
}
266304

267305
fn make_exports_impl(class_name: &Ident, fields: &Fields) -> TokenStream {
268-
let export_tokens = fields
269-
.exported_fields
270-
.iter()
271-
.map(|exported_field: &ExportedField| {
272-
use std::str::FromStr;
273-
let name = exported_field.field.name.to_string();
274-
let getter = proc_macro2::Literal::from_str(&exported_field.getter).unwrap();
275-
let setter = proc_macro2::Literal::from_str(&exported_field.setter).unwrap();
276-
let field_type = exported_field.field.ty.clone();
277-
278-
let ExportHint {
279-
hint_type,
280-
description,
281-
} = exported_field.hint.clone().unwrap_or_else(ExportHint::none);
282-
283-
// trims '"' and '\' from both ends of the hint description.
284-
let description = description.trim_matches(|c| c == '\\' || c == '"');
285-
286-
quote! {
287-
use ::godot::builtin::meta::VariantMetadata;
288-
289-
let class_name = ::godot::builtin::StringName::from(#class_name::CLASS_NAME);
290-
let property_info = ::godot::builtin::meta::PropertyInfo::new(
291-
<#field_type>::variant_type(),
292-
::godot::builtin::meta::ClassName::of::<#class_name>(),
293-
::godot::builtin::StringName::from(#name),
294-
::godot::engine::global::PropertyHint::#hint_type,
295-
GodotString::from(#description),
306+
let mut getter_setter_impls = Vec::new();
307+
let mut export_tokens = Vec::with_capacity(fields.exported_fields.len());
308+
309+
for exported_field in &fields.exported_fields {
310+
let field_name = exported_field.field.name.to_string();
311+
let field_ident = ident(&field_name);
312+
let field_type = exported_field.field.ty.clone();
313+
314+
let ExportHint {
315+
hint_type,
316+
description,
317+
} = exported_field.hint.clone().unwrap_or_else(ExportHint::none);
318+
319+
// trims '"' and '\' from both ends of the hint description.
320+
let description = description.trim_matches(|c| c == '\\' || c == '"');
321+
322+
let getter_name;
323+
match &exported_field.getter {
324+
GetterSetter::Omitted => {
325+
getter_name = "".to_owned();
326+
}
327+
GetterSetter::Generated => {
328+
getter_name = format!("get_{field_name}");
329+
let getter_ident = ident(&getter_name);
330+
let signature = quote! {
331+
fn #getter_ident(&self) -> #field_type
332+
};
333+
getter_setter_impls.push(quote! {
334+
pub #signature {
335+
self.#field_ident
336+
}
337+
});
338+
export_tokens.push(quote! {
339+
::godot::private::gdext_register_method!(#class_name, #signature);
340+
});
341+
}
342+
GetterSetter::Custom(name) => {
343+
getter_name = name.clone();
344+
let getter_ident = ident(&getter_name);
345+
export_tokens.push(make_existence_check(&getter_ident));
346+
}
347+
}
348+
349+
let setter_name;
350+
match &exported_field.setter {
351+
GetterSetter::Omitted => {
352+
setter_name = "".to_owned();
353+
}
354+
GetterSetter::Generated => {
355+
setter_name = format!("set_{field_name}");
356+
let setter_ident = ident(&setter_name);
357+
let signature = quote! {
358+
fn #setter_ident(&mut self, #field_ident: #field_type)
359+
};
360+
getter_setter_impls.push(quote! {
361+
pub #signature {
362+
self.#field_ident = #field_ident;
363+
}
364+
});
365+
export_tokens.push(quote! {
366+
::godot::private::gdext_register_method!(#class_name, #signature);
367+
});
368+
}
369+
GetterSetter::Custom(name) => {
370+
setter_name = name.clone();
371+
let setter_ident = ident(&setter_name);
372+
export_tokens.push(make_existence_check(&setter_ident));
373+
}
374+
};
375+
376+
export_tokens.push(quote! {
377+
use ::godot::builtin::meta::VariantMetadata;
378+
379+
let class_name = ::godot::builtin::StringName::from(#class_name::CLASS_NAME);
380+
381+
let property_info = ::godot::builtin::meta::PropertyInfo::new(
382+
<#field_type>::variant_type(),
383+
::godot::builtin::meta::ClassName::of::<#class_name>(),
384+
::godot::builtin::StringName::from(#field_name),
385+
::godot::engine::global::PropertyHint::#hint_type,
386+
::godot::builtin::GodotString::from(#description),
387+
);
388+
let property_info_sys = property_info.property_sys();
389+
390+
let getter_name = ::godot::builtin::StringName::from(#getter_name);
391+
let setter_name = ::godot::builtin::StringName::from(#setter_name);
392+
unsafe {
393+
::godot::sys::interface_fn!(classdb_register_extension_class_property)(
394+
::godot::sys::get_library(),
395+
class_name.string_sys(),
396+
std::ptr::addr_of!(property_info_sys),
397+
setter_name.string_sys(),
398+
getter_name.string_sys(),
296399
);
297-
let property_info_sys = property_info.property_sys();
298-
299-
let getter_string_name = ::godot::builtin::StringName::from(#getter);
300-
let setter_string_name = ::godot::builtin::StringName::from(#setter);
301-
unsafe {
302-
::godot::sys::interface_fn!(classdb_register_extension_class_property)(
303-
::godot::sys::get_library(),
304-
class_name.string_sys(),
305-
std::ptr::addr_of!(property_info_sys),
306-
setter_string_name.string_sys(),
307-
getter_string_name.string_sys(),
308-
);
309-
}
310400
}
311401
});
402+
}
403+
312404
quote! {
405+
impl #class_name {
406+
#(#getter_setter_impls)*
407+
}
408+
313409
impl ::godot::obj::cap::ImplementsGodotExports for #class_name {
314410
fn __register_exports() {
315411
#(
@@ -321,3 +417,12 @@ fn make_exports_impl(class_name: &Ident, fields: &Fields) -> TokenStream {
321417
}
322418
}
323419
}
420+
421+
/// Checks at compile time that a function with the given name exists on `Self`.
422+
#[must_use]
423+
fn make_existence_check(ident: &Ident) -> TokenStream {
424+
quote! {
425+
#[allow(path_statements)]
426+
Self::#ident;
427+
}
428+
}

godot-macros/src/lib.rs

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,137 @@ mod godot_api;
1616
mod itest;
1717
mod util;
1818

19-
#[proc_macro_derive(GodotClass, attributes(class, property, export, base, signal))]
19+
/// Derive macro for [`GodotClass`](godot_core::obj::GodotClass) on structs. You should normally use
20+
/// this macro, rather than implement `GodotClass` manually for your type.
21+
///
22+
/// # Construction
23+
///
24+
/// To generate a constructor that will let you call `MyStruct.new()` from GDScript, annotate your
25+
/// struct with `#[class(init)]`:
26+
///
27+
/// ```
28+
/// # use godot_macros::GodotClass;
29+
/// #[derive(GodotClass)]
30+
/// #[class(init)]
31+
/// struct MyStruct {
32+
/// // ...
33+
/// }
34+
/// ```
35+
///
36+
/// # Inheritance
37+
///
38+
/// Unlike C++, Rust doesn't really have inheritance, but the GDExtension API lets us "inherit"
39+
/// from a built-in engine class.
40+
///
41+
/// By default, classes created with this library inherit from `RefCounted`.
42+
///
43+
/// To specify a different class to inherit from, add `#[class(base = Base)]` as an annotation on
44+
/// your `struct`:
45+
///
46+
/// ```
47+
/// use godot::prelude::*;
48+
///
49+
/// #[derive(GodotClass)]
50+
/// #[class(base = Node2D)]
51+
/// struct MyStruct {
52+
/// // ...
53+
/// }
54+
/// ```
55+
///
56+
/// If you need a reference to the base class, you can add a field of type `Gd<Base>` and annotate
57+
/// it with `#[base]`:
58+
///
59+
/// ```
60+
/// use godot::prelude::*;
61+
///
62+
/// #[derive(GodotClass)]
63+
/// #[class(base = Node2D)]
64+
/// struct MyStruct {
65+
/// #[base]
66+
/// base: Gd<Node2D>,
67+
/// }
68+
/// ```
69+
///
70+
/// # Exported properties
71+
///
72+
/// In GDScript, there is a distinction between
73+
/// [properties](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_basics.html#properties-setters-and-getters)
74+
/// (fields with a `get` or `set` declaration) and
75+
/// [exports](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_exports.html)
76+
/// (fields annotated with `@export`). In the GDExtension API, these two concepts are merged into
77+
/// one.
78+
///
79+
/// You can export fields of your struct using the `#[export]` annotation:
80+
///
81+
/// ```
82+
/// use godot::prelude::*;
83+
///
84+
/// #[derive(GodotClass)]
85+
/// struct MyStruct {
86+
/// #[export]
87+
/// my_field: i64,
88+
/// }
89+
/// ```
90+
///
91+
/// This makes the field accessible in GDScript using `my_struct.my_field` syntax. Additionally, it
92+
/// generates a trivial getter and setter named `get_my_field` and `set_my_field`, respectively.
93+
/// These are `pub` in Rust, since they're exposed from GDScript anyway.
94+
///
95+
/// If you want to implement your own getter and/or setter, write those as a function on your Rust
96+
/// type, expose it using `#[func]`, and annotate the field with
97+
/// `#[export(get = "...", set = "...")]`:
98+
///
99+
/// ```
100+
/// use godot::prelude::*;
101+
///
102+
/// #[derive(GodotClass)]
103+
/// struct MyStruct {
104+
/// #[export(get = "get_my_field", set = "set_my_field")]
105+
/// my_field: i64,
106+
/// }
107+
///
108+
/// #[godot_api]
109+
/// impl MyStruct {
110+
/// #[func]
111+
/// pub fn get_my_field(&self) -> i64 {
112+
/// self.my_field
113+
/// }
114+
///
115+
/// #[func]
116+
/// pub fn set_my_field(&mut self, value: i64) {
117+
/// self.my_field = value;
118+
/// }
119+
/// }
120+
/// ```
121+
///
122+
/// If you specify only `get`, no setter is generated, making the field read-only. If you specify
123+
/// only `set`, no getter is generated, making the field write-only (rarely useful). To add a
124+
/// generated getter or setter in these cases anyway, use `get` or `set` without a value:
125+
///
126+
/// ```
127+
/// use godot::prelude::*;
128+
///
129+
/// #[derive(GodotClass)]
130+
/// struct MyStruct {
131+
/// // Default getter, custom setter.
132+
/// #[export(get, set = "set_my_field")]
133+
/// my_field: i64,
134+
/// }
135+
///
136+
/// #[godot_api]
137+
/// impl MyStruct {
138+
/// #[func]
139+
/// pub fn set_my_field(&mut self, value: i64) {
140+
/// self.my_field = value;
141+
/// }
142+
/// }
143+
/// ```
144+
///
145+
/// # Signals
146+
///
147+
/// The `#[signal]` attribute is accepted, but not yet implemented. See [issue
148+
/// #8](https://github.com/godot-rust/gdext/issues/8).
149+
#[proc_macro_derive(GodotClass, attributes(class, export, base, signal))]
20150
pub fn derive_native_class(input: TokenStream) -> TokenStream {
21151
translate(input, derive_godot_class::transform)
22152
}

0 commit comments

Comments
 (0)