Skip to content

Proposal: Moving from Script API to GDExtension #799

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

Open
CedNaru opened this issue Mar 30, 2025 · 3 comments · May be fixed by #825
Open

Proposal: Moving from Script API to GDExtension #799

CedNaru opened this issue Mar 30, 2025 · 3 comments · May be fixed by #825

Comments

@CedNaru
Copy link
Member

CedNaru commented Mar 30, 2025

Overview

This proposal outlines a major architectural shift for the Godot Kotlin project: transitioning from the current use of the Godot Script API to leveraging the more modern and flexible GDExtension API. This change would transform our Kotlin/JVM integration from a script-based system into an extension-based one, aligning better with the design philosophy of the Godot engine and delivering a more streamlined and maintainable user experience.

Currently, Godot Kotlin is implemented as a Godot module that exports Kotlin classes as scripts. This allows users to attach Kotlin classes to nodes in the editor, similar to GDScript or C#. While functional, this model introduces several constraints and maintenance burdens:

  • Each supported JVM language requires its own glue code and implementation for both Script and Language (Kotlin, Java, GDJ, and potentially Scala).
  • Kotlin/JVM code must be precompiled into .jar files, and the module must map script files to JVM classes at runtime.
  • The Godot Script system is fundamentally designed for interpreted, dynamically typed languages like GDScript or Python—not compiled, static ones like Kotlin.
  • Our use of the Script API diverges significantly from its intended philosophy. In Godot, scripts are meant to be small, portable code units—optionally named, file-based, and easily shared between projects. Our approach, by contrast, relies entirely on the presence of a compiled .jar file. Our scripts are not standalone, and due to the constraints of JVM languages, they cannot be anonymous.

The .gdj system was created as a workaround to provide "scripts" support for classes that don’t come from an existing file in the Godot project but directly from the jar (when coming from an imported library for example)—but in hindsight, this is a hack built on top of a file-centric system.

Scripts do retain some advantages—such as the ability to dynamically switch scripts on a node at runtime—but these are features we do not actively use, and they come at the cost of complexity and performance overhead.

Why Consider Extensions Now?

As Godot's GDExtension API has evolved, it now provides powerful new capabilities that make it a compelling alternative for our use case. Several of its limitations have been fixed since the original release of Godot 4 and there is no obvious drawback to using this approach.

Dynamic Type Registration

Previously, GDExtension required individual function pointers for every method in an extended type. This made it incompatible with dynamic JVM-based systems where script types are discovered at runtime when opening the .jar. The alternative before that changes would be to modify the entry generator to a C++ output and then compile the dynamic library again, which is not an acceptable workflow for JVM devs.

Recent versions of the GDExtension API now allow passing custom data alongside the function pointer. We can use that custom data to dynamically find the correct JNI method to call, just like we currently do with scripts.

Extensions not running in the editor
By default, extensions are all running in the editor, just like regular nodes. There was previously no way for extension to act like "non-tool scripts" (except checking for the editor hint in every single implemented virtual method), but it's no longer the case (only difference is that extension are "tools" by default, but you can opt out). It means we automatically get "Tool mode" support. This also means getting reloading of our "tool scripts" with state restoration because the editor can also handle that (But I think it that so far it can only reload the entire dynamic library, meaning all the jars at once in our case, with no more fine-grained control over that reloading).

Removing complexity
Making a complete move to extension removes the need for any custom build of Godot. Just adding the extension to a project would be enough to dynamically turn any jar into a set of extended types.
It will also remove all the complexity we have built over the years regarding file management:

  • No more Language implementation (currently 4 of them, one for each language, including our dummy GDJ language)
  • No more Script implementation (currently one for each language)
  • No Resource Loader/Saver, the editor won't even need to be aware of the original language, it will only see regular types.
  • No more GDJ generation/refreshing
  • No script reloading, Godot already has its own GDextension reloading system that can recover the state of types currently running in the editor.
  • No UID/partial parsing of script necessary, the editor won't even see the .kt/.java/.scala files.

From Godot’s point of view, the JVM classes just become a built-in engine type—completely language-agnostic.
It goes well with our new ClassGraph implementation. The idea is that any KtClass found in the .jar can become an extended type. It doesn't matter if it was written in Java, Kotlin, Scala and where the source file was initially located. As long as it is present in the jar, it's enough.

Performance Consideration

There has always been some concern about potential performance loss when moving to an extension-based system, given the thin overhead of using a dynamic library instead of statically linked code. While we won’t know for certain until we benchmark it, current investigation suggests the performance impact would be minimal or even negligible in most cases:

  • Godot → JVM calls: Godot will directly invoke function pointers we provide, similar to how C++ uses virtual tables for dynamic dispatch. Expected impact: None.
  • JVM → Godot API calls: These already use direct pointers to MethodBind objects. Expected impact: None at runtime, though startup might be slightly slower due to initialization overhead.
  • JVM → Native Godot Core Types: This introduces one extra indirection layer via a function pointer to the core type methods obtained through the GDExtension interface instead of using the statically linked methods. Expected impact: Light

In practice, this overhead is insignificant. Core types like VariantArray, Dictionary, and PackedArray are already bottlenecked by JNI overhead. Others, like StringName or NodePath, are cached or used infrequently. For the rest, most types are fully implemented in Kotlin and don’t cross the JNI boundary at all.

Why This Makes Sense for Godot Kotlin

In reality, our current architecture already behaves more like an extension system than a script system:

  • Our runtime logic comes from precompiled .jar files, not standalone source scripts.
  • All our scripts are named
  • A single script change require rebuilding the entire jar.
  • User won't have to deal with script files anymore, removing the usual confusion they have with .gdj as well. Their classes will just be visible/usable like any regular type.

The current implementation has required workarounds to fit into a system designed for file-based, interpreted code.
Switching to GDExtension would bring our architecture in line with how we actually operate, reduce technical debt, and provide a cleaner, more scalable foundation for the future.

Conclusion

Initially, the plan was to keep using the Script system—both as a module or an extension. The original vision was to reimplement our current Language/Script classes using GDExtension, but still export user classes as scripts.
However, in the interest of reducing maintenance burden, supporting all major JVM languages cleanly, and keeping the project’s scope manageable, I’ve come to believe that we should fully embrace the extension model.
If we adopt this proposal, our goal will be to shift to a dynamic library that converts exported KtClass instances into proper extended types via a smaller C++ layer.

@CedNaru CedNaru pinned this issue Mar 30, 2025
@CedNaru
Copy link
Member Author

CedNaru commented Mar 30, 2025

To give an idea of what code it removes:

Image

All those directories would disappear. Note that all the GDJ generation logic in the entry-generator would be removed as wellas they are no longer necessary.

@CedNaru
Copy link
Member Author

CedNaru commented Apr 9, 2025

It's not all perfect.
As it is right now, there is quite a bit of features we would be using switching to pure extension.
While each individual loss may seem minor, their combined effect impacts the developer experience in significant ways.

  • Quality-of-Life features lost with Extensions: Source file visibility: Kotlin/Java/Scala source files currently appear in the Godot editor, allowing for quick inspection without switching to an external IDE.

  • Drag-and-drop attachment: Script files can be dragged onto nodes to quickly assign behavior without having to explore a list of type.

  • Script creation templates: New scripts can be created using language-specific templates directly in the editor.

  • Source change detection: The system warns when source files have changed, but the project hasn't been rebuilt—this would no longer apply when using extensions.

It's certainly possible to reimplement those feature to some extent manually with a few editor plugins, but it would be more complex than just overriding a few virtual methods from the Language class than today.

The source change is particularly nasty, the Godot editor only refreshes imported resource (which scrips are not), scripts global names and already loaded resources when a file is changed.
As extension, the source file would no longer be recognized as script or resources, so we can't even hook into the EditorFileSystem signals to check for changes when modifying code from an external idea.
Not impossible to overcome, but the more obvious solutions feel a bit hacky to me.

@CedNaru
Copy link
Member Author

CedNaru commented Apr 9, 2025

We might see improvements on those points in the near future if Godot takes the direction mentioned in this discussion.

In the long term, we might have no other choice than using the extension system, so it makes the choice easier.

@CedNaru CedNaru linked a pull request Apr 23, 2025 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant