Skip to content

Commit 1606a7f

Browse files
authored
Merge pull request #1261 from ttencate/feature/synthetic_properties
Add PhantomVar<T> to support properties without a backing field
2 parents a3e2cdf + 3fa3eb1 commit 1606a7f

File tree

11 files changed

+419
-37
lines changed

11 files changed

+419
-37
lines changed

godot-core/src/registry/property.rs renamed to godot-core/src/registry/property/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ use std::fmt::Display;
1616
use crate::meta::{ClassName, FromGodot, GodotConvert, GodotType, PropertyHintInfo, ToGodot};
1717
use crate::obj::{EngineEnum, GodotClass};
1818

19+
mod phantom_var;
20+
21+
pub use phantom_var::PhantomVar;
22+
1923
// ----------------------------------------------------------------------------------------------------------------------------------------------
2024
// Trait definitions
2125

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
/*
2+
* Copyright (c) godot-rust; Bromeon and contributors.
3+
* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
6+
*/
7+
8+
use std::{
9+
cmp::Ordering,
10+
fmt,
11+
hash::{Hash, Hasher},
12+
marker::PhantomData,
13+
};
14+
15+
use crate::{
16+
meta::{ClassName, GodotConvert, GodotType, PropertyHintInfo},
17+
registry::property::{Export, Var},
18+
};
19+
20+
/// A zero-sized type for creating a property without a backing field, accessible only through custom getter/setter functions.
21+
///
22+
/// This must be used in a struct deriving [`GodotClass`](../register/derive.GodotClass.html) and requires that the field has
23+
/// an explicit [`#[var]` attribute](../register/derive.GodotClass.html#register-properties--var) with a custom getter,
24+
/// and optionally a custom setter. Both getter and setter operate on the specified type `T`.
25+
///
26+
/// (Note that write-only properties, with a setter but not a getter, are not currently supported.
27+
/// Godot doesn't fully support them either, silently returning null instead of an error if the property is being read.)
28+
///
29+
/// # Example
30+
///
31+
/// Suppose you have a field `text` whose value you want to keep as a Rust `String` rather than a Godot `GString`,
32+
/// accepting the performance penalty for conversions whenever the property is accessed from Godot:
33+
///
34+
/// ```
35+
/// # use godot::prelude::*;
36+
/// #[derive(GodotClass)]
37+
/// #[class(init)]
38+
/// struct Banner {
39+
/// #[var(get = get_text, set = set_text)]
40+
/// text: PhantomVar<GString>,
41+
///
42+
/// text_string: String,
43+
/// }
44+
///
45+
/// #[godot_api]
46+
/// impl Banner {
47+
/// #[func]
48+
/// fn get_text(&self) -> GString {
49+
/// GString::from(&self.text_string)
50+
/// }
51+
///
52+
/// #[func]
53+
/// fn set_text(&mut self, text: GString) {
54+
/// self.text_string = String::from(&text);
55+
/// }
56+
/// }
57+
/// ```
58+
///
59+
/// This field can now be accessed from GDScript as `banner.text`.
60+
// Bounds for T are somewhat un-idiomatically directly on the type, rather than impls.
61+
// This improves error messages in IDEs when using the type as a field.
62+
pub struct PhantomVar<T: GodotType + Var>(PhantomData<T>);
63+
64+
impl<T: GodotType + Var> GodotConvert for PhantomVar<T> {
65+
type Via = T;
66+
}
67+
68+
// `PhantomVar` supports only part of `Var`, but it has to implement it, otherwise we cannot implement `Export` either.
69+
// The `GodotClass` derive macro should ensure that the `Var` implementation is not used.
70+
impl<T: GodotType + Var> Var for PhantomVar<T> {
71+
fn get_property(&self) -> Self::Via {
72+
unreachable!("code generated by GodotClass should call the custom getter")
73+
}
74+
75+
fn set_property(&mut self, _value: Self::Via) {
76+
unreachable!("code generated by GodotClass should call the custom setter");
77+
}
78+
79+
fn var_hint() -> PropertyHintInfo {
80+
<T as Var>::var_hint()
81+
}
82+
}
83+
84+
// Reuse values from `T`, if any.
85+
impl<T: GodotType + Var + Export> Export for PhantomVar<T> {
86+
fn export_hint() -> PropertyHintInfo {
87+
<T as Export>::export_hint()
88+
}
89+
90+
fn as_node_class() -> Option<ClassName> {
91+
<T as Export>::as_node_class()
92+
}
93+
}
94+
95+
impl<T: GodotType + Var> Default for PhantomVar<T> {
96+
fn default() -> Self {
97+
Self(Default::default())
98+
}
99+
}
100+
101+
// Like `PhantomData` from the Rust standard library, `PhantomVar` implements many common traits like `Eq` and `Hash`
102+
// to allow these traits to be derived on containing structs as well.
103+
104+
impl<T: GodotType + Var> Clone for PhantomVar<T> {
105+
fn clone(&self) -> Self {
106+
*self
107+
}
108+
}
109+
110+
impl<T: GodotType + Var> Copy for PhantomVar<T> {}
111+
112+
impl<T: GodotType + Var> fmt::Debug for PhantomVar<T> {
113+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
114+
f.debug_tuple("PhantomVar").finish()
115+
}
116+
}
117+
118+
impl<T: GodotType + Var> PartialEq for PhantomVar<T> {
119+
fn eq(&self, _other: &Self) -> bool {
120+
true
121+
}
122+
}
123+
124+
impl<T: GodotType + Var> Eq for PhantomVar<T> {}
125+
126+
impl<T: GodotType + Var> PartialOrd for PhantomVar<T> {
127+
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
128+
Some(self.cmp(other))
129+
}
130+
}
131+
132+
impl<T: GodotType + Var> Ord for PhantomVar<T> {
133+
fn cmp(&self, _other: &Self) -> Ordering {
134+
Ordering::Equal
135+
}
136+
}
137+
138+
impl<T: GodotType + Var> Hash for PhantomVar<T> {
139+
fn hash<H: Hasher>(&self, _state: &mut H) {}
140+
}
141+
142+
/// This type exists only as a place to add `compile_fail` doctests for `PhantomVar`, which do not need to be in the public documentation.
143+
///
144+
/// Omitting the `#[var]` attribute is an error:
145+
///
146+
/// ```compile_fail
147+
/// # use godot::prelude::*;
148+
/// #[derive(GodotClass)]
149+
/// #[class(init)]
150+
/// struct Oops {
151+
/// missing_var: PhantomVar<i64>,
152+
/// }
153+
/// ```
154+
///
155+
/// Declaring `#[var]` without a getter and/or setter is an error:
156+
///
157+
/// ```compile_fail
158+
/// # use godot::prelude::*;
159+
/// #[derive(GodotClass)]
160+
/// #[class(init)]
161+
/// struct Oops {
162+
/// #[var]
163+
/// missing_get_set: PhantomVar<i64>,
164+
/// }
165+
/// ```
166+
///
167+
/// Declaring `#[var]` without a getter is an error:
168+
///
169+
/// ```compile_fail
170+
/// # use godot::prelude::*;
171+
/// #[derive(GodotClass)]
172+
/// #[class(init)]
173+
/// struct Oops {
174+
/// #[var(set = setter)]
175+
/// missing_get: PhantomVar<i64>,
176+
/// }
177+
///
178+
/// #[godot_api]
179+
/// impl Oops {
180+
/// #[func]
181+
/// fn setter(&mut self, value: i64) {
182+
/// }
183+
/// }
184+
/// ```
185+
///
186+
/// Declaring `#[var]` with a default getter is an error:
187+
///
188+
/// ```compile_fail
189+
/// # use godot::prelude::*;
190+
/// #[derive(GodotClass)]
191+
/// #[class(init)]
192+
/// struct Oops {
193+
/// #[var(get, set = setter)]
194+
/// default_get: PhantomVar<i64>,
195+
/// }
196+
///
197+
/// #[godot_api]
198+
/// impl Oops {
199+
/// #[func]
200+
/// fn setter(&mut self, value: i64) {
201+
/// }
202+
/// }
203+
/// ```
204+
///
205+
/// Declaring `#[var]` with a default setter is an error:
206+
///
207+
/// ```compile_fail
208+
/// # use godot::prelude::*;
209+
/// #[derive(GodotClass)]
210+
/// #[class(init)]
211+
/// struct Oops {
212+
/// #[var(get = getter, set)]
213+
/// missing_set: PhantomVar<i64>,
214+
/// }
215+
///
216+
/// #[godot_api]
217+
/// impl Oops {
218+
/// #[func]
219+
/// fn getter(&self) -> i64 {
220+
/// 0
221+
/// }
222+
/// }
223+
/// ```
224+
#[allow(dead_code)]
225+
struct PhantomVarDoctests;

godot-macros/src/class/data_models/field.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ pub struct Field {
2121
pub subgroup: Option<FieldGroup>,
2222
pub is_onready: bool,
2323
pub is_oneditor: bool,
24+
pub is_phantomvar: bool,
2425
#[cfg(feature = "register-docs")]
2526
pub attributes: Vec<venial::Attribute>,
2627
pub span: Span,
@@ -38,6 +39,7 @@ impl Field {
3839
subgroup: None,
3940
is_onready: false,
4041
is_oneditor: false,
42+
is_phantomvar: false,
4143
#[cfg(feature = "register-docs")]
4244
attributes: field.attributes.clone(),
4345
span: field.span(),

godot-macros/src/class/data_models/field_var.rs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,8 @@ impl FieldVar {
3737
/// - `usage_flags =
3838
pub(crate) fn new_from_kv(parser: &mut KvParser) -> ParseResult<Self> {
3939
let span = parser.span();
40-
let mut getter = GetterSetter::parse(parser, "get")?;
41-
let mut setter = GetterSetter::parse(parser, "set")?;
42-
43-
if getter.is_omitted() && setter.is_omitted() {
44-
getter = GetterSetter::Generated;
45-
setter = GetterSetter::Generated;
46-
}
40+
let getter = GetterSetter::parse(parser, "get")?;
41+
let setter = GetterSetter::parse(parser, "set")?;
4742

4843
let hint = parser.handle_ident("hint")?;
4944

@@ -77,6 +72,14 @@ impl FieldVar {
7772
span,
7873
})
7974
}
75+
76+
/// If both `get` and `set` are omitted, pretends that autogenerated getters and setters were requested.
77+
pub(crate) fn default_to_generated_getter_setter(&mut self) {
78+
if self.getter.is_omitted() && self.setter.is_omitted() {
79+
self.getter = GetterSetter::Generated;
80+
self.setter = GetterSetter::Generated;
81+
}
82+
}
8083
}
8184

8285
impl Default for FieldVar {

godot-macros/src/class/data_models/property.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ impl FieldHint {
4141

4242
pub fn make_property_impl(class_name: &Ident, fields: &Fields) -> TokenStream {
4343
let mut getter_setter_impls = Vec::new();
44+
let mut phantom_var_dummy_uses = Vec::new();
4445
let mut func_name_consts = Vec::new();
4546
let mut export_tokens = Vec::new();
4647

@@ -163,6 +164,13 @@ pub fn make_property_impl(class_name: &Ident, fields: &Fields) -> TokenStream {
163164
class_name,
164165
);
165166

167+
if field.is_phantomvar {
168+
let field_name = field.name.clone();
169+
phantom_var_dummy_uses.push(quote! {
170+
let _ = &self.#field_name;
171+
});
172+
}
173+
166174
export_tokens.push(quote! {
167175
// This type may be reused in #hint, in case of generic functions.
168176
type FieldType = #field_type;
@@ -176,13 +184,29 @@ pub fn make_property_impl(class_name: &Ident, fields: &Fields) -> TokenStream {
176184
});
177185
}
178186

187+
let phantom_var_dummy_use_fn = if phantom_var_dummy_uses.is_empty() {
188+
quote! {}
189+
} else {
190+
// `PhantomVar` fields are not normally accessed, resulting in undesired dead-code warnings.
191+
// We are in a derive macro, so we cannot alter the original struct definition to add `#[allow(dead_code)]` to the field.
192+
// Instead, we generate an unused, hidden function that mentions the field.
193+
quote! {
194+
#[expect(dead_code)]
195+
#[doc(hidden)]
196+
fn __phantom_var_dummy_uses(&self) {
197+
#(#phantom_var_dummy_uses)*
198+
}
199+
}
200+
};
201+
179202
// For each generated #[func], add a const declaration.
180203
// This is the name of the container struct, which is declared by #[derive(GodotClass)].
181204
let class_functions_name = format_funcs_collection_struct(class_name);
182205

183206
quote! {
184207
impl #class_name {
185208
#(#getter_setter_impls)*
209+
#phantom_var_dummy_use_fn
186210
}
187211

188212
impl #class_functions_name {

0 commit comments

Comments
 (0)