Skip to content

Latest commit

 

History

History
 
 

1.hello-c

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 

Hello GDExtension from C: an in-depth introduction

In this first interaction with the GDExtension API, we'll create a "Hello World" extension that prints a static message when it gets loaded.

To get to know GDExtension in depth, we'll be using the raw API in plain C. In a later sample we'll use C++ with the godot-cpp bindings, which should provide much better usability.

Generating GDExtension API files

First thing we need to do is either download or generate the gdextension_interface.h and extension_api.json files. The gdextension_interface.h contains the GDExtension API definitions, while extension_api.json is a huge JSON file containing metadata about all Godot classes, utility functions and enumerations.

At the time of writing, the godot-headers repository has an outdated version of these files, so we'll generate them using the Godot editor.

To keep things organized, let's first create a new folder for them called include:

mkdir include

Assuming Godot 4 is available in your system's PATH as godot, run the following inside our newly created include directory:

godot --dump-extension-api --dump-gdextension-interface --headless

Using --dump-extension-api asks Godot to generate the extension_api.json file. Using --dump-gdextension-interface asks Godot to generate the gdextension_interface.h file. Using --headless makes Godot use the dummy display and audio drivers, speeding the whole process up.

In the end, our directory should be something like this:

.
└─ include/
   ├─ extension_api.json
   └─ gdextension_interface.h

Creating an empty library

Ok, now that we have the API available, we can start creating our extension.

Let's start by creating a C file named hello-gdextension.c. We'll include the gdextension_interface.h file and define a function that will be the entry point to our library, the one that Godot will call when loading our extension. This entry point must follow the GDExtensionInitializationFunction prototype, defined in gdextension_interface.h.

// hello-gdextension.c

// 1. Include the GDExtension API file
#include "include/gdextension_interface.h"

// 2. Define the function with prototype matching GDExtensionInitializationFunction
GDExtensionBool hello_extension_entry(
    const GDExtensionInterface *p_interface,
    GDExtensionClassLibraryPtr p_library,
    GDExtensionInitialization *r_initialization
) {
    // returning 0 means error, returning non-zero means success
    return 1;
}

Now that we have our C file, we need to compile it to a shared library, a.k.a. DLL. This project is so simple, with a single source file, that we could run the C compiler directly. But to simplify our lives and prepare ourselves to handle more complex projects in the future, let's use a build system.

We can use any one of them. There are lots of good ones, like Make, CMake, Meson, SCons, xmake, Bazel... For this tutorial, we'll be using SCons, which is the same one that Godot and godot-cpp use.

After installing SCons, create a file named SConstruct with the following contents:

SharedLibrary('hello-gdextension.c')

Now running the scons command should build our shared library correctly, independent of the platform we are running and which C compiler is installed:

scons
# scons: Reading SConscript files ...
# scons: done reading SConscript files.
# scons: Building targets ...
# gcc -o hello-gdextension.os -c -fPIC hello-gdextension.c
# gcc -o libhello-gdextension.so -shared hello-gdextension.os
# scons: done building targets.

Ignoring the files generated by scons, our current directory should look like this:

.
├─ hello-gdextension.c
├─ include/
│  ├─ extension_api.json
│  └─ gdextension_interface.h
└─ SConstruct

Configuring the extension

As well as the shared libraries containing the built code, extensions are composed by an INI-formatted file with extension .gdextension that contains a symbol to the entry point function and the library paths, one for each platform/CPU architecture.

Let's define our own hello.gdextension file:

[configuration]
entry_symbol = "hello_extension_entry"

[libraries]
linux.x86_64 = "libhello-gdextensions.so"
windows.x86_64 = "hello-gdextensions.dll"
macos = "libhello-gdextensions.dylib"

If we eventually add support for other platforms, like Android or iOS, or CPU architectures, like x86 or arm64, we need to add more entries to this file.

Also notice that, in Linux and macOS, scons generates libraries with lib prefixed in their name.

All right, now when we run Godot 4 with our built extension, it should be correctly loaded. I've added a sample project file with an empty scene in the root of this repository, but you can also create your own to test this out.

godot --upwards --headless --quit
# ERROR: Condition "initialization.initialize == nullptr" is true.
#    at: initialize_library (core/extension/gdextension.cpp:447)
# Godot Engine v4.0.beta10.official.d0398f62f - https://godotengine.org
# ...
# ================================================================
# handle_crash: Program crashed with signal 11
# Engine version: Godot Engine v4.0.beta10.official (d0398f62f08ce0cfba80990b21c6af4181f93fe9)
# Dumping the backtrace. Please include this when reporting the bug on: https://github.com/godotengine/godot/issues
# [1] /usr/lib/libc.so.6 ...
# -- END OF BACKTRACE --
# ================================================================

I didn't promise the editor wouldn't crash, only that our library would be loaded. Our extension still needs to define the r_initialization->initialize and deinitialize functions.

Current and final directory:

.
├─ hello.gdextension
├─ hello-gdextension.c
├─ include/
│  ├─ extension_api.json
│  └─ gdextension_interface.h
└─ SConstruct

GDExtension life cycle

While Godot is initializing, it also initializes all GDExtensions by calling the initialize function in the GDExtensionInitialization pointer that was passed to our entry point. While quitting, Godot deinitializes all extensions by calling the deinitialize function.

As extension developers, we are responsible for implementing both functions and setting them up in the initialization structure. Open up hello-gdextension.c and create both functions. To help us understanding that our library is running, let's also add some debug logs.

// `printf` is part of the `stdio.h` header
#include <stdio.h>
#include "include/gdextension_interface.h"

void initialize(void *userdata, GDExtensionInitializationLevel p_level) {
    printf("initialize at level %d\n", p_level);
}

void deinitialize(void *userdata, GDExtensionInitializationLevel p_level) {
    printf("deinitialize at level %d\n", p_level);
}

GDExtensionBool hello_extension_entry(
    const GDExtensionInterface *p_interface,
    GDExtensionClassLibraryPtr p_library,
    GDExtensionInitialization *r_initialization
) {
    // setup the `initialize` function
    r_initialization->initialize = &initialize;
    // setup the `deinitialize` function
    r_initialization->deinitialize = &deinitialize;
    return 1;
}

Recompile using scons, run Godot again and check out the output:

godot --upwards --headless --quit
# initialize at level 0
# Godot Engine v4.0.beta10.official.d0398f62f - https://godotengine.org
# initialize at level 1
# 
# initialize at level 2
# initialize at level 3
# deinitialize at level 3
# deinitialize at level 2
# deinitialize at level 1
# deinitialize at level 0

As we can see, Godot initializes and deinitializes our extension 4 times, one for each initialization level. The GDExtensionInitializationLevel enumeration lists the possible initialization levels Godot use:

  • GDEXTENSION_INITIALIZATION_CORE: happens right after the engine's core modules are initialized.
  • GDEXTENSION_INITIALIZATION_SERVERS: happens right after the engine's servers are initialized.
  • GDEXTENSION_INITIALIZATION_SCENE: happens right after the engine's runtime classes are registered. Only then classes, including core ones like Object, Reference and Node, are in the ClassDB and may be extended.
  • GDEXTENSION_INITIALIZATION_EDITOR: happens only in the editor, right after editor classes are registered, like EditorPlugin. Use this for editor-only code in extensions.

We are responsible for checking the right initialization level and only run our code when appropriate. We can use conditional statements for that:

#include <stdio.h>
#include "include/gdextension_interface.h"

void initialize(void *userdata, GDExtensionInitializationLevel p_level) {
    // return early without printing if initialization level is not "Scene"
    if (p_level != GDEXTENSION_INITIALIZATION_SCENE) {
        return;
    }

    printf("initialize at level %d\n", p_level);
}

void deinitialize(void *userdata, GDExtensionInitializationLevel p_level) {
    // return early without printing if initialization level is not "Scene"
    if (p_level != GDEXTENSION_INITIALIZATION_SCENE) {
        return;
    }

    printf("deinitialize at level %d\n", p_level);
}

GDExtensionBool hello_extension_entry(
    const GDExtensionInterface *p_interface,
    GDExtensionClassLibraryPtr p_library,
    GDExtensionInitialization *r_initialization
) {
    r_initialization->initialize = &initialize;
    r_initialization->deinitialize = &deinitialize;
    return 1;
}

Recompile and re-run Godot to see that only initialization level 2 ("Scene") is being processed:

godot --upwards --headless --quit
# Godot Engine v4.0.beta10.official.d0398f62f - https://godotengine.org
# 
# initialize at level 2
# deinitialize at level 2

All right, our library is now being initialized without crashes!

Using Godot utility functions

If you run the project from the Godot Editor, you'll see that printf output is only shown if you ran Godot from a terminal. It does not appear in the editor's Output tab. Let's fix that by calling Godot's built-in print utility function, the same one we use in GDScript code.

If you take a good look at the gdextension_interface.h header file, you'll see print_warning, print_error and print_script_error functions, but no plain print function. In fact, the GDExtension API is really succinct, requiring us to fetch pretty much every built-in functionality at runtime. This makes it really slim and dynamic, so that people building their own forks of Godot can use their own additional classes/functions in extensions in the same way as built-in ones. This also makes writing extensions by hand quite cumbersome. GDExtensions is designed for code generation, that's why the extension_api.json file exists: so bindings can be automatically generated for all classes and functions, including the ones added by custom forks.

Calling print is simple enough without generating any code, so we'll use the raw GDExtensions API for now. This will help us understand how GDExtension works behind the scenes.

To call the implementation of print, we need to fetch it first by using variant_get_ptr_utility_function. Its arguments are a StringName with the function name, in this case "print", and an integer with the utility function's hash. The hash can be found in the extension_api.json file and for print the value is 2648703342.

Creating a StringName

First of all, let's get to the job of creating the StringName "print". StringName is a version of the String type that is optimized to be used as unique identifiers. This technique of having unique instances of Strings is called "String Interning" and is used by several systems, including programming languages like Java and Lua. More information on Godot's StringName can be found in its documentation page.

Looking at the gdextension_interface.h header file, there are only functions for creating String types from C strings. One of the StringName's constructor accepts a String as input, so we'll need to first create a String, then construct a StringName from it.

Notice that if we use godot-cpp or any other bindings, this will all have been taken care of and we would be able to just construct a StringName directly from C/C++ data. The next articles will use them, so we won't need such boilerplate code then.

As well as utility functions, built-in type constructors, destructors and methods must be fetched at runtime using the GDExtensionInterface. So the first thing we'll do is fetch the constructors and destructors for String and StringName. To make the implementation simpler, let's also store a global reference to the GDExtensionInterface received in hello_extension_entry.

Let's open up hello-gdextension.c and fetch them constructors and destructors at our initialize function:

#include <stdio.h>
#include "include/gdextension_interface.h"

// GDExtensions interface pointer
const GDExtensionInterface *interface;
// Godot API function pointers
static GDExtensionPtrConstructor construct_StringName_from_String;
static GDExtensionPtrDestructor destroy_String;
static GDExtensionPtrDestructor destroy_StringName;

void initialize(void *userdata, GDExtensionInitializationLevel p_level) {
    if (p_level != GDEXTENSION_INITIALIZATION_SCENE) {
        return;
    }

    // StringName constructor at index 2 is the one that receives String
    // You can find this information in `extension_api.json` file
    construct_StringName_from_String = interface->variant_get_ptr_constructor(GDEXTENSION_VARIANT_TYPE_STRING_NAME, 2);
    destroy_String = interface->variant_get_ptr_destructor(GDEXTENSION_VARIANT_TYPE_STRING);
    destroy_StringName = interface->variant_get_ptr_destructor(GDEXTENSION_VARIANT_TYPE_STRING_NAME);

    printf("initialize at level %d\n", p_level);
}

void deinitialize(void *userdata, GDExtensionInitializationLevel p_level) {
    if (p_level != GDEXTENSION_INITIALIZATION_SCENE) {
        return;
    }

    printf("deinitialize at level %d\n", p_level);
}

GDExtensionBool hello_extension_entry(
    const GDExtensionInterface *p_interface,
    GDExtensionClassLibraryPtr p_library,
    GDExtensionInitialization *r_initialization
) {
    r_initialization->initialize = &initialize;
    r_initialization->deinitialize = &deinitialize;
    // save the GDExtensionInterface globally
    interface = p_interface;
    return 1;
}

Now we can implement a function that creates a StringName from a C string. To make our code easier to follow, we'll create struct definitions for String and StringName, both taking the size of a single pointer. You can find the struct sizes in extension_api.json.

// ...

typedef struct {
    uint8_t godot_data_dont_touch_this[sizeof(void *)];
} String;

typedef struct {
    uint8_t godot_data_dont_touch_this[sizeof(void *)];
} StringName;

StringName construct_StringName_from_cstring(const char *text) {
    // 1. Construct a String from the C string
    String string;
    interface->string_new_with_latin1_chars(&string, text);

    // 2. Construct a StringName from the String
    StringName string_name;
    GDExtensionConstTypePtr constructor_arguments[1] = { &string };
    construct_StringName_from_String(&string_name, constructor_arguments);

    // 3. Destroy the String, since it's not needed anymore
    destroy_String(&string);

    return string_name;
}

// ...

Finally, let's create our "print" StringName in initialize and destroy it in deinitialize. To make things simple, we'll store it globally as well. The final code is as follows:

#include <stdio.h>
#include "include/gdextension_interface.h"

typedef struct {
    uint8_t godot_data_dont_touch_this[sizeof(void *)];
} String;

typedef struct {
    uint8_t godot_data_dont_touch_this[sizeof(void *)];
} StringName;

// GDExtensions interface pointer
const GDExtensionInterface *interface;
// Godot API function pointers
static GDExtensionPtrConstructor construct_StringName_from_String;
static GDExtensionPtrDestructor destroy_String;
static GDExtensionPtrDestructor destroy_StringName;

// here is our global "print" StringName
static StringName print_StringName;

StringName construct_StringName_from_cstring(const char *text) {
    // 1. Construct a String from the C string
    String string;
    interface->string_new_with_latin1_chars(&string, text);

    // 2. Construct a StringName from the String
    StringName string_name;
    GDExtensionConstTypePtr constructor_arguments[1] = { &string };
    construct_StringName_from_String(&string_name, constructor_arguments);

    // 3. Destroy the String, since it's not needed anymore
    destroy_String(&string);

    return string_name;
}

void initialize(void *userdata, GDExtensionInitializationLevel p_level) {
    if (p_level != GDEXTENSION_INITIALIZATION_SCENE) {
        return;
    }

    // StringName constructor at index 2 is the one that receives String
    // You can find this information in `extension_api.json` file
    construct_StringName_from_String = interface->variant_get_ptr_constructor(GDEXTENSION_VARIANT_TYPE_STRING_NAME, 2);
    destroy_String = interface->variant_get_ptr_destructor(GDEXTENSION_VARIANT_TYPE_STRING);
    destroy_StringName = interface->variant_get_ptr_destructor(GDEXTENSION_VARIANT_TYPE_STRING_NAME);

    // Initialize "print" StringName
    print_StringName = construct_StringName_from_cstring("print");

    printf("initialize at level %d\n", p_level);
}

void deinitialize(void *userdata, GDExtensionInitializationLevel p_level) {
    if (p_level != GDEXTENSION_INITIALIZATION_SCENE) {
        return;
    }

    // Destroy "print" StringName
    destroy_StringName(&print_StringName);

    printf("deinitialize at level %d\n", p_level);
}

GDExtensionBool hello_extension_entry(
    const GDExtensionInterface *p_interface,
    GDExtensionClassLibraryPtr p_library,
    GDExtensionInitialization *r_initialization
) {
    r_initialization->initialize = &initialize;
    r_initialization->deinitialize = &deinitialize;
    // save the GDExtensionInterface globally
    interface = p_interface;
    return 1;
}

Calling print

Now that we have the "print" StringName, all that's left to do is fetch the print function implementation and call it. We'll declare the global print function pointer:

static GDExtensionPtrUtilityFunction print_function;

And fetch it using the "print" StringName and the integer hash we already found in extension_api.json:

print_function = interface->variant_get_ptr_utility_function(&print_StringName, 2648703342);

To call print_function, we'll first need to convert every passed parameter to a Variant. Variants are structures that hold any of Godot's built-in types, including numbers, Arrays, Strings, StringNames and Objects. It's the basic block for accessing Godot data! First declare the constructor function globally:

static GDExtensionVariantFromTypeConstructorFunc construct_Variant_from_String;

Then fetch it in initialize:

construct_Variant_from_String = interface->get_variant_from_type_constructor(GDEXTENSION_VARIANT_TYPE_STRING);

Let's also create a struct definition for Variants. They occupy 24 bytes size on Godot's default build configuration.

typedef struct {
    uint8_t godot_data_dont_touch_this[24];
} Variant;

Now let's create our own wrapper for the utility "print" function that accepts a C string.

void print(const char *text) {
    // 1. Construct a String from the C string
    String string;
    interface->string_new_with_utf8_chars(&string, text);

    // 2. Construct a Variant from the String
    Variant arg1;
    construct_Variant_from_String(&arg1, &string);

    // 3. Call the utility function
    // Since it's a void function, use "NULL" for the return pointer
    GDExtensionConstVariantPtr args[1] = { &arg1 };
    print_function(NULL, args, 1);

    // 4. Clean up resources that are no longer needed
    interface->variant_destroy(&arg1);
    destroy_String(&string);
}

Finally, we can call our own print function and it will call Godot's built-in "print" utility function, so our messages will now appear in the editor's Output tab. The final code is as follows:

#include "include/gdextension_interface.h"

typedef struct {
    uint8_t godot_data_dont_touch_this[sizeof(void *)];
} String;

typedef struct {
    uint8_t godot_data_dont_touch_this[sizeof(void *)];
} StringName;

typedef struct {
    uint8_t godot_data_dont_touch_this[24];
} Variant;

// GDExtensions interface pointer
const GDExtensionInterface *interface;
// Godot API function pointers
static GDExtensionPtrConstructor construct_StringName_from_String;
static GDExtensionVariantFromTypeConstructorFunc construct_Variant_from_String;
static GDExtensionPtrDestructor destroy_String;
static GDExtensionPtrDestructor destroy_StringName;

// here is our global "print" StringName
static StringName print_StringName;
// and here is our global "print" function
static GDExtensionPtrUtilityFunction print_function;

StringName construct_StringName_from_cstring(const char *text) {
    // 1. Construct a String from the C string
    String string;
    interface->string_new_with_latin1_chars(&string, text);

    // 2. Construct a StringName from the String
    StringName string_name;
    GDExtensionConstTypePtr constructor_arguments[1] = { &string };
    construct_StringName_from_String(&string_name, constructor_arguments);

    // 3. Destroy the String, since it's not needed anymore
    destroy_String(&string);

    return string_name;
}

void print(const char *text) {
    // 1. Construct a String from the C string
    String string;
    interface->string_new_with_utf8_chars(&string, text);

    // 2. Construct a Variant from the String
    Variant arg1;
    construct_Variant_from_String(&arg1, &string);

    // 3. Call the utility function
    // Since it's a void function, use "NULL" for the return pointer
    GDExtensionConstVariantPtr args[1] = { &arg1 };
    print_function(NULL, args, 1);

    // 4. Clean up resources that are no longer needed
    interface->variant_destroy(&arg1);
    destroy_String(&string);
}

void initialize(void *userdata, GDExtensionInitializationLevel p_level) {
    if (p_level != GDEXTENSION_INITIALIZATION_SCENE) {
        return;
    }

    // StringName constructor at index 2 is the one that receives String
    // You can find this information in `extension_api.json` file
    construct_StringName_from_String = interface->variant_get_ptr_constructor(GDEXTENSION_VARIANT_TYPE_STRING_NAME, 2);
    construct_Variant_from_String = interface->get_variant_from_type_constructor(GDEXTENSION_VARIANT_TYPE_STRING);
    destroy_String = interface->variant_get_ptr_destructor(GDEXTENSION_VARIANT_TYPE_STRING);
    destroy_StringName = interface->variant_get_ptr_destructor(GDEXTENSION_VARIANT_TYPE_STRING_NAME);

    // Initialize "print" StringName
    print_StringName = construct_StringName_from_cstring("print");
    // then fetch the "print" function pointer
    print_function = interface->variant_get_ptr_utility_function(&print_StringName, 2648703342);

    print("Hello GDExtension from C!");
}

void deinitialize(void *userdata, GDExtensionInitializationLevel p_level) {
    if (p_level != GDEXTENSION_INITIALIZATION_SCENE) {
        return;
    }

    print("Goodbye GDExtension from C!");

    // Destroy "print" StringName
    destroy_StringName(&print_StringName);
}

GDExtensionBool hello_extension_entry(
    const GDExtensionInterface *p_interface,
    GDExtensionClassLibraryPtr p_library,
    GDExtensionInitialization *r_initialization
) {
    r_initialization->initialize = &initialize;
    r_initialization->deinitialize = &deinitialize;
    // save the GDExtensionInterface globally
    interface = p_interface;
    return 1;
}

And that's it! Recompile, open the project and run any scene from the Godot editor and you'll see the message "Hello GDExtension from C!" on the Output tab.

Conclusion

The GDExtension API is very powerful and flexible, letting custom native code add new classes to Godot without having to recompile the engine. On the other hand, it is really succint and requires a lot of boilerplate code for actually accessing built-in classes and utility functions, this part being well suited for automatic code generation.

In the next post we'll take a look at creating an extension in C++ using the godot-cpp bindings, which handles all the boilerplate and lets us go directly to implementing functionality. See you then!