Godot + GDNative + PluginScript: the groundwork

Versão em Português

In the last post we talked about the design of a plugin for using Lua in the Godot game engine. Today we'll start implementing our plugin with the barebones infrastructure: a GDNative library that registers Lua as a scripting language for Godot. The scripting runtime won't work for now, but Godot will correctly load our library and recognize .lua files.

How to GDNative

Let's start building an empty GDNative library. These are shared libraries (DLLs) that are loaded at runtime by the engine. They must declare and export the functions godot_gdnative_init and godot_gdnative_terminate, to be called when the module is initialized and terminated, respectively.

GDNative libraries are loaded only when needed by the project, unless they are marked as singletons. Since we want ours to be loaded at project startup, so that Lua scripts can be imported, we'll make it a singleton. For this, we also need to declare a function called godot_gdnative_singleton, or Godot won't load our library. The downside of using singleton GDNative libraries is that we'll have to reopen the editor each time we recompile it.

Ok, time to start this up!
First of all, let's download the GDNative C API repository. Since I'm using Git for the project, I'll add it as a submodule. I'm using the lib directory for maintaining all third-party libraries at the same place.

git submodule add https://github.com/godotengine/godot-headers.git lib/godot-headers

The GDNative API is very low-level, so I'll also be using my own High Level GDNative C/C++ API (HGDN):

git submodule add https://github.com/gilzoide/high-level-gdnative.git lib/high-level-gdnative

We'll be storing our source files in the src folder, for organization. Here is the skeleton for our GDNative module source, in C:

// src/language_gdnative.c

// HGDN already includes godot-headers
#include "hgdn.h"

// GDN_EXPORT makes sure our symbols are exported in the way Godot expects
// This is not needed, since symbols are exported by default, but it
// doesn't hurt being explicit about it
GDN_EXPORT void godot_gdnative_init(godot_gdnative_init_options *options) {
    hgdn_gdnative_init(options);
}

GDN_EXPORT void godot_gdnative_terminate(godot_gdnative_terminate_options *options) {
    hgdn_gdnative_terminate(options);
}

GDN_EXPORT void godot_gdnative_singleton() {
}

Since HGDN is a header-only library, we need a C or C++ file for compiling its implementation. We could use src/language_gdnative.c for this, but I'll add a new file for it to avoid recompiling HGDN implementation on future builds:

// src/hgdn.c
#define HGDN_IMPLEMENTATION
#include "hgdn.h"

Time to build! 🛠
I'll be using xmake as build system, because it is simple to use and supports several platforms, as well as cross-compiling, out of the box. Also, it has a nice package system integrated that we'll use for embedding Lua/LuaJIT later. The xmake.lua build script is as follows:

-- xmake.lua
target("lua_pluginscript")
    set_kind("shared")
    -- Set the output name to something easy to find like `build/lua_pluginscript_linux_x86_64.so`
    set_targetdir("$(buildir)")
    set_prefixname("")
    set_suffixname("_$(os)_$(arch)")
    -- Add "-I" flags for locating HGDN and godot-header files
    add_includedirs("lib/godot-headers", "lib/high-level-gdnative")
    -- src/hgdn.c, src/language_gdnative.c
    add_files("src/*.c")
target_end()

Run xmake and, if all goes well, we should have a .so or .dll or .dylib shared library in the build folder.

Time to open Godot.
I've created a new project and added our module repository at addons/godot-lua-pluginscript. To make the FileSystem dock cleaner, I also added .gdignore files in the build, lib and src folders. Now we need to create a new GDNativeLibrary Resource:

Set it as singleton:

And set the path to the shared library we just built:

Restart the editor and our module should be imported. Nice!

How to PluginScript

If we look at the PluginScript API, there is only one function defined, responsible for registering scripting languages based on a description. This description has information about the language name, file extensions, information used for syntax highlighting in the code editor such as reserved words, comment delimiters and string delimiters, as well as several callbacks that Godot will call, e.g.: for initializing/finalizing our language runtime, scripts and instances, debugging and profiling code.

All we have to do is create the required callbacks, fill in the description and register Lua as a scripting language! For now we'll just add stubs for the plugin to be loaded, in the next post we'll start implementing these callbacks. We'll also skip the optional callbacks for now.

Add the following in src/language_gdnative.c, just below the initial #include "hgdn.h":

// Called when our language runtime will be initialized
godot_pluginscript_language_data *lps_language_init() {
    // TODO
    return NULL;
}

// Called when our language runtime will be terminated
void lps_language_finish(godot_pluginscript_language_data *data) {
    // TODO
}

// Called when Godot registers globals in the language, such as Autoload nodes
void lps_language_add_global_constant(godot_pluginscript_language_data *data, const godot_string *name, const godot_variant *value) {
    // TODO
}

// Called when a Lua script is loaded, e.g.: const SomeScript = preload("res://some_script.lua")
godot_pluginscript_script_manifest lps_script_init(godot_pluginscript_language_data *data, const godot_string *path, const godot_string *source, godot_error *error) {
    godot_pluginscript_script_manifest manifest = {};
    // All Godot objects must be initialized, or else our plugin SEGFAULTs
    hgdn_core_api->godot_string_name_new_data(&manifest.name, "");
    hgdn_core_api->godot_string_name_new_data(&manifest.base, "");
    hgdn_core_api->godot_dictionary_new(&manifest.member_lines);
    hgdn_core_api->godot_array_new(&manifest.methods);
    hgdn_core_api->godot_array_new(&manifest.signals);
    hgdn_core_api->godot_array_new(&manifest.properties);
    // TODO
    return manifest;
}

// Called when a Lua script is unloaded
void lps_script_finish(godot_pluginscript_script_data *data) {
    // TODO
}

// Called when a Script Instance is being created, e.g.: var instance = SomeScript.new()
godot_pluginscript_instance_data *lps_instance_init(godot_pluginscript_script_data *data, godot_object *owner) {
    // TODO
    return NULL;
}

// Called when a Script Instance is being finalized
void lps_instance_finish(godot_pluginscript_instance_data *data) {
    // TODO
}

// Called when setting a property on an instance, e.g.: instance.prop = value
godot_bool lps_instance_set_prop(godot_pluginscript_instance_data *data, const godot_string *name, const godot_variant *value) {
    // TODO
    return false;
}

// Called when getting a property from instance, e.g.: var value = instance.prop
godot_bool lps_instance_get_prop(godot_pluginscript_instance_data *data, const godot_string *name, godot_variant *ret) {
    // TODO
    return false;
}

// Called when calling a method on an instance, e.g.: instance.method(args)
godot_variant lps_instance_call_method(godot_pluginscript_instance_data *data, const godot_string_name *method, const godot_variant **args, int argcount, godot_variant_call_error *error) {
    // TODO
    return hgdn_new_nil_variant();
}

// Called when a notification is sent to instance
void lps_instance_notification(godot_pluginscript_instance_data *data, int notification) {
    // TODO
}

Right below, let's define our language description:

// Declared as a global variable, because Godot needs the
// memory to be valid until our plugin is unloaded 
godot_pluginscript_language_desc lps_language_desc = {
    .name = "Lua",
    .type = "Lua",
    .extension = "lua",
    .recognized_extensions = (const char *[]){ "lua", NULL },
    .reserved_words = (const char *[]){
        // Lua keywords
        "and", "break", "do", "else", "elseif", "end",
        "false", "for", "function", "goto", "if", "in",
        "local", "nil", "not", "or", "repeat", "return",
        "then", "true", "until", "while",
        // Other important identifiers
        "self", "_G", "_ENV", "_VERSION",
        NULL
    },
    .comment_delimiters = (const char *[]){ "--", "--[[ ]]", NULL },
    .string_delimiters = (const char *[]){ "' '", "\" \"", "[[ ]]", "[=[ ]=]", NULL },
    // Lua scripts don't care about the class name
    .has_named_classes = false,
    // Builtin scripts didn't work in my tests, disabling...
    .supports_builtin_mode = false,

    // Callbacks
    .init = &lps_language_init,
    .finish = &lps_language_finish,
    .add_global_constant = &lps_language_add_global_constant,
    .script_desc = {
        .init = &lps_script_init,
        .finish = &lps_script_finish,
        .instance_desc = {
            .init = &lps_instance_init,
            .finish = &lps_instance_finish,
            .set_prop = &lps_instance_set_prop,
            .get_prop = &lps_instance_get_prop,
            .call_method = &lps_instance_call_method,
            .notification = &lps_instance_notification,
        },
    },
};

Now the final touch, change godot_gdnative_init to register the language:

GDN_EXPORT void godot_gdnative_init(godot_gdnative_init_options *options) {
    hgdn_gdnative_init(options);
    hgdn_pluginscript_api->godot_pluginscript_register_language(&lps_language_desc);
}

Recompile the project by running the xmake command and reopen Godot. Look at that, our xmake.lua file is being recognized as a Lua script and syntax highlighting works! Awesome! =D

Wrapping up

With the base of our PluginScript ready, we can now focus on implementing the functionality. The version of the project built in this article is available here.

In the next post we'll link the project with LuaJIT and start implementing the plugin callbacks. I'll be using some Lua/C API and LuaJIT's FFI for this, so it will be a very interesting adventure!

Until next time! ;]