Add PhantomVar<T> to support properties without a backing field#1261
Add PhantomVar<T> to support properties without a backing field#1261Bromeon merged 1 commit intogodot-rust:masterfrom
Conversation
|
API docs are being generated and will be shortly available at: https://godot-rust.github.io/docs/gdext/pr-1261 |
96fa239 to
59e5b32
Compare
Bromeon
left a comment
There was a problem hiding this comment.
Fantastic work! Solid implementation, great docs and examples, great tests.
Thanks a lot! 💪
godot-core/src/obj/phantom_var.rs
Outdated
| // `PhantomVar` does not _really_ implement `Var`, but it has to, otherwise we cannot implement `Export` either. | ||
| // The `GodotClass` derive macro should ensure that the `Var` implementation is not used. | ||
| impl<T> Var for PhantomVar<T> | ||
| where | ||
| T: GodotType + Var, | ||
| { | ||
| fn get_property(&self) -> Self::Via { | ||
| unreachable!("PhantomVar::get_property should never have been called") | ||
| } | ||
|
|
||
| fn set_property(&mut self, _value: Self::Via) { | ||
| unreachable!("PhantomVar::set_property should never have been called"); | ||
| } | ||
|
|
||
| fn var_hint() -> PropertyHintInfo { | ||
| <T as Var>::var_hint() | ||
| } | ||
| } |
There was a problem hiding this comment.
The var_hint part is used though, no? Just get/set_property aren't useful.
So, part of Var is implemented 🤔
Also, "PhantomVar::set_property should never have been called" is somewhat implied by unreachable!... you can maybe just use unreachable!() or add some extra information, like "property getters/setters should be directly called by proc-macro, not through Var".
There was a problem hiding this comment.
Updated comment and strings.
godot-core/src/obj/phantom_var.rs
Outdated
| impl<T> Default for PhantomVar<T> { | ||
| fn default() -> Self { | ||
| Self(Default::default()) | ||
| } | ||
| } | ||
|
|
||
| impl<T> Clone for PhantomVar<T> { | ||
| fn clone(&self) -> Self { | ||
| *self | ||
| } | ||
| } | ||
|
|
||
| impl<T> Copy for PhantomVar<T> {} | ||
|
|
||
| impl<T> fmt::Debug for PhantomVar<T> { | ||
| fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||
| f.debug_tuple("PhantomVar").finish() | ||
| } | ||
| } | ||
|
|
||
| impl<T> PartialEq for PhantomVar<T> { | ||
| fn eq(&self, _other: &Self) -> bool { | ||
| true | ||
| } | ||
| } | ||
|
|
||
| impl<T> Eq for PhantomVar<T> {} | ||
|
|
||
| impl<T> PartialOrd for PhantomVar<T> { | ||
| fn partial_cmp(&self, other: &Self) -> Option<Ordering> { | ||
| Some(self.cmp(other)) | ||
| } | ||
| } | ||
|
|
||
| impl<T> Ord for PhantomVar<T> { | ||
| fn cmp(&self, _other: &Self) -> Ordering { | ||
| Ordering::Equal | ||
| } | ||
| } | ||
|
|
||
| impl<T> Hash for PhantomVar<T> { | ||
| fn hash<H: std::hash::Hasher>(&self, _state: &mut H) {} | ||
| } |
There was a problem hiding this comment.
Can any of these be derived, or does it cause problems due to the generic parameter?
There was a problem hiding this comment.
Exactly. The stdlib's derive macros always add a bound T: TraitWeAreDeriving even if it's not strictly needed. This is why crates like derive_where exist.
| if let Some(field_var) = &mut field.var { | ||
| if field_var.getter.is_generated() { | ||
| errors.push(error!( | ||
| field_var.span, | ||
| "PhantomVar<T> stores no data, so it cannot use an autogenerated getter" | ||
| )); | ||
| } | ||
| if field_var.setter.is_generated() { | ||
| errors.push(error!( | ||
| field_var.span, | ||
| "PhantomVar<T> stores no data, so it cannot use an autogenerated setter" | ||
| )); | ||
| } | ||
|
|
||
| if field_var.getter.is_omitted() && field_var.setter.is_omitted() { | ||
| errors.push(error!( | ||
| field_var.span, | ||
| "PhantomVar<T> is useless without a custom getter, setter, or both" | ||
| )); | ||
| } | ||
| } else { | ||
| errors.push(error!( | ||
| field.span, | ||
| "PhantomVar<T> field is useless without attribute #[var]" | ||
| )); | ||
| } |
There was a problem hiding this comment.
Maybe extract into a function, e.g. validate_phantomvar(...)
There was a problem hiding this comment.
OK but then I'll also extract validate_base_field for the code right above this.
itest/godot/ManualFfiTests.gd
Outdated
| # This test is commented out, because Godot seems to allow reading a write-only property, and simply returns null. | ||
| # func test_phantom_var_reading_write_only(): | ||
| # # This must be untyped, otherwise the parser complains about our invalid read. | ||
| # var obj = HasPhantomVar.new() | ||
| # expect_fail() | ||
| # var x = obj.write_only | ||
| # assert_fail("HasPhantomVar.write_only should not be readable") |
There was a problem hiding this comment.
Do you believe this behavior will change in the future?
If not, we should maybe rather test the current behavior, and document why we can't have write-only. If semantics on Godot's side ever change in that regard, at least we'll notice...
There was a problem hiding this comment.
Couldn't we implement a custom getter that panics instead when no_get is specified?
There was a problem hiding this comment.
Couldn't we implement a custom getter that panics instead when no_get is specified?
as far as I'm aware the editor must be able to read given property (in case of #[export]), so it can't really panic.
There was a problem hiding this comment.
Couldn't we return default values for a type? Is that even more unsound? Right now you get null anyway, so what would be wrong in doing push_error + return null / default value if type is non-nullable?
There was a problem hiding this comment.
Wait, how would the editor even read a write only property? Hm, I suppose that would push an error every time the node's inspector is open? We can make it push_warning instead, or disable it when #[var(no_get, set)]#[export]. I would prefer push error if no editor, push warning if editor and add a bit of "Ignore this message if you haven't used the getter from code, the editor is trying to fetch the new value" or something.
There was a problem hiding this comment.
I can think of two use cases for write-only properties:
- Setting a password, which is immediately hashed so it can never be recovered.
- Setting a seed on a random number generator, which becomes meaningless after changing the RNG state. Godot's RandomNumberGenerator actually allows you to read the seed back again but has some warnings in the docs about it.
Both use cases are better served by methods, I think.
As for #[export], it also has the effect of serializing the value, which can never work if the editor can't read it back. So exported write-only properties are also a bit of a contradiction.
Let me just make write-only PhantomVar properties an error for now.
(None of this is specific to PhantomVar by the way, but I don't want to break backwards compatibility yet!)
There was a problem hiding this comment.
Do I open an issue for write-only properties and their soundness? And there still has to be the rework on [no_get, get, no_set, set] done, right?
There was a problem hiding this comment.
If you have a concrete use case for write-only properties, you could open an issue to investigate. I personally don't think it's worth it, but don't let that stop you 🙂
The compatibility-breaking changes with no_get, no_set etc. should be a separate issue for sure.
godot-core/src/obj/mod.rs
Outdated
| mod instance_id; | ||
| mod on_editor; | ||
| mod on_ready; | ||
| mod phantom_var; |
There was a problem hiding this comment.
I think this should be added to the godot::register::property module instead.
Maybe we need to move OnReady/OnEditor as well in the future... 🤔 modules generally need a bit of cleanup at some point.
There was a problem hiding this comment.
To me it makes most sense for PhantomVar to be next to OnReady and OnEditor, wherever those are. I'd be happy to move all three of them in a follow-up PR.
There was a problem hiding this comment.
It's just that we can't change modules for patch releases (at least not without deprecation), and if we already know now that godot::obj is the "wrong" place, it just means one extra breaking change for users that we could easily avoid...
There was a problem hiding this comment.
Fair enough, I thought these modules were all internal. I'd expect that most usage is through the prelude, but breaking is breaking. Moved to godot-core/src/registry/property.rs instead.
There was a problem hiding this comment.
For the record, I never use prelude, don't like them, I prefer being explicit with imports and I believe rust-analyzer finds their definition rather than re-exports, so yeah, this would indeed be a breaking change for all my godot-rust code (although an easy one to fix).
There was a problem hiding this comment.
I always use the godot prelude, as I can't generally be bothered to micromanage modules. It's also a pain for merge conflicts and the like, without adding real value.
That said, we should have a somewhat structured module hierarchy for people who do want it -- and let's not forget that lots of symbols aren't in the prelude (especially godot::classes::*). However, if someone opts in to use fine-grained modules rather than prelude, they readily accept the risk of more micromanaging. So it's OK to break module paths as long as we respect SemVer -- i.e. remove across minor versions, or deprecate across patch versions.
godot-core/src/registry/property.rs
Outdated
| { | ||
| } | ||
|
|
||
| // ---------------------------------------------------------------------------------------------------------------------------------------------- |
There was a problem hiding this comment.
It's now 200 LoC, I think it deserves its own file phantom_var.rs? I should have mentioned this (sorry!), I just think it's easier to navigate to it in the IDE, especially with the doctests "polluting" a considerable part.
There was a problem hiding this comment.
Sure, I would prefer that too.
Would you want the module private with a pub use crate::registry::phantom_var::PhantomVar in property.rs? This is a bit weird because it's a sibling module though. I could also make property.rs into property/mod.rs and put property/phantom_var.rs next to it.
Or just expose the phantom_var module directly?
There was a problem hiding this comment.
It can also just be a sister module in https://github.com/godot-rust/gdext/tree/master/godot-core/src/registry. I need to clean up a bit in v0.4 anyway, but if they are already separate files, it's going to be simpler 🙂
Or the mod.rs approach is also fine -- I hope Git picks up the rename.
There was a problem hiding this comment.
OK, I hope I got it right. The public name is now godot::register::property::PhantomVar.
|
Good from my side, you can squash. Maybe @Yarwin has more comments, otherwise we can merge 🙂 |
b9a8288 to
e9c26d6
Compare
e9c26d6 to
3fa3eb1
Compare
Closes #1151.