-
-
Notifications
You must be signed in to change notification settings - Fork 21.6k
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
Improve GDExtension user-friendliness by being more explicit on when return value should be passed initialized #35813
Improve GDExtension user-friendliness by being more explicit on when return value should be passed initialized #35813
Conversation
Issue #35609 is also related to this Considering this code (see touilleMan/godot-python@d0f2245): godot_object *__ret; // C doesn't automatically do zero initialization
gdapi10.godot_method_bind_ptrcall(
__methbind__Object__get_class, // Object.get_class returns a Reference object
my_obj_ptr,
NULL,
&__ret // Reference.unreference will be called on __ret as part of it destructor
// hence causing a segfault given __ret points on nothing
) |
bdb714a
to
1407ab6
Compare
I've rebased this branch on the master, should be ready to merge ;-) |
@vnen can you have a look at this ? 😃 |
Not entirely sure what this does. How would this affect engine code (not GDNative) like the following?: String ret;
method->ptrcall(ptr, nullptr, &ret);
Ref<Reference> ret;
method->ptrcall(ptr, nullptr, &ret); This is what the glue for C# uses. |
@neikeq thanks you for your input, this is a very interesting
The initial This is "fine" in your example given Line 438 in 0ee744f
and neither does it CowData field: godot/core/templates/cowdata.h Line 63 in 0ee744f
It feels however very fragile:
So I would say there different expectation between the C and the C++ API here: godot_string ret;
gdapi10.godot_method_bind_ptrcall(
__methbind__Object__get_class,
my_obj_ptr,
NULL,
&ret
)
[...]
godot_string_destroy(&ret) in C++, developer expects to do: String ret;
method->ptrcall(ptr, nullptr, &ret);
[...] Currently the C API forces you to do the C++ way (which leads to segfault), this PR forces the C++ API to do the C way (which leads to memory leaks as you pointed out ^^) The solutions I see:
|
Can confirm this is already the case with Lines 571 to 574 in dfe4feb
So this PR would cause leaks with the C# glue: https://gist.github.com/neikeq/857a621f2878135ce1e2a702cd0a2bb9#file-mono_glue-gen-cpp-L119 |
I agree with the change and I don't have a problem updating the C# glue. I'm committing some changes soon which will make this update easier. However, this is a breaking change so we need opinion from the @godotengine/gdnative team. There's also the concern you mention about someone passing a constructed object like |
@touilleMan GDNative has been replaced with GDExtension. Does this problem still exist on the current master? If so, this PR needs to be rebased on the latest master branch. |
Assuming this PR is still valid, this would introduce a memory leak in GDScript here: godot/modules/gdscript/gdscript_vm.cpp Line 1816 in f323d25
The fix should be easy. Just want to make sure it doesn't slip through. |
I don't think this should be done. AFAIK you're expected to pass a constructed value to ptrcall. If the problem is the C API then I think it should be done only for the C API. |
see my previous comment on C++ vs C expectations #35813 (comment) I agree that the best way to solve this would be to keep the |
This PR is totally outdated, but the issue still remains 😄 (tl;dr: well maybe there is only a documentation issue after all) So here is an attempt to provide an up-to-date view on the issue (hence ping @godotengine/gdextension !): 1 - How things work inside GodotGodot is a C++ codebase, so all the builtins classes (e.g. So it's fine to have method that get output parameter as reference (i.e. Line 538 in 57bdddc
and use them this way: godot/core/variant/variant.cpp Lines 864 to 866 in 57bdddc
And finally in godot/core/variant/variant_op.cpp Line 1027 in 57bdddc
So here (one could argue that 2 - How things works with GDExtensionOn the other hand, GDExtension is a C API, so the godot/core/extension/gdnative_interface.h Line 414 in 4ab3fdc
Being a C API, the user would expect to be able to do: gd_variant_t return_value; // Let's pretend gd_variant_t is defined as a C opaque struct of sizeof(Variant)
GDNativeBool valid;
godot_api->variant_evaluate(p_op, p_a, p_b, &return_value, &valid) This will work fine as long as the compiler does zero initialization, which is not guaranteed in C (unlike for C++). This are getting worst when doing ptrcall on Godot classe method given some of them returns a Lines 182 to 184 in 4ab3fdc
There is no destructor for godot/core/templates/cowdata.h Lines 413 to 415 in 4ab3fdc
...which will lead to a segfault :'(
So to summarize, forcing zero-initialization may seem a good rule of thumb to be safe when using GDExtension API, however:
3 - Zero-initialization on C is hard !On top of that doing zero initialization is suprizingly error prone in C: gd_variant_t a;
memset(&a, 0, sizeof(a)); However I've just written a buggy code because it should be written Also C++ programmer are often eager to use Of course it depends on how The most obvious way of defining an opaque structure would be to make it include a singe char buffer field: struct gd_variant_t {
char opaque[3];
}; So now given we have only one attribute in the structure we can do this fancy 4 - Bonus: Correctly implementing return value as parameter is tricky !1 - In GodotConsidering how godot/core/extension/gdnative_interface.cpp Lines 82 to 89 in 57bdddc
Line 89 we use gd_variant_t ret;
memset(&ret, 0, sizeof(gd_variant_t));
for (int i = 0; i++; i < 10) {
// No need to put memset&destructor in the loop given `gdnative_variant_call` takes care of that !
godot_api->gdnative_variant_call(&self, &method, NULL, 0, &ret, NULL);
}
godot_api->variant_destroy(&ret); And now we've leaked data :'( 2 - In native extensionGDExtension allows user to register they own native extension, which should provide call&ptrcall function pointers for each methods: typedef void (*GDNativeExtensionClassMethodCall)();
typedef void (*GDNativeExtensionClassMethodPtrCall)();
static void my_extension_do_something_call(void *method_userdata, GDExtensionClassInstancePtr p_instance, const GDNativeVariantPtr *p_args, const GDNativeInt p_argument_count, GDNativeVariantPtr r_return, GDNativeCallError *r_error) {
GDNativeCallError error
// Oups ! we forget to call `godot_api->variant_destroy(r_return)`, now we leak memory !
godot_api->variant_constructor(GDNATIVE_VARIANT_TYPE_DICTIONARY, r_return, NULL, 0, &error);
// Continue to populate the dictionary
}
static void my_extension_do_something_ptrcall_v1(void *method_userdata, GDExtensionClassInstancePtr p_instance, const GDNativeTypePtr *p_args, GDNativeTypePtr r_ret) {
// r_ret is already a valid Dictionary given it has been initialized by the caller, let's use it right away...
// ...but this is only true when the caller is Godot, if the caller is another native extension r_ret might just be
// zero-initialized and we will get a segfault when using it :(
}
static void my_extension_do_something_ptrcall_v2(void *method_userdata, GDExtensionClassInstancePtr p_instance, const GDNativeTypePtr *p_args, GDNativeTypePtr r_ret) {
// Oups ! we forget to call dictionary destructor first ! Dictionary does heap alloc on init so we most likely leak memory !
godot_dictionary_constructor(r_ret);
// Continue to populate the dictionary
} So the rule of thumb for native extension developer is "always start by calling destructor on the return value" which seems 5 - What can we do about this ?So to summarize:
I see three possibilities to fix this, by order of simplicity: 1 - Always use constructor policyThe rule of thumb becomes:
This is the cheapest solution given we only have to add huge stickers everywhere to tell about the rule of thumb (typically in GDExtension documentation when it will be available and in Of course the downside is that it leaves the burden on GDExtension users and native extension developer. 2 - Don't do allocation on initThis is a variation of the previous, the idea is to guarantee that all builtins don't do allocation on init. This way not calling the destructor is ok as long as the object has not been used yet. This has the added benefit of allowing to do initialization with zero-initialization instead of calling the constructor (but as we saw, this is error prone so I'm not sure how much of a benefit this is 😄 ) The main benefit is it remove the necessity for native extension developer to call the destructor on return value. However removing allocation on init might cause issue with how array/dictionnary behave... 3 - Stick with the C convensionThis is the approach I originally implemented in this PR. We would have to make sure the GDExtension API only expose functions The trick is, as @vnen said, we have to make sure this C API stays isolated and doesn't leak in the rest of the codebase. This is obvious enough for most of the GDExtension API given it return value type is a The drawback is the increased complexity of the code base and the size of the final binary to have this second flavor of ptrcall :/ 4 - Modify GDExtension API to never pass return value by parameterThe issue here comes from the fact we pass the return value as parameter. If we were returning the return value we wouldn't have this issue in the first place ! 6 - ConclusionIt's 2am, I've spent ~8h on this topic and I end up concluding GDExtension has already implemented the "least worst" solution and there is nothing to do here, talking of wasted time 😭 More seriously I hope this research work will help when writing GDExtension documentation to provide the right guidelines information. I guess we can start by adding a comment about that in |
1407ab6
to
9b2c844
Compare
1 - More graphAfter a good night of sleep, here is an exaustive view of all the places we use return value as pointer in GDExtension:
Column "part of The "destructor first called on return value" column should be understood as "can this segfault if I pass an uninitialized return value ?". The "GDExtenion specific" column indicate the functions that are only implemented for GDExtension (and hence where we are free to change it behavior without impacting the rest of the Godot internals). A couple of remarks:
The updated conclusionProvided my study is right, it is currently very hard to tell when return value should be initialized and when it should not. However I think we can make this API much more consistent by doing some small changes:
This way we would have a much more explicit and consistent API ;-) The update PR \o/I've updated this PR to implemented the improvement I was talking about. |
rebased |
606b463
to
984ead4
Compare
984ead4
to
5a42665
Compare
5a42665
to
f88ccc5
Compare
@YuriSizov I've seen you've marked changed the milestone for this PR to 4.1 I'm okay with that (I've already talked with @akien-mga during FOSDEM 🇧🇪 🍻 about the fact GDExtension in Godot 4.0 will be considered beta and so breaking changes are allowed) However I'd just want to point out the actual breaking changes this PR contains: the GDExtension code written before this PR should still work but will leak a small amount of memory when calling certain GDExtension API function (e.g. This should be easy to fix for GDExtension code writer (especially given documenting which return value passed as parameter should be pre-initialized or not is precisily the point of this PR 😃 ) |
Even if it contains breaking changes, at this point we don't include any breaking changes in 4.0. GDExt is expected to be expanded upon in 4.1, so that's okay. |
Discussed at the GDExtension meeting and we think it makes sense. Just a rebase needed and fixes for the CI errors Also, @Bromeon will check with the Rust binding and see if this causes any issues |
b7dc1e2
to
2796b93
Compare
2796b93
to
426f8df
Compare
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, and approved! (As that's what we agreed at the GDExtension meeting on 2023-04-24)
While I was looking at it again, I noticed that the wording on the comment explaining the new typedefs could be improved, so if you have time to update that, it'd be great. But we could always fix it up afterwards too, I don't necessarily want that to hold up committing this just because of the comment.
@Bromeon It would still be great if could test this with the Rust bindings, if you have time!
And, if I've understood correctly, this will lead to a small number of situations where godot-cpp could leak memory until its updated? Is there there already a follow-up PR to address that?
I ran our CI against this commit, however there seem to be changes in typedef void *GDExtensionVariantPtr;
typedef const void *GDExtensionConstVariantPtr;
+ typedef void *GDExtensionUninitializedVariantPtr;
typedef void *GDExtensionStringNamePtr;
typedef const void *GDExtensionConstStringNamePtr;
+ typedef void *GDExtensionUninitializedStringNamePtr;
typedef void *GDExtensionStringPtr;
typedef const void *GDExtensionConstStringPtr;
+ typedef void *GDExtensionUninitializedStringPtr;
typedef void *GDExtensionObjectPtr;
typedef const void *GDExtensionConstObjectPtr;
+ typedef void *GDExtensionUninitializedObjectPtr;
typedef void *GDExtensionTypePtr;
typedef const void *GDExtensionConstTypePtr;
+ typedef void *GDExtensionUninitializedTypePtr;
typedef const void *GDExtensionMethodBindPtr;
typedef int64_t GDExtensionInt;
typedef uint8_t GDExtensionBool; The reason is that we don't treat those typedefs as |
@dsnopek I've updated my PR with your comments suggestions 😃
I'd say this is a really good thing to avoid mixing errors ! As a matter of fact I wonder if there is a way to do the same thing in C (this is outside of the scope of this PR of course, but would be an interesting point to bring at next GDExtension meeting 😄 ) Maybe by declaring dummy struct types then only using pointer on them: // Not the actual types ! You should never build these types directly !
struct __GDExtensionVariantPlaceholder {}
struct __GDExtensionStringPlaceholder {}
...
typedef __GDExtensionVariantPlaceholder*GDExtensionVariantPtr;
typedef __GDExtensionStringPlaceholder*GDExtensionStringPtr;
... What do you think ? |
… in GDExtension API This commit introduce separate types (e.g. GDNativeStringPtr vs GDNativeUninitializedStringPtr) depending on if the pointed data is already initialized (C++ style where constructor is alway called when create a variable even if it is to be passed as return value) or not (C style). On top of that, small changes has been made to `GDNativeInterface` so that it methods are consistent on using uninitialized return value.
426f8df
to
e785dd9
Compare
Thanks! |
To be honest, I was surprised that this PR was merged too quickly. You can see in the comments above that the committer of Rust binding was showing direct concern about the changes. The author of this PR was asking about how the changes could be handled on Rust-side. The contributor of GDExtension was also kindly asking about the same issue on the contributors chat (link). I personally don't write Rust, I am just a godot-cpp user, but I believe that the Rust binding is an important part in the GDExtension ecosystem. I don't think that the PR should have been merged before receiving a reply from the participant. |
Yes, would have been nice to test it -- this has been open for 3 years, I'm sure a couple of days wouldn't have mattered 😉 That said, it's less of direct concern I had, but more that it would have required some work until I can test it. If we face problems as a result of this PR, I will definitely report them here, and I'm optimistic that Godot devs are willing to address them 🙂 |
I interpreted @Bromeon's comment as highlighting that some work will be needed on the Rust bindings side, but not that anything is wrong per se with this PR. This is the development branch for 4.1, so if a PR merged upstream means some work will be needed downstream, it's not a blocker in general - unless there's reasonable concern that the downstream changes will not be possible. The further reply from @touilleMan outlined that the way Rust handles this would be a good improvement to make for C too if possible, in a follow up PR. So I took this as a hint that this PR is fine to merge for now, Rust bindings will need to do some extra changes, and further work may be done to unify things further. If I misinterpreted and merged too fast, I'm sorry. But there's also #76406 which was building upon this and waiting for a merge, and I keep hearing that GDExtension PRs don't get merged fast enough, so... |
@akien-mga Thanks for spending your time investigating this issue. I truly appreciate that the PR was merged for now, and I have no further argument if @Bromeon has no concern about the future work required on Rust side.
The issue regarding the merging process of GDExtension is a broader matter. It's not limited to this specific PR. I can't pretend not to be surprised if some PRs are going to be merged suddenly whereas other PRs which had received enough attentions are kept unhandled. Many PRs related to GDExtension needs more attension, and I believe the real problem is the lack of the maintainers. Recently on #gdextension channel in Godot Contributors Chat, I have proposed to promote some contributor to maintainer. Doing so will fundamentally improve the situation, and the change will motivate people (like me) who are trying to submit new patches. I've got positive responses from several people (including the people from GDExtension team), but the actual movement has not happened yet. |
@saki7 There is no need to do such a promotion. Anyone is able to, and welcome to, contribute to the engine. Anyone can review PRs. Regardless of whether or not the review is from a trusted member, additional effort towards PR reviewing is always appreciated. If someone continuously puts in effort, their efforts will be noticed. If you want things to move forward, convince the people on the GDExtension team to leave PR reviews on GitHub, not just positive responses on RocketChat. |
@aaronfranke I know that's how OSS works. I'm afraid there's some misunderstanding, and the actual situation is worse than you think. Let me clarify the recent situation of GDExtension team:
This is why I'm repeatedly asking for promotion. GDExtension team is also aware of the situation, and people are trying to advance. You could check the log for This is getting off-topic, so please don't reply directly on this PR. If you need to ask me something, I'm also available on the Contributors Chat. |
284: Compatibility with 4.1 GDExtension API r=Bromeon a=Bromeon This pull request migrates gdext to the **major GDExtension API change**, as introduced in godotengine/godot#76406. It also adopts to the new "uninitialized pointer" types, which were added in godotengine/godot#35813. Doing so renders the CI green again, unblocking other PRs that have been broken by the upstream changes. --- # Major GDExtension API update TLDR of Godot's changes: instead of one giant `GDExtensionInterface` struct with all the function pointers as data members, the C API now offers only a single function pointer `get_proc_address`. This one is passed to the entry point, in place of a pointer to the interface. With `get_proc_address`, it is possible to retrieve concrete API functions. In C++, this is done by looking up via name and casting the result to the correct function pointer type: ```cpp auto variant_call = (GDExtensionInterfaceVariantCall) get_proc_address("variant_call"); variant_call(...); ``` On the Rust side, I incorporated these changes by only modifying the FFI layer (`godot-ffi` crate), so the rest of the project is mostly unaffected. This is done by generating a `GDExtensionInterface` struct very similarly to how it existed before, in `godot-codegen`. As input, we parse the `gdextension_interface.h` header's documentation metadata. This works well so far, but we'll need to make sure with Godot devs that the doc metadata doesn't suddenly change format. --- # Uninitialized pointer types Certain FFI functions construct objects "into an uninitialized pointer" (placement new in C++). There used to be quite some confusion regarding _which_ functions do that. As a result, the C API introduced new types such as `GDExtensionUninitializedStringPtr`, which represent the uninitialized counterpart to `GDExtensionStringPtr`. These typedefs are declared as `void*` in the C API, but we turn them into strongly typed opaque pointers by post-processing the C header. As such, it is impossible to accidentally mix initialized and uninitialized pointer types. I also added a trait `AsUninit` which allows explicit conversion between the two. This may not be necessary in the future, but is helpful until we are sure about correct usage everywhere. I marked some places that may need another look as `// TODO(uninit)`. --- # Compatibility between Godot 4.0.x and 4.1+ Due to the API changes in GDExtension, extensions compiled under Godot 4.0.x are by default **not compatible** with Godot 4.1+ (which includes current `master` versions of Godot, so also our nightly CI builds). From now on, the `.gdextension` interface must contain a new property `compatibility_minimum`: ```toml [configuration] entry_symbol = "gdext_rust_init" compatibility_minimum = 4.0 [libraries] linux.debug.x86_64 = "res://../../../target/debug/libdodge_the_creeps.so" ... ``` This value must be set to `4.0` if you compile against the old GDExtension API (current gdext uses 4.0.3 by default). Set it to `4.1` if you use a Godot development version (Cargo feature `custom-godot`). If you do this wrong, gdext will abort initialization and print a descriptive error message. ## Compat bridge Since breaking changes can be very disruptive, I built a bridging layer that allows to use existing compiled extensions under Godot v4.1 (or current development versions). Because the Rust code relies on certain features only available in the newer C header (e.g. uninitialized pointers), such changes were backported via dynamic post-processing of `gdextension_interface.h`. Therefore, you don't need to mess around with `custom-godot`, just keep using gdext as-is and set `compatibility_minimum = 4.0` to run it under newer engine versions. Developing against a specific GDExtension API is still possible via patch, the prebuilt artifacts have been updated for all 4.0.x versions. For example, to use version `4.0`: ```toml [patch."https://github.com/godot-rust/godot4-prebuilt"] godot4-prebuilt = { git = "https://github.com//godot-rust/godot4-prebuilt", branch = "4.0"} ``` Once Godot 4.1.0 is released, we will likely make that the default version used by gdext, but I plan to maintain the compat layer as long as this is possible with reasonable effort. A new CI setup verifies that the integration tests run against 4.0, 4.0.1, 4.0.2, 4.0.3 and nightly Godot versions, at least for Linux. This will become a challenge as soon as there are breaking API changes (and there is already one upcoming in `Basis::looking_at()`). --- # Other changes There is now an experimental `godot::sys::GdextBuild` struct, which provides informations about static (compiled-against) and runtime (loaded-by) Godot versions. This may be extended by other build/runtime metadata and will likely be promoted to an official API in the `godot::init` module. Several memory leaks that came up during the change to uninitialized pointers were addressed. In many places, we now use `from_sys_init()` instead of `from_sys_init_default()`, avoiding the need to construct throwaway default instances. In addition to more CI jobs for integration tests, there are now also 2 more linux-memcheck ones, to cover both 4.0.3 and nightly. This is mostly to have extra confidence during the migration phase and may be removed later. Godot 4.0.3 is now the default version used by gdext. Co-authored-by: Jan Haller <bromeon@gmail.com>
fix #34264
ptrcall always overwrite the return value buffer with it own return value
data. Hence a non initialized buffer should be provided to ptrcall.
I've tested this with code like this:
without the fix this endup in a segfault, with this fix it works fine
I guess we should be double careful before merging this given ptrcall is a strange beast and my knowledge is limited on it ;-)