-
-
Notifications
You must be signed in to change notification settings - Fork 21.1k
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
Implement reloading of GDExtensions #80284
Conversation
a29104e
to
be63e0f
Compare
be63e0f
to
a4f5780
Compare
Looks fantastic! Some comments on your text:
Maybe this can be something in the configuration file, so its easy to change for development.
For GDScript maybe when running the editor, it should just not cache the MethodBinds of extension classes that are marked as reloadable (extension could mark this in ClassDB) and use the more traditional call pathway. This would avoid breaking them @vnen ?
I think this is kind of unavoidable. In large part, this is why the editor avoids using threads for most stuff.
Well, its for when you run the game and not the editor, as examples:
Unless, of course, you want to actually reload extensions on the running game, which may be cool but not clear if desired.
If you have another extension that inherits classes from this one, then you want to keep the inheritance chain properly in ClassDB and not remove it during reload. |
Thanks!
I have tested it game, and it does work with my very simple test project. :-) However, I agree that in game is harder with a more complex project, so, I'll add a flag to disable it there.
Hm, this is a very tricky case to solve. What happens if the GDExtension that provides the parent class reloads, and that class doesn't come back? Can we even support reloading in this context? The more I think about it, I think GDExtensions should have to opt in to reloading (I'm thinking a I think this should be OK since reloading is only used when developing the GDExtension, and during that phase you probably don't actually want other GDExtension extending its classes. After you're no longer actively developing the "parent GDExtension", you can disable reloadability, and then other GDExtensions can extend its classes again. |
If it does not come back, I would still leave the class around. This is why I also suggested leaving the MethodBinds around even though invalidated in ClassDB. But I think this is a pessimistic use case. I mean, obviously it would not work properly if you remove the class but, if you think about it, this is a very corner case. The reason you leave it in ClassDB is mainly because the most common case is that you will reload the class and it will come back, hence those inheriting from it should continue working. The feature is more geared to the optimistic use case, which should be the norm. |
Sure. But I'm not convinced that supporting this one use case (reloading a GDExtension that has another GDExtension extending one or more of its classes) is compelling enough for all the added complexity. So, I'm going to leave this out of the PR for now, and focus on finishing all the rest of the things that need to get done (which is still quite a lot). If further testing/review shows that this is really needed, I can always add it later, before the PR is merged :-) |
@dsnopek I think its not very difficult, but if it makes things easier for you go ahead, can always be added later. |
c76792f
to
90a8fcc
Compare
6a230a2
to
4a1dca8
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.
Went through the code together with David to do a bit of a deep dive.
This is looking really good, all makes sense and so far testing of this logic give a solid feel.
Suggest we merge this before the feature freeze, this is going to be a game changer for many GDExtension developers.
4a1dca8
to
2733a6f
Compare
Thanks! Amazing work 🎉 |
@dsnopek Any way to experimentally enable this again to see how feasible game hot-reloading is with GDExtension? |
No, although, I think it would be fairly easy to implement. Here's what I think it would take:
I suspect there's some areas of the code that do If I can find the time, I'll throw together a draft PR. However, if anyone else is motivated to give it a try, that'd be awesome :-) |
@dandersch I just posted draft PR #97991 which does the basics I described above, but needs testing and probably some fixes on Windows |
This is a
draftPR adding "live" reloading of GDExtensions!This would (partially) fix #66231
In order to work with godot-cpp, you need to use the companion PR godotengine/godot-cpp#1200
What works in my limited testing on Linux/Windows
_process()
and after reload it'll immediately start printing the new text!)How to test
GDExtensionClassCreationInfo2::recreate_instance_func
which is used to create a new object defined by the GDExtension for a pre-existing object on the Godot side.reloadable = true
to your .gdextension fileDownloading pre-compiled binaries:
TODO:
UPDATE: I think the remaining TODO's can be done in follow-ups. What's in this PR should provide a working basis that can be built upon!
Actual "live reloading" where the editor detects that the shared library has been updated and automatically initiates the reload. In the current PR, you have to manually callGDExtensionManager.reload_extension(path)
With the previous point, I think we probably want to allow GDExtensions to mark themselves as not being safe to reload. Or maybe each GDExtension should opt in? It's really only the developer of the GDExtension that wants live reloading - users of third-party GDExtensions probably don't want or need this, especially given all the extra accounting necessary to make it work.Right now, it will update an existing method bind that has the same name, regardless of any similarity to the original method. We should probably do some sort of "compatibility check" and only reuse method binds if they have the same signature or something?Right now, it's trashing all theObject::_instance_bindings
when clearing out the GDExtension data from an Object before reloading, but we really should be keeping instance bindings from other extensions that may be wrapping the objectThis PR adds a new property on theGDExtensionClassCreationInfo
struct ingdextension_interface.h
in a way that breaks compatibility. We've already discussed how to handle these sort of situations at GDExtension meetings in the past, I just need to update the PR to do it the right way :-)Differences from @reduz's proposal:
This implementation is based on this proposed implementation from Juan:
https://files.godot.foundation/s/WcyHmRjLbsggyGL
However, there are a number of differences in this PR, which I'd like to explain!
I didn't include this, because I'm not entirely sure what this is for and how it should work. What determines ifEngine::is_extension_reloading_enabled()
should return true or false?Right now, all the extra accounting necessary to make this work will only kick in where a GDExtension is actually in use, so it shouldn't have any effect if you're not using any GDExtensions. If we do add a way for GDExtensions to opt into reloading, we could make sure it isn't adding anything in that case either.UPDATE: This is included now.
There's quite a bit in the proposal around making sure the class remains in the
ClassDB
(but marked special) during the reload process. I don't think this is actually necessary:GDExtension
class intercepts all theregister_extension_class()
,register_extension_class_method()
andunregister_extension_class()
calls from the GDExtension before calling intoClassDB
, my PR handles all the special cases for re-registering inGDExtension
-ClassDB
doesn't need to know about itClassDB
and then re-appearing a moment later is actually fine. We're clearing out theObject::_extension
andObject::_extension_instance
on all instances, so during this window when the class isn't inClassDB
, an existing instances will simply act like their native parent class. So, if your extension class extendsNode2D
, it will revert to acting like a plain oldNode2D
for a moment. The main thing that changes by the class not being inClassDB
, is that it can't be instantiated, but I think that's actually a good thing.ClassDB
but marked it special, we'd need to add special cases all over the place to make sureClassDB
didn't do something unsafe while the class was reloading. But if the class just isn't there,ClassDB
is already checking for that everywhere.Only testing and more time will tell if I'm right or not, though :-)
I attempted to implement this, but it won't actually work. For
ObjectGDExtension::create_instance
this approach would work just fine, because the function pointer from the GDExtension will return aGDExtensionObjectPtr
(which is really just anObject *
), so we can get ahold of the Godot-side instance. ButObjectGDExtension::free_instance
is passed aGDExtensionClassInstancePtr
which is the GDExtension-side instance, and that isn't what we need.So, instead I added some additional function pointers on
ObjectGDExtension
for tracking.The day after sending his proposal, Juan came back and said he thinks this might not be necessary, so my PR is leaving it out for now to see how it works.
@vnen has a separate PR (#80188) implementing this.