Skip to content

Commit 667c78c

Browse files
bors[bot]ttencate
andauthored
Merge #177
177: More complete #[export] implementation r=Bromeon a=ttencate Also: - Remove unused `property` argument. - Rename arguments `getter` and `setter` to `get` and `set` to stay closer to GDScript and save keystrokes. - Check at compilation time that the referenced getter and setter actually exist (otherwise Godot gives a cryptic "invalid get/set index" error). See #3. TBD: - [ ] ~~`strip_quotes` should go away, not sure if it even works correctly if using e.g. raw string literals. Use an actual Rust parser? Omit the quotes from the argument instead, i.e. `get = get_my_field` instead of `get = "get_my_field"`?~~ See discussion below. - [ ] ~~Make `KvParser::parse` take a closure so we can check that all fields have been consumed~~ See discussion below. - [x] Omitting one of getter/setter should make field write/read only - [x] Use `get`/`set` without arguments to generate a default one - [x] Make generated getters and setters `pub` since they're public to Godot anyway Co-authored-by: Thomas ten Cate <ttencate@gmail.com>
2 parents 47027d3 + 1460960 commit 667c78c

File tree

6 files changed

+374
-86
lines changed

6 files changed

+374
-86
lines changed

godot-core/src/obj/traits.rs

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ use godot_ffi as sys;
1313
/// Makes `T` eligible to be managed by Godot and stored in [`Gd<T>`][crate::obj::Gd] pointers.
1414
///
1515
/// The behavior of types implementing this trait is influenced by the associated types; check their documentation for information.
16+
///
17+
/// You wouldn't usually implement this trait yourself; use the [`GodotClass`](godot_macros::GodotClass) derive macro instead.
1618
pub trait GodotClass: 'static
1719
where
1820
Self: Sized,

godot-macros/src/derive_godot_class.rs

+152-47
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

+131-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,137 @@ mod godot_api;
9393
mod itest;
9494
mod util;
9595

96-
#[proc_macro_derive(GodotClass, attributes(class, property, export, base, signal))]
96+
/// Derive macro for [`GodotClass`](godot_core::obj::GodotClass) on structs. You should normally use
97+
/// this macro, rather than implement `GodotClass` manually for your type.
98+
///
99+
/// # Construction
100+
///
101+
/// To generate a constructor that will let you call `MyStruct.new()` from GDScript, annotate your
102+
/// struct with `#[class(init)]`:
103+
///
104+
/// ```
105+
/// # use godot_macros::GodotClass;
106+
/// #[derive(GodotClass)]
107+
/// #[class(init)]
108+
/// struct MyStruct {
109+
/// // ...
110+
/// }
111+
/// ```
112+
///
113+
/// # Inheritance
114+
///
115+
/// Unlike C++, Rust doesn't really have inheritance, but the GDExtension API lets us "inherit"
116+
/// from a built-in engine class.
117+
///
118+
/// By default, classes created with this library inherit from `RefCounted`.
119+
///
120+
/// To specify a different class to inherit from, add `#[class(base = Base)]` as an annotation on
121+
/// your `struct`:
122+
///
123+
/// ```
124+
/// use godot::prelude::*;
125+
///
126+
/// #[derive(GodotClass)]
127+
/// #[class(base = Node2D)]
128+
/// struct MyStruct {
129+
/// // ...
130+
/// }
131+
/// ```
132+
///
133+
/// If you need a reference to the base class, you can add a field of type `Gd<Base>` and annotate
134+
/// it with `#[base]`:
135+
///
136+
/// ```
137+
/// use godot::prelude::*;
138+
///
139+
/// #[derive(GodotClass)]
140+
/// #[class(base = Node2D)]
141+
/// struct MyStruct {
142+
/// #[base]
143+
/// base: Gd<Node2D>,
144+
/// }
145+
/// ```
146+
///
147+
/// # Exported properties
148+
///
149+
/// In GDScript, there is a distinction between
150+
/// [properties](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_basics.html#properties-setters-and-getters)
151+
/// (fields with a `get` or `set` declaration) and
152+
/// [exports](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_exports.html)
153+
/// (fields annotated with `@export`). In the GDExtension API, these two concepts are merged into
154+
/// one.
155+
///
156+
/// You can export fields of your struct using the `#[export]` annotation:
157+
///
158+
/// ```
159+
/// use godot::prelude::*;
160+
///
161+
/// #[derive(GodotClass)]
162+
/// struct MyStruct {
163+
/// #[export]
164+
/// my_field: i64,
165+
/// }
166+
/// ```
167+
///
168+
/// This makes the field accessible in GDScript using `my_struct.my_field` syntax. Additionally, it
169+
/// generates a trivial getter and setter named `get_my_field` and `set_my_field`, respectively.
170+
/// These are `pub` in Rust, since they're exposed from GDScript anyway.
171+
///
172+
/// If you want to implement your own getter and/or setter, write those as a function on your Rust
173+
/// type, expose it using `#[func]`, and annotate the field with
174+
/// `#[export(get = "...", set = "...")]`:
175+
///
176+
/// ```
177+
/// use godot::prelude::*;
178+
///
179+
/// #[derive(GodotClass)]
180+
/// struct MyStruct {
181+
/// #[export(get = "get_my_field", set = "set_my_field")]
182+
/// my_field: i64,
183+
/// }
184+
///
185+
/// #[godot_api]
186+
/// impl MyStruct {
187+
/// #[func]
188+
/// pub fn get_my_field(&self) -> i64 {
189+
/// self.my_field
190+
/// }
191+
///
192+
/// #[func]
193+
/// pub fn set_my_field(&mut self, value: i64) {
194+
/// self.my_field = value;
195+
/// }
196+
/// }
197+
/// ```
198+
///
199+
/// If you specify only `get`, no setter is generated, making the field read-only. If you specify
200+
/// only `set`, no getter is generated, making the field write-only (rarely useful). To add a
201+
/// generated getter or setter in these cases anyway, use `get` or `set` without a value:
202+
///
203+
/// ```
204+
/// use godot::prelude::*;
205+
///
206+
/// #[derive(GodotClass)]
207+
/// struct MyStruct {
208+
/// // Default getter, custom setter.
209+
/// #[export(get, set = "set_my_field")]
210+
/// my_field: i64,
211+
/// }
212+
///
213+
/// #[godot_api]
214+
/// impl MyStruct {
215+
/// #[func]
216+
/// pub fn set_my_field(&mut self, value: i64) {
217+
/// self.my_field = value;
218+
/// }
219+
/// }
220+
/// ```
221+
///
222+
/// # Signals
223+
///
224+
/// The `#[signal]` attribute is accepted, but not yet implemented. See [issue
225+
/// #8](https://github.com/godot-rust/gdext/issues/8).
226+
#[proc_macro_derive(GodotClass, attributes(class, export, base, signal))]
97227
pub fn derive_native_class(input: TokenStream) -> TokenStream {
98228
translate(input, derive_godot_class::transform)
99229
}

0 commit comments

Comments
 (0)