Skip to content

Proposal: Moving from Script API to GDExtension #799

Open
@CedNaru

Description

@CedNaru

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions