gdnative: GDNative default parameters cause breaking changes
The dodge_the_creeps example in godot-rust, which uses Input::is_action_pressed(), no longer compiles with Godot 3.4.
Reason is an API change in GDNative:
-
Class
Inputfor Godot 3.3:
-
Class
Inputfor Godot 3.4:
The extra parameter exact is optional, which is a non-breaking change in GDScript and C++, but a breaking one in Rust, since the language does not support default parameters.
There are likely more places in the GDNative API with such changes. At the moment, they make it impossible for godot-rust to support multiple Godot minor versions simultaneously. Either we find a way to support such APIs in Rust, or we can only support the latest minor version.
About this issue
- Original URL
- State: open
- Created 3 years ago
- Reactions: 4
- Comments: 20 (16 by maintainers)
Commits related to this issue
- Merge #829 829: Support Godot 3.4 r=Bromeon a=Bromeon Updates api.json, CI scripts and examples. This is a **breaking change**. Some GDNative APIs added default parameters, which can currently no... — committed to godot-rust/gdnative by bors[bot] 3 years ago
- Merge #829 829: Support Godot 3.4 r=Bromeon a=Bromeon Updates api.json, CI scripts and examples. This is a **breaking change**. Some GDNative APIs added default parameters, which can currently no... — committed to godot-rust/gdnative by bors[bot] 3 years ago
- Make ptrcalls opt-in This implements cuddlefishie's suggestion in #814, but with the semantics reversed to better conform with Cargo's additive combination of features. - Added the new feature flag ... — committed to chitoyuu/godot-rust by chitoyuu 2 years ago
- Make ptrcalls opt-in This implements cuddlefishie's suggestion in #814, but with the semantics reversed to better conform with Cargo's additive combination of features. - Added the new feature flag ... — committed to chitoyuu/godot-rust by chitoyuu 2 years ago
- Make ptrcalls opt-in This implements cuddlefishie's suggestion in #814, but with the semantics reversed to better conform with Cargo's additive combination of features. - Added the new feature flag ... — committed to chitoyuu/godot-rust by chitoyuu 2 years ago
- Merge #973 973: Make ptrcalls opt-in r=chitoyuu a=chitoyuu This implements cuddlefishie's suggestion in #814, but with the semantics reversed to better conform with Cargo's additive combination of f... — committed to godot-rust/gdnative by bors[bot] 2 years ago
- Merge #322 322: Default parameters in Engine API + meta param/return types r=Bromeon a=Bromeon # Default parameters in Engine API Example [`RenderingServer::environment_set_ambient_light`](https:... — committed to godot-rust/gdext by bors[bot] a year ago
Using “varcalls” instead of “ptrcalls” should make those additions of optional parameters non-breaking changes, at the expense of having slower method calls into engine classes.
Using “varcall” is essentially the same mechansim that GDScript or VisualScript uses, so also shares the performance characteristics.
Doing a “ptrcall” is basically the same as taking an untyped function pointer that points directly (more or less) to the in-engine implementation of a method. When doing such a call the number and types of arguments and return values must match exactly, because it’s like calling a function pointer.
Because the performance is better with ptrcalls, the bindings generator tries to select those wherever possible (there are some conditions where a varcall is necessary).
For more “used across different versions” projects, such as plugins that build on GDNative and ship with binaries, it might make sense to switch every method binding to use varcalls. That would mean performance might be a little worse, but it would be guaranteed to be compatible across minor versions, even with the same binary.
We could add a feature flag like
generate_only_varcalls(or with a different name) that overrides this selection process to never generate ptrcalls, in case a plugin author wants to guarantee compatibility across minor versions.https://github.com/godot-rust/godot-rust/blob/42a6e768a02ed32068ae1dfe5a5e655181ea2e6f/bindings_generator/src/methods.rs#L124-L135
Would that be interesting for users of this crate or does this seem like a niche case and not requiring an extra feature flag and another knob to turn or not turn?
Would it make sense to make varcalls the default and recommend users to use ptrcalls only in release builds?
Let me know what you think!
Sorry, forgot to update this issue.
Over the next days, I’ll work on making
mastercompatible with stable Godot 3.4. There are likely more people who are willing to upgrade and benefit from latest Godot features/bugfixes, than downgrade. Those who want to stay with 3.2 or 3.3 can still manually generate theapi.json.In a next step, we should think about simplifying the custom
api.jsongeneration step. https://github.com/godot-rust/godot-rust/issues/640 offers a very interesting idea, for example.I don’t think we want to jump to conclusions here, before looking at empirical data of how often did any of these “exotic” breaking patterns actually happen for the whole duration of Godot 3, if ever. Every approach here is indeed ugly, but not in equal ways so:
The nightmare case is, of course, as you have said, becoming forced to keep multiple legacy versions for years. Docs get clogged with
v1,v2,v3andv4s of multiple functions and everything is terrible. The best case here, however, is just perfectly natural Rust.This would be ideal in a world where optionalizations are few and far-between, and when they do happen, multiple arguments become optional at once (so fewer versions are necessary).
This avoids the nightmare case, at the cost of making every single method call slightly, but uniformly ugly, regardless of which world we’re living in.
This would be ideal in a world where arguments become optionalized, one by one, all the time, as such events do not generate extra permanent ugliness under this design.
It can also be argued that it’s a feature that Rust libraries commonly provide, and is thus expected by users.
This is something I haven’t considered about. It should be however be possible to “remember” all the past names of an argument with automation, and keep any old named methods around (with
#[deprecated]). This only needs to apply to crates builds, of course.We might need to dig into past
api.jsons to find out the actual probability of this happening, but I do not anticipate it to be very high.This neither. For the named approach it’s possible to force all parameters to be specified through the builder interface (i.e. no arguments in the initial
is_action_pressed_excall), but this still leaves open the question of non-builderized versions.One option is again to memorize the original required parameter count, and generate a new version each time a parameter is optional-ized and deprecate the original. I’m not sure how ugly this will get though, since this scenario may have a higher chance of actual occurrence compared to the previous one, and the impact is higher (new method on the base type vs. new method on a builder). Might need to dig into past
api.jsons to find out.I’m not sure I can come up with something better for the positional API.
Overall I feel like that the proposed pros of the positional API do not outweigh the ability to have a idiomatic Rust interface. Realistically
call()is very unlikely to be forgotten due to#[must_use]– the lack of strict linearity being only a theoretical issue. The last two a named approach does as well. So ultimately, it comes down to a stylistic choice, and I sense that this is where the two libraries must diverge.Thanks a lot for your input!
Very nice! I like the use of
()to signal “no argument present”.One thing I was thinking about – in our previous discussions, we always assumed that default parameters should be provided as named parameters. However, in both GDScript and C++ they are simply positional ones.
While it’s useful to skip some default parameters (e.g. only provide a value for the last one), things to consider:
That said, it could still prove a very useful addition, especially compared to the current approach.
For GDExtension, I’d like to experiment a bit with positional arguments. One idea I had:
Pros:
call()at the end.method+method_extoverloads).OptionalArgs3would still haveimplfor a 2-tuple).Cons:
The ABI part is addressed in #973. I’ll try to implement the call-builder solution for the API part for v0.12. The plans to deal with previously unresolved questions (https://github.com/godot-rust/gdnative/issues/814#issuecomment-963308724) are:
{original_name}_ex. “Raw” versions with full optional args are no longer generated. Currently, there are no methods with a_exsuffix in the Godot API. To guard against name collisions in the future, any Godot methods with names that end in_ex(_ex|_godot)+will have an extra_godotappended to their names (and_godot_exappended to their builderized versions).Examples of collision avoidance
Given the following Godot methods, all containing optional args:
The following Rust methods will be generated:
This may seem horrifying at first, but really what we’re hoping for here is for the engine to never introduce
_exand much less_ex_godotsuffixes to their methods in the first place. It doesn’t make much sense from an Engine point of view to create such variants, because they can leverage optional arguments for GDScript and C#.#[must_use], which should cause Rust to emit warnings unless they are explicitly ignored (probably not what the user wants).ptrcallwill only be used if all optional parameters are provided, and if theptrcallfeature is enabled. Non-builderized versions of methods will always usevarcallunder the hood, if any optional arguments exist, even ifptrcallis enabled as a feature.I expect the initial API to be something like: