X Tutup
Skip to content

Expand GDType to cover GDExtension types as well#106016

Merged
Repiteo merged 1 commit intogodotengine:masterfrom
Ivorforce:gdtype-loves-gdextension
Dec 3, 2025
Merged

Expand GDType to cover GDExtension types as well#106016
Repiteo merged 1 commit intogodotengine:masterfrom
Ivorforce:gdtype-loves-gdextension

Conversation

@Ivorforce
Copy link
Member

@Ivorforce Ivorforce commented May 2, 2025

GDType is explained in the corresponding proposal: godotengine/godot-proposals#12323.

This PR expands GDType to cover GDExtension added classes as well, by swapping the pointer when a GDExtension subtype is assigned to the Object. This eliminates branching and separate computation in typing functions such as is_class.

This should be merged relatively soon after, or even together with #105793, to eliminate the possibility of GDType being used in contexts assuming internal types only.

Explanation

Object instances can get assigned a GDExtension 'subtype'. According to Godot's typing logic, this makes the Object's new type the GDExtension's supplied type, i.e. a subtype of the builtin type. You can read more about this general idea here: godotengine/godot-proposals#12155

Note: Instances are usually assigned a GDExtension instance exactly once, right after the Object is created. When this happens, all we have to do is use the GDExtension's GDType instead of the builtin one.

@IphStich
Copy link
Contributor

I can confirm that this PR increased type-checking performance using is_class (at least in my test scenarios). I didn't do the most scientific testing, so don't wish to report the numbers, but it is a definite improvement. (interestingly, it seems GDScript's is operator doesn't run any faster, so might be we need to look at optimizing that?)

The duplication of data and relationships irks me a bit, but I imagine it's out of scope of this PR to look at changing these. Looking forward to the ClassDB cleanup! :)


I don't understand the purpose of clearing _gdtype_ptr in Object::_predelete(). This results in Object::get_gdtype() returning _get_typev(). Which should be it's value in most cases anyway. Which would mean that for the duration of the Dispose() call, C# wouldn't have access to accurate type information? (Just a lot of stuff I know nothing about tho. And I felt it was necessary to express my concern and lack of knowledge.)

I feel like it should be set during the Object constructor, instead of during initialization? This would allow us to get rid of the check in get_gdtype() (saving a few CPU cycles) and ensure that it is always valid. It also doesn't really need to be set to nullptr during destruction.


This is the first time I saw the GDType changes! Looks like a solid start! :D

Would it be worthwhile changing these to static variables and using static constructors for in-built classes? That way we don't have to dynamically allocate memory for them, and only have to initialize the properties.


PR looks good overall!

Only major issue is the possible memory leak concern. Maybe some confusion/concern around _reset_gdtype().

The ClassDB changes look good! I'm really excited for the future of this! :D

@Ivorforce
Copy link
Member Author

Thanks a lot for the review!

I can confirm that this PR increased type-checking performance using is_class (at least in my test scenarios). I didn't do the most scientific testing, so don't wish to report the numbers, but it is a definite improvement. (interestingly, it seems GDScript's is operator doesn't run any faster, so might be we need to look at optimizing that?)

The singular branch is unlikely to be a bottleneck, especially when compared to some of the other logic slowing these functions down. So I wouldn't expect any observable performance improvements just from this change, especially on hot execution.

I don't understand the purpose of clearing _gdtype_ptr in Object::_predelete(). This results in Object::get_gdtype() returning _get_typev(). Which should be it's value in most cases anyway. Which would mean that for the duration of the Dispose() call, C# wouldn't have access to accurate type information? (Just a lot of stuff I know nothing about tho. And I felt it was necessary to express my concern and lack of knowledge.)
I feel like it should be set during the Object constructor, instead of during initialization? This would allow us to get rid of the check in get_gdtype() (saving a few CPU cycles) and ensure that it is always valid. It also doesn't really need to be set to nullptr during destruction.

It's part of the Object lifecycle, and has been the case for a while. It's essentially a type safety feature, though it's unlikely to be very impactful. The short explanation is that it's part of the C++ contract of object destruction that subtypes leave no traces. For this reason, for example, vtables are exchanged with supertypes' vtables after each subtype is done destructing.

It's possible we can change this eventually, but now is not the time.

Would it be worthwhile changing these to static variables and using static constructors for in-built classes? That way we don't have to dynamically allocate memory for them, and only have to initialize the properties.

Built-in classes are already created statically, the only classes that are created dynamically are those created from GDExtension (which this PR focuses on).

Copy link
Contributor

@dsnopek dsnopek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

For the most part this looks great :-)

However, I tested it with hot reload, and it seems to work, but I get 3 errors printed when it reloads:

  ERROR: core/object/object.cpp:234 - Condition "gdtype" is true.
  ERROR: core/object/object.cpp:234 - Condition "gdtype" is true.
  ERROR: core/object/object.cpp:234 - Condition "gdtype" is true.

I think we might be missing some place where we should be disposing of the gdtype in preparation for reload? Perhaps it should be deleted in GDExtension::_clear_extension()?

@Ivorforce Ivorforce force-pushed the gdtype-loves-gdextension branch from bf9104f to 0e7168f Compare October 14, 2025 14:43
@Ivorforce
Copy link
Member Author

Ivorforce commented Oct 14, 2025

@dsnopek Thanks a lot for testing! I had another look, and it seems like I failed to realize that GDExtension::_register_extension_class_internal is capable of hot reload.
For now, there are no properties that GDType needs to hot reload, so I was able to fix the issue simply be removing the create_gdtype call entirely when the function is called as a hot reload. But it's good that I know this now, because hot reload will need to be considered for all future additions to GDType.

@dsnopek
Copy link
Contributor

dsnopek commented Nov 1, 2025

Thanks, however, in the latest version of this PR, I'm still getting those errors on reload.

I suspect the right thing to do is to have GDExtension::_clear_extension() delete the GDType from GDExtension, so it'll get recreated. Something like this:

Patch
diff --git a/core/extension/gdextension.cpp b/core/extension/gdextension.cpp
index ad5b6a8442f..8c38ae0be48 100644
--- a/core/extension/gdextension.cpp
+++ b/core/extension/gdextension.cpp
@@ -976,6 +976,8 @@ void GDExtension::_clear_extension(Extension *p_extension) {
 
                obj->clear_internal_extension();
        }
+
+       p_extension->gdextension.destroy_gdtype();
 }
 
 void GDExtension::track_instance_binding(Object *p_object) {
diff --git a/core/object/object.cpp b/core/object/object.cpp
index 2ffea4aefa3..10bd258b707 100644
--- a/core/object/object.cpp
+++ b/core/object/object.cpp
@@ -236,6 +236,19 @@ void ObjectGDExtension::create_gdtype() {
        gdtype = memnew(GDType(ClassDB::get_gdtype(parent_class_name), class_name));
 }
 
+void ObjectGDExtension::destroy_gdtype() {
+       ERR_FAIL_COND(!gdtype);
+
+       memdelete(const_cast<GDType *>(gdtype));
+       gdtype = nullptr;
+}
+
+ObjectGDExtension::~ObjectGDExtension() {
+       if (gdtype) {
+               memdelete(const_cast<GDType *>(gdtype));
+       }
+}
+
 bool Object::Connection::operator<(const Connection &p_conn) const {
        if (signal == p_conn.signal) {
                return callable < p_conn.callable;
diff --git a/core/object/object.h b/core/object/object.h
index 11cb9095855..3438e5ece56 100644
--- a/core/object/object.h
+++ b/core/object/object.h
@@ -374,6 +374,9 @@ struct ObjectGDExtension {
        /// This is not exposed through the GDExtension API (yet) so it is inferred from above parameters.
        const GDType *gdtype;
        void create_gdtype();
+       void destroy_gdtype();
+
+       ~ObjectGDExtension();
 };
 
 #define GDVIRTUAL_CALL(m_name, ...) _gdvirtual_##m_name##_call(__VA_ARGS__)

When I use that, I don't get the errors anymore!

Also, it appears the reload isn't full working with the latest version of this PR (whereas before there were the errors, but it still worked), at least using the same test as I used last time (I've got a Node in the scene with _process() printing something to the log - after the reload it just stops printing). This issue isn't fixed by my patch, and it appears to be broken on master as well - more investigation is needed

@dsnopek
Copy link
Contributor

dsnopek commented Nov 1, 2025

Also, it appears the reload isn't full working with the latest version of this PR (whereas before there were the errors, but it still worked), at least using the same test as I used last time (I've got a Node in the scene with _process() printing something to the log - after the reload it just stops printing). This issue isn't fixed by my patch, and it appears to be broken on master as well - more investigation is needed

This turned out to be a godot-cpp issue godotengine/godot-cpp#1876

@Ivorforce Ivorforce force-pushed the gdtype-loves-gdextension branch from 9b54928 to 1646b32 Compare November 8, 2025 10:30
@Ivorforce
Copy link
Member Author

Ivorforce commented Nov 8, 2025

Thanks, however, in the latest version of this PR, I'm still getting those errors on reload.

Sorry about that! Somehow, I managed to patch the wrong part of the PR with the fix. I've corrected this now. With the latest version, I can hot reload without any error messages (macOS).

I suspect the right thing to do is to have GDExtension::_clear_extension() delete the GDType from GDExtension, so it'll get recreated. Something like this:

Patch
When I use that, I don't get the errors anymore!

Unfortunately, this is not an option. The GDType must persist, because existing Object classes may have a reference to it. Remember that we're doing this because Object should have a pointer to its GDType, to avoid needing to jump through hoops during use to find out what the right thing to do is given its internal subclass, gdextension subclass, and scripting subclass.
GDType will therefore have to support hot reload in the debug builds (or rather, step by step adopt the hot reload implementation that is currently in GDExtension).

@dsnopek
Copy link
Contributor

dsnopek commented Nov 8, 2025

Unfortunately, this is not an option. The GDType must persist, because existing Object classes may have a reference to it.

The reload process goes through all instances and resets the GDType to the one from the native class. Unless you're saying there's something that is getting missed?

But assuming nothing is getting missed, if you look at my patch above, in the first chunk where it's calling destroy_gdtype() is right after it has finished looping over all the instances and and calling clear_internal_extension(), so there shouldn't be any remaining references to the GDType from GDExtension.

@Ivorforce
Copy link
Member Author

Unfortunately, this is not an option. The GDType must persist, because existing Object classes may have a reference to it.

The reload process goes through all instances and resets the GDType to the one from the native class. Unless you're saying there's something that is getting missed?

Oh that's interesting! That totally explains your suggestion.
I'm a bit surprised though. How does exchanging _extension deal with potential multithreading problems? During the brief moment of writing to the _extension field, any reads to _extension might fail.

@dsnopek
Copy link
Contributor

dsnopek commented Nov 8, 2025

How does exchanging _extension deal with potential multithreading problems? During the brief moment of writing to the _extension field, any reads to _extension might fail.

It doesn't - at the moment, reload is unsafe with regard to multithreading. This was one of the "let's get this out now, and fix it later" things. :-) That said, I don't think multithreading issues affect very many users - maybe more often with editor plugins that spawn threads? And they could maybe handle it manually by stopping their threads on NOTIFICATION_EXTENSION_RELOAD

The solution is probably some variation of protecting access to _extension with a mutex, but only if that GDExtension has reload enabled, so we aren't paying for that on extensions that don't need it? Something for another PR or issue, though

@Ivorforce Ivorforce force-pushed the gdtype-loves-gdextension branch from 1646b32 to dbf3ba8 Compare November 8, 2025 12:35
@Ivorforce
Copy link
Member Author

Ivorforce commented Nov 8, 2025

Alright, thanks for clearing that up. I'm totally happy to go with your suggestion then!

The hot reload seems to work for me with your suggestions applied, without any errors printed1.

And they could maybe handle it manually by stopping their threads on NOTIFICATION_EXTENSION_RELOAD
The solution is probably some variation of protecting access to _extension with a mutex, but only if that GDExtension has reload enabled, so we aren't paying for that on extensions that don't need it? Something for another PR or issue, though

An idea i had was to synchronize threads when a reload is scheduled, and only start exchanging extension pointers when they're all paused. That would make hot reload compatibility completely free, but if there's any long running threads, it would take a while to reload.
Anyway, if people haven't been complaining so far, it probably isn't a priority.

Footnotes

  1. I also needed to remove the p_static-ness from GDType class names, otherwise it started printing an error on exit. I'm not sure why it's not complaining about the superfluous non-static StringName entities on exit right now, but I suppose that's for another day to find out.

Co-authored-by: David Snopek <dsnopek@gmail.com>
@Ivorforce Ivorforce force-pushed the gdtype-loves-gdextension branch from dbf3ba8 to aa33b53 Compare December 3, 2025 20:32
Copy link
Contributor

@dsnopek dsnopek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

I re-ran this through some manual hot-reload tests and everything seems to be working fine :-)

@akien-mga akien-mga modified the milestones: 4.x, 4.6 Dec 3, 2025
@Repiteo Repiteo merged commit 662db6e into godotengine:master Dec 3, 2025
20 checks passed
@Repiteo
Copy link
Contributor

Repiteo commented Dec 3, 2025

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

X Tutup