-
-
Notifications
You must be signed in to change notification settings - Fork 217
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Property attribute for GDClass. #31
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot for this contribution, very nice to see property support! 👍
I need to look closer into it over the next few days, here just a broad first feedback. I would appreciate if you could outline in code how you intend the API to look, so it's clearer what the proc-macro is doing internally. This would also serve as a base for writing tests.
I also happily merge PRs that implement only a part of property functionality. In fact, smaller PRs are often at an advantage because they are easier to review, easier to unit-test, need less effort in implementation and thus also run a lower risk of requiring rewrite/discarding. So don't hesitate to go for an initial implementation (like the current, or even less) that is not feature-complete, but can serve as a base for future improvements! It's also very well possible that things change anyway for consistency/naming/ergonomic reasons.
let name = proc_macro2::Literal::from_str(&property_info.name).unwrap(); | ||
let getter = proc_macro2::Literal::from_str(&property_info.getter).unwrap(); | ||
let setter = proc_macro2::Literal::from_str(&property_info.setter).unwrap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since you expect those literals to be strings, you might want to use Literal::string()
.
Also, for methods we might just want to use symbols (setter = my_setter
instead of setter = "my_setter"
) for consistency with #[class(base=Node)]
and #[gdextension(entry_point=my_func)]
, but that can always be changed later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason I went with ="my_setter"
rather than =my_setter
is because there's no guarantee that users will expose methods to godot using their rust name (though I guess this library currently enforces that).
I've dealt with enough binding libraries that I've seen all kinds of conflicting naming requirements and would recommend building to support divorcing rust's function name form the godot-exposed name. I could totally see going with the function identifier for now and adding an option for string values later once #[func]
gets support for renaming.
What do you want me to go with for this first PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, the reason I couldn't go with Literal::string() was (IIRC) that the property_info fields include the enclosing "
characters. So #getter
would expand to "\"my_getter\""
rather than "my_getter"
.
Is there a better way to handle this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason I went with ="my_setter" rather than =my_setter is because there's no guarantee that users will expose methods to godot using their rust name (though I guess this library currently enforces that).
That's a valid point indeed. It depends a bit how this is used -- is it referring to a Rust function, or just the name of a registered function? Maybe some example code would be helpful 😉
Is there a better way to handle this?
I'm not aware of one, feel free to write a utility function for this if it occurs multiple times.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Responding to/resolving some comments.
let name = proc_macro2::Literal::from_str(&property_info.name).unwrap(); | ||
let getter = proc_macro2::Literal::from_str(&property_info.getter).unwrap(); | ||
let setter = proc_macro2::Literal::from_str(&property_info.setter).unwrap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason I went with ="my_setter"
rather than =my_setter
is because there's no guarantee that users will expose methods to godot using their rust name (though I guess this library currently enforces that).
I've dealt with enough binding libraries that I've seen all kinds of conflicting naming requirements and would recommend building to support divorcing rust's function name form the godot-exposed name. I could totally see going with the function identifier for now and adding an option for string values later once #[func]
gets support for renaming.
What do you want me to go with for this first PR?
let name = proc_macro2::Literal::from_str(&property_info.name).unwrap(); | ||
let getter = proc_macro2::Literal::from_str(&property_info.getter).unwrap(); | ||
let setter = proc_macro2::Literal::from_str(&property_info.setter).unwrap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, the reason I couldn't go with Literal::string() was (IIRC) that the property_info fields include the enclosing "
characters. So #getter
would expand to "\"my_getter\""
rather than "my_getter"
.
Is there a better way to handle this?
Thanks for the update! I think at this stage this would be most helpful:
So before further implementation, maybe write an example code that demonstrates the features implemented in this PR, from a user perspective. This could then serve as a base for API discussion 🙂 |
I added an integration test, that using a godot script to get & set the property. This then also self-documents in the code how it's meant to be used (though it could probably use actual documentation, once we finalize the API I should write some, lemme know where). Duplicating the example here for discussion: #[derive(GodotClass)]
#[class(base=Node)]
#[property(name = "val", getter = "get_val", setter = "set_val")]
struct HasProperty {
val: i32,
#[base]
base: Base<Node>,
}
#[godot_api]
impl HasProperty {
#[func]
pub fn get_val(&self) -> i32 {
return self.val;
}
#[func]
pub fn set_val(&mut self, val: i32) {
self.val = val;
}
}
#[godot_api]
impl GodotExt for HasProperty {
fn init(base: Base<Node>) -> Self {
HasProperty { val: 0, base }
}
} The rest of the features (func, base classing) that I saw all used macros, so I initially jumped to a macro. And an attribute on the GDClass derive seemed best way to handle it. Edit bromeon: |
Thanks for the example! #[derive(GodotClass)]
#[class(base=Node)]
#[property(name = "val", getter = "get_val", setter = "set_val")]
struct HasProperty {
val: i32,
#[base]
base: Base<Node>,
} This is good as a start, maybe mid-term we can go for an API similar to the one we have in GDNative, see docs for some insights. But I'd be happy to start with PR that functionally works, and then tweak the proc-macro API myself later 🙂 I'll comment on the implementation later today. |
We should probably be careful with adding new item-level attributes. They go out of control pretty fast. We've had that in GDNative (godot-rust/gdnative#848) and hopefully not again here.
Regarding this, I want to add that the common practice in the ecosystem right now is to quote identifiers and paths in attribute parameters. See IIRC it mostly have to do with complications parsing paths and complex types ( We need to decide whether the convenience right now is worth the surprise factor, and a possible future breaking change here. |
Yeah, it can also look a bit nicer if more complex values are in quotes (syntax highlighting makes clear what is the value). Not sure about super-short idents. Would you then write |
There's a tiny difference here though. The current GDNative |
This is mostly an argument from implementation perspective though -- for a user, it could make sense to have values in We currently support this: #[class(init, base=RefCounted)] // or, equivalent:
#[class(init)] and here, For GDExtension, I'm not sure if we would even have the syntax #[attribute(Value)] somewhere, or rather use |
It's not -- I'm only talking in terms of Of course, the decision here is yours to make. I'm just trying to say that going forward, you are likely to see this as a point of contention for many proc-macro PRs to come. |
I agree with What I meant is: from perspective of a GDExtension user, I would probably regard the @mhoff12358 sorry for the off-topic discussion 😀 |
As opposed to whom? I see that it's your stance to draw a clear separation between the two projects, which I'll respect. 😃 |
Haha no, that's not how I meant it 😅 more that the proc-macro APIs are naturally diverging due to underlying architecture ( In general, I believe both projects can greatly benefit from each other. There's a ton of prior art in GDNative for all kinds of mechanisms, and years of knowledge that we can build upon. At the same time, GDExtension is a unique chance to experiment with some new approaches, without causing a large userbase to grab their pitchforks 😀 |
Yeah, I definitely think it's worth moving some of the suggestions from here into the issue and working on a longer term API design (relinking here for convenience of anyone following along #3). |
@Bromeon was there anything else you wanted me to do/add before merging this? |
@mhoff12358 Sorry for the delay. #[derive(GodotClass)]
#[class(base=Node)]
#[property(name = "val", getter = "get_val", setter = "set_val")]
struct HasProperty {
val: i32,
#[base]
base: Base<Node>,
}
#[godot_api]
impl HasProperty {
// Does not need to be #[func], correct?
fn get_val(&self) -> i32 {
return self.val;
}
fn set_val(&mut self, val: i32) {
self.val = val;
}
} As mentioned above, what do you think about having the annotation on the property rather than on the class? You would have more locality and not need to repeat #[derive(GodotClass)]
#[class(base=Node)]
struct HasProperty {
#[property(getter = "get_val", setter = "set_val")]
val: i32,
#[base]
base: Base<Node>,
} Also, we should probably name it
It could also be |
@Bromeon Sure, yeah I can do that. I personally still have a gripe with how Godot does exports/properties, coming from C#. It has you put the export on a member variable, but then you can provide arbitrary setter and getter methods that don't necessarily need to use that member variable for storage. A GDScript that wraps a non-exported array might want a property to set the array's size, but would have to define an extra member variable just to make the size-changing methods available to the editor. But in this case, yeah, if the goal is to have the API directly mirror GDScript I can see how that matters more. I think my needs can be served by just better documenting whatever method I had previously found that lets you add custom code to the registration and letting the user call the registration methods themselves directly. I'll probably start with a fresh PR in this case and copy things over. I'll close this once that happens. |
FYI we have |
Working on #3
This is in-progress. I have tested that multiple properties do seem to work on the Godot 4 beta 6 release. The code could undoubtedly use a cleanup pass before going in.
It only handles the basic case with a property + getter/setter. There's no support for editor hints.
The issue I've run into is that the way attribute arguments are being parsed at the moment isn't expressive enough. They're parsed into a Map<String, KvValue>, and KvValue only handles literals or identifiers, which doesn't seem to account for something like
godot_ffi::VariantType::Int
. Either KvValue will have to be expanded to account for more general expressions, or the property attribute will have to parse its arguments some other way.