Пример GDExtension на C

Введение

Это простой пример работы с GDExtension напрямую на языке C. Обратите внимание, что API не предназначен для прямого использования, поэтому даже для небольшого примера потребуется много шагов. Однако он служит справочником для создания привязок для другого языка. По-прежнему возможно использовать API напрямую, что может быть удобно при привязке сторонней библиотеки.

В этом примере мы создадим пользовательский узел, который перемещает спрайт по экрану на основе параметров пользователя. Хотя он очень прост, он служит для демонстрации некоторых возможностей GDExtension, таких как регистрация пользовательских классов с методами, свойствами и сигналами. Это даёт представление об API GDExtension.

Настройка проекта

Вам необходимо выполнить несколько условий:

  • исполняемый файл Godot 4.2 (или новее),

  • компилятор C,

  • SCons в качестве инструмента сборки.

Поскольку здесь используется API напрямую, нет необходимости использовать репозиторий godot-cpp.

Структура файла

Для организации файлов мы разделим их на две основные папки:

gdextension_c_example/
|
+--project/                  # game example/demo to test the extension
|
+--src/                   # source code of the extension we are building

Нам также понадобится копия заголовочного файла gdextension_interface.h из исходного кода Godot, который можно получить напрямую из исполняемого файла Godot, выполнив команду:

godot --dump-gdextension-interface

Это создаст заголовочный файл в текущей папке, поэтому вы можете просто скопировать его в папку src в проекте примера.

Наконец, есть ещё один источник информации, к которому нам нужно обращаться - JSON-файл со справочником API Godot. Этот файл не будет использоваться кодом напрямую, мы будем использовать его только для ручного извлечения некоторой информации.

Чтобы получить этот JSON-файл, просто вызовите исполняемый файл Godot:

godot --dump-extension-api

Результирующий файл extension_api.json будет создан в текущей папке. Вы можете скопировать этот файл в папку примера для удобства.

Примечание

Это расширение предназначено для Godot 4.2, но должно работать и в более поздних версиях. Если вы хотите поддерживать другую минимальную версию, убедитесь, что получили заголовочный файл и JSON из версии Godot, которую вы используете.

Система сборки

Использование системы сборки значительно упрощает работу с кодом на C. Для удобства мы будем использовать SCons, так как он используется самим Godot.

Следующий файл SConstruct - это простой файл, который соберёт ваше расширение для текущей платформы, будь то Linux, macOS или Windows. Это будет неоптимизированная сборка для целей отладки. Он также предполагает 64-битную сборку, что важно для некоторых частей примера кода. Создание других типов сборок и кросс-компиляция выходят за рамки данного руководства. Сохраните этот файл в корневую папку.

#!/bin/env python
from SCons.Script import Environment
from os import path
import sys

env = Environment()

# Set the target path and name.
target_path = "project/bin/"
target_name = "libgdexample"

# Set the compiler and flags.
env.Append(CPPPATH=["src"])  # Add the src folder to the include path.
env.Append(CFLAGS=["-O0", "-g"])  # Make it a debug build.

# Use Clang on macOS.
if sys.platform == "darwin":
    env["CC"] = "clang"

# Add all C files in "src" folder as sources.
sources = env.Glob("src/*.c")

# Create a shared library.
library = env.SharedLibrary(
    target=path.join(target_path, target_name),
    source=sources,
)

# Set the library as the default target.
env.Default(library)

Это включит все C-файлы в папке src, поэтому нам не нужно изменять этот файл при добавлении новых исходных файлов.

Инициализация расширения

Первый фрагмент кода будет отвечать за инициализацию расширения. Это то, что позволяет Godot узнать, что предоставляет наше GDExtension, например классы и плагины.

Создайте файл init.h в папке src со следующим содержимым:

#pragma once

#include "defs.h"

#include "gdextension_interface.h"

void initialize_gdexample_module(void *p_userdata, GDExtensionInitializationLevel p_level);
void deinitialize_gdexample_module(void *p_userdata, GDExtensionInitializationLevel p_level);
GDExtensionBool GDE_EXPORT gdexample_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization);

Объявленные здесь функции имеют сигнатуры, ожидаемые API GDExtension.

Обратите внимание на включение файла defs.h. Это один из наших вспомогательных файлов для упрощения написания кода расширения. Пока он будет содержать только определение GDE_EXPORT - макроса, который делает функцию публичной в общей библиотеке, чтобы Godot мог её правильно вызывать. Этот макрос помогает абстрагировать требования разных компиляторов.

Создайте файл defs.h в папке src со следующим содержимым:

#pragma once

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>

#if !defined(GDE_EXPORT)
#if defined(_WIN32)
#define GDE_EXPORT __declspec(dllexport)
#elif defined(__GNUC__)
#define GDE_EXPORT __attribute__((visibility("default")))
#else
#define GDE_EXPORT
#endif
#endif // ! GDE_EXPORT

Мы также включаем некоторые стандартные заголовки для упрощения работы. Теперь нам достаточно включить defs.h, и они придут как бонус.

Теперь реализуем объявленные функции. Создайте файл init.c в папке src и добавьте следующий код:

#include "init.h"

void initialize_gdexample_module(void *p_userdata, GDExtensionInitializationLevel p_level)
{
}

void deinitialize_gdexample_module(void *p_userdata, GDExtensionInitializationLevel p_level)
{
}

GDExtensionBool GDE_EXPORT gdexample_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization)
{
    r_initialization->initialize = initialize_gdexample_module;
    r_initialization->deinitialize = deinitialize_gdexample_module;
    r_initialization->userdata = NULL;
    r_initialization->minimum_initialization_level = GDEXTENSION_INITIALIZATION_SCENE;

    return true;
}

Это настраивает данные инициализации, которые ожидает Godot. Функции инициализации и деинициализации установлены, чтобы Godot вызывал их при необходимости. Также устанавливается уровень инициализации, который варьируется в зависимости от расширения. Поскольку мы планируем добавить пользовательский узел, уровня SCENE достаточно.

Позже мы заполним функцию initialize_gdexample_module() для регистрации нашего пользовательского класса.

Базовый класс

Чтобы создать реальный узел, сначала мы создадим структуру на C для хранения данных и функций, которые будут действовать как методы. Планируется сделать это пользовательским узлом, наследующим от Sprite2D.

Создайте файл gdexample.h в папке src со следующим содержимым:

#pragma once

#include "gdextension_interface.h"

#include "defs.h"

// Struct to hold the node data.
typedef struct
{
    // Metadata.
    GDExtensionObjectPtr object; // Stores the underlying Godot object.
} GDExample;

// Constructor for the node.
void gdexample_class_constructor(GDExample *self);

// Destructor for the node.
void gdexample_class_destructor(GDExample *self);

// Bindings.
void gdexample_class_bind_methods();

Примечательны здесь поле object, которое содержит указатель на объект Godot, и функция gdexample_class_bind_methods(), которая зарегистрирует метаданные нашего пользовательского класса (свойства, методы и сигналы). Последнее не совсем необходимо, так как мы можем сделать это при регистрации класса, но это делает разделение ответственности более чётким и позволяет нашему классу регистрировать свои собственные метаданные.

Поле object необходимо, потому что наш класс будет наследовать класс Godot. Поскольку мы не можем наследовать его напрямую (мы не взаимодействуем с исходным кодом, и в C даже нет классов), мы вместо этого говорим Godot создать объект известного ему типа и прикрепить наше расширение к нему. Нам понадобится ссылка на такие объекты, например, при вызове методов родительского класса.

Создадим исходный файл для этого заголовка. Создайте файл gdexample.c в папке src и добавьте в него следующий код:

#include "gdexample.h"

void gdexample_class_constructor(GDExample *self)
{
}

void gdexample_class_destructor(GDExample *self)
{
}

void gdexample_class_bind_methods()
{
}

Поскольку нам пока нечего делать с этими функциями, они останутся пустыми на некоторое время.

Следующий шаг - регистрация нашего класса. Однако для этого нам нужно создать StringName, а для этого нужно получить функцию из API GDExtension. Поскольку нам это понадобится несколько раз, и нам также понадобятся другие вещи, давайте создадим обёртку API для облегчения этой рутины.

Обёртка API

Начнём с создания файла api.h в папке src:

#pragma once

/*
This file works as a collection of helpers to call the GDExtension API
in a less verbose way, as well as a cache for methods from the discovery API,
just so we don't have to keep loading the same methods again.
*/

#include "gdextension_interface.h"

#include "defs.h"

extern GDExtensionClassLibraryPtr class_library;

// API methods.

extern struct Constructors
{
    GDExtensionInterfaceStringNameNewWithLatin1Chars string_name_new_with_latin1_chars;
} constructors;

extern struct Destructors
{
    GDExtensionPtrDestructor string_name_destructor;
} destructors;

extern struct API
{
    GDExtensionInterfaceClassdbRegisterExtensionClass2 classdb_register_extension_class2;
} api;

void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address);

Этот файл будет включать множество других вспомогательных функций по мере наполнения нашего расширения полезным содержимым. Пока в нём есть только указатель на функцию, создающую StringName из C-строки (в кодировке Latin-1), и другая для уничтожения StringName, которые нам понадобятся, чтобы избежать утечек памяти, а также функция регистрации класса, что является нашей первоначальной целью.

Мы также сохраняем здесь ссылку на class_library. Это то, что Godot предоставляет нам при инициализации расширения, и нам нужно будет использовать это при регистрации создаваемых объектов, чтобы Godot мог определить, какое расширение делает вызов.

Также есть функция для загрузки этих указателей функций из API GDExtension.

Давайте поработаем над исходным файлом для этого заголовка. Создайте файл api.c в папке src, добавив следующий код:

#include "api.h"

GDExtensionClassLibraryPtr class_library = NULL;

struct Constructors constructors;
struct Destructors destructors;
struct API api;

void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address)
{
    // Get helper functions first.
    GDExtensionInterfaceVariantGetPtrDestructor variant_get_ptr_destructor = (GDExtensionInterfaceVariantGetPtrDestructor)p_get_proc_address("variant_get_ptr_destructor");

    // API.
    api.classdb_register_extension_class2 = (GDExtensionInterfaceClassdbRegisterExtensionClass2)p_get_proc_address("classdb_register_extension_class2");

    // Constructors.
    constructors.string_name_new_with_latin1_chars = (GDExtensionInterfaceStringNameNewWithLatin1Chars)p_get_proc_address("string_name_new_with_latin1_chars");

    // Destructors.
    destructors.string_name_destructor = variant_get_ptr_destructor(GDEXTENSION_VARIANT_TYPE_STRING_NAME);
}

Первая важная вещь здесь - p_get_proc_address. Это функция из API GDExtension, которая передаётся во время инициализации. Вы можете использовать эту функцию для запроса конкретных функций API по их имени. Здесь мы кэшируем результаты, чтобы не хранить ссылку на p_get_proc_address повсюду и вместо этого использовать нашу обёртку.

В начале мы запрашиваем функцию variant_get_ptr_destructor(). Она не будет использоваться вне этой функции, поэтому мы не добавляем её в нашу обёртку, а только кэшируем локально. Приведение типа необходимо для подавления предупреждений компилятора.

Затем мы получаем функцию, создающую StringName из C-строки, именно то, что мы упоминали ранее как необходимую функцию. Мы сохраняем её в нашей структуре constructors.

Далее мы используем только что полученную функцию variant_get_ptr_destructor() для запроса деструктора StringName, используя значение перечисления из API gdextension_interface.h в качестве параметра. Мы могли бы получить деструкторы для других типов аналогичным образом, но ограничимся тем, что необходимо для примера.

Наконец, мы получаем функцию classdb_register_extension_class2(), которая понадобится нам для регистрации нашего пользовательского класса.

Примечание

Вам может быть интересно, почему в имени функции есть 2. Это означает, что это вторая версия этой функции. Старая версия сохраняется для обеспечения обратной совместимости со старыми расширениями, но поскольку у нас доступна вторая версия, лучше использовать новую, так как в этом примере мы не планируем поддерживать старые версии Godot.

Заголовочный файл gdextension_interface.h документирует, в какой версии Godot была введена каждая функция.

Мы также определяем здесь переменную class_library, которая будет установлена во время инициализации.

Говоря об инициализации, теперь нам нужно изменить файл init.c, чтобы заполнить то, что мы только что добавили:

GDExtensionBool GDE_EXPORT gdexample_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization)
{
    class_library = p_library;
    load_api(p_get_proc_address);

    ...

Здесь мы устанавливаем class_library по мере необходимости и вызываем нашу новую функцию load_api(). Не забудьте также включить новые заголовки в верхней части этого файла:

#include "init.h"

#include "api.h"
#include "gdexample.h"
...

Раз уж мы здесь, мы можем зарегистрировать наш новый пользовательский класс. Давайте заполним функцию initialize_gdexample_module():

void initialize_gdexample_module(void *p_userdata, GDExtensionInitializationLevel p_level)
{
    if (p_level != GDEXTENSION_INITIALIZATION_SCENE)
    {
        return;
    }

    // Register class.
    StringName class_name;
    constructors.string_name_new_with_latin1_chars(&class_name, "GDExample", false);
    StringName parent_class_name;
    constructors.string_name_new_with_latin1_chars(&parent_class_name, "Sprite2D", false);

    GDExtensionClassCreationInfo2 class_info = {
        .is_virtual = false,
        .is_abstract = false,
        .is_exposed = true,
        .set_func = NULL,
        .get_func = NULL,
        .get_property_list_func = NULL,
        .free_property_list_func = NULL,
        .property_can_revert_func = NULL,
        .property_get_revert_func = NULL,
        .validate_property_func = NULL,
        .notification_func = NULL,
        .to_string_func = NULL,
        .reference_func = NULL,
        .unreference_func = NULL,
        .create_instance_func = gdexample_class_create_instance,
        .free_instance_func = gdexample_class_free_instance,
        .recreate_instance_func = NULL,
        .get_virtual_func = NULL,
        .get_virtual_call_data_func = NULL,
        .call_virtual_with_data_func = NULL,
        .get_rid_func = NULL,
        .class_userdata = NULL,
    };

    api.classdb_register_extension_class2(class_library, &class_name, &parent_class_name, &class_info);

    // Bind methods.
    gdexample_class_bind_methods();

    // Destruct things.
    destructors.string_name_destructor(&class_name);
    destructors.string_name_destructor(&parent_class_name);
}

Структура с информацией о классе — это самое важное здесь. Ни одно из её полей не является обязательным, за исключением create_instance_func и free_instance_func. Мы ещё не создали эти функции, поэтому скоро займёмся ими. Обратите внимание, что мы пропускаем инициализацию, если она не находится на уровне SCENE. Эта функция может вызываться несколько раз, по одному разу на каждый уровень, но нам нужно зарегистрировать класс только один раз.

Ещё один неопределённый параметр — StringName. Это будет непрозрачная структура, предназначенная для хранения данных Godot StringName в нашем расширении. Мы определим её в файле defs.h с соответствующим именем:

...
// The sizes can be obtained from the extension_api.json file.
#ifdef BUILD_32
#define STRING_NAME_SIZE 4
#else
#define STRING_NAME_SIZE 8
#endif

// Types.

typedef struct
{
    uint8_t data[STRING_NAME_SIZE];
} StringName;

Как уже упоминалось в комментарии, размеры можно найти в файле extension_api.json, который мы сгенерировали ранее, в свойстве builtin_class_sizes. Значение BUILD_32 не определено, поскольку мы предполагаем, что работаем с 64-битной сборкой Godot, но при необходимости вы можете добавить env.Append(CPPDEFINES=["BUILD_32"]) в файл SConstruct.

Комментарий // Types. предвещает, что мы будем добавлять в этот файл новые типы. Оставим это на потом.

Структура StringName здесь предназначена только для хранения данных Godot, поэтому нам не важно, что именно находится внутри. Хотя в данном случае это просто указатель на данные в куче. Мы будем использовать эту структуру, когда нам понадобится самостоятельно выделить данные для StringName, например, при регистрации нашего класса.

Возвращаясь к регистрации, нам нужно поработать над функциями create и free. Включим их в gdexample.h, поскольку они относятся к пользовательскому классу:

...
// Bindings.
void gdexample_class_bind_methods();
GDExtensionObjectPtr gdexample_class_create_instance(void *p_class_userdata);
void gdexample_class_free_instance(void *p_class_userdata, GDExtensionClassInstancePtr p_instance);
...

Прежде чем мы сможем реализовать эти функции, нам потребуется ещё кое-что в нашем API. Нам нужен способ выделять и освобождать память. Хотя это можно было бы сделать с помощью старого доброго malloc(), вместо этого можно использовать функции управления памятью Godot. Нам также понадобится способ создать объект Godot и назначить его нашему экземпляру.

Давайте изменим api.h, чтобы включить эти новые функции:

...
extern struct API
{
    GDExtensionInterfaceClassdbRegisterExtensionClass2 classdb_register_extension_class2;
    GDExtensionInterfaceClassdbConstructObject classdb_construct_object;
    GDExtensionInterfaceObjectSetInstance object_set_instance;
    GDExtensionInterfaceObjectSetInstanceBinding object_set_instance_binding;
    GDExtensionInterfaceMemAlloc mem_alloc;
    GDExtensionInterfaceMemFree mem_free;
} api;

Затем мы изменяем функцию load_api() в api.c, чтобы она включала эти новые функции:

...
void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address)
{
    ...
    // API.
    api.classdb_register_extension_class2 = p_get_proc_address("classdb_register_extension_class2");
    api.classdb_construct_object = (GDExtensionInterfaceClassdbConstructObject)p_get_proc_address("classdb_construct_object");
    api.object_set_instance = (GDExtensionInterfaceObjectSetInstance)p_get_proc_address("object_set_instance");
    api.object_set_instance_binding = (GDExtensionInterfaceObjectSetInstanceBinding)p_get_proc_address("object_set_instance_binding");
    api.mem_alloc = (GDExtensionInterfaceMemAlloc)p_get_proc_address("mem_alloc");
    api.mem_free = (GDExtensionInterfaceMemFree)p_get_proc_address("mem_free");
}

Теперь мы можем вернуться к gdexample.c и определить новые функции, не забыв включить заголовок api.h:

#include "gdexample.h"

#include "api.h"

...

const GDExtensionInstanceBindingCallbacks gdexample_class_binding_callbacks = {
    .create_callback = NULL,
    .free_callback = NULL,
    .reference_callback = NULL,
};

GDExtensionObjectPtr gdexample_class_create_instance(void *p_class_userdata)
{
    // Create native Godot object;
    StringName class_name;
    constructors.string_name_new_with_latin1_chars(&class_name, "Sprite2D", false);
    GDExtensionObjectPtr object = api.classdb_construct_object(&class_name);
    destructors.string_name_destructor(&class_name);

    // Create extension object.
    GDExample *self = (GDExample *)api.mem_alloc(sizeof(GDExample));
    gdexample_class_constructor(self);
    self->object = object;

    // Set the extension instance in the native Godot object.
    constructors.string_name_new_with_latin1_chars(&class_name, "GDExample", false);
    api.object_set_instance(object, &class_name, self);
    api.object_set_instance_binding(object, class_library, self, &gdexample_class_binding_callbacks);
    destructors.string_name_destructor(&class_name);

    return object;
}

void gdexample_class_free_instance(void *p_class_userdata, GDExtensionClassInstancePtr p_instance)
{
    if (p_instance == NULL)
    {
        return;
    }
    GDExample *self = (GDExample *)p_instance;
    gdexample_class_destructor(self);
    api.mem_free(self);
}

При создании экземпляра объекта, сначала мы создаём новый объект Sprite2D, поскольку он является родительским для нашего класса. Затем мы выделяем память для нашей пользовательской структуры и вызываем её конструктор. Мы также сохраняем указатель на объект Godot в структуре, как уже упоминалось ранее.

Затем мы устанавливаем нашу пользовательскую структуру в качестве данных экземпляра. Это позволит Godot определить, что объект является экземпляром нашего пользовательского класса, и, например, корректно вызывать наши пользовательские методы, а также передавать эти данные обратно.

Обратите внимание, что мы возвращаем созданный нами объект Godot, а не нашу пользовательскую структуру.

Для функции gdextension_free_instance() мы только вызываем деструктор и освобождаем память, выделенную для пользовательских данных. Уничтожать объект Godot не обязательно, так как об этом позаботится сам движок.

Демо-проект

Now that we can create and free our custom object, we should be able to try it out in an actual project. For this, you need to open Godot and create a new project in the project folder. The project manager may warn you the folder isn't empty if you have compiled the extension before, you can safely ignore this warning this time.

Если вы ещё не скомпилировали расширение, самое время сделать это сейчас. Для этого откройте терминал или командную строку, перейдите в корневую папку расширения и выполните команду scons. Компиляция должна пройти быстро, поскольку расширение очень простое.

Then, create a file called gdexample.gdextension inside the project folder. This is a Godot resource that describes the extension, allowing the engine to properly load it. Put the following content in this file:

[configuration]

entry_symbol = "gdexample_library_init"
compatibility_minimum = "4.2"

[libraries]
macos.debug = "res://bin/libgdexample.dylib"
linux.debug = "res://bin/libgdexample.so"
windows.debug = "res://bin/libgdexample.dll"

Как видите, gdexample_library_init() — это то же имя, что и у функции, которую мы определили в файле init.c. Важно, чтобы имена совпадали, поскольку Godot называет точку входа расширения именно так.

Мы также устанавливаем минимальный уровень совместимости 4.2, поскольку ориентируемся на эту версию. Он должен работать и в более поздних версиях. Если вы используете более позднюю версию Godot и рассчитываете на новые функции, вам необходимо увеличить это значение до номера версии, в которой есть всё необходимое. Подробнее см. в документе Совместимость версий.

В разделе [libraries] мы настраиваем пути к общей библиотеке на разных платформах. Здесь представлены только отладочные версии, поскольку именно с ними мы работаем в этом примере. Используя feature tags, вы можете настроить их так, чтобы они также предоставляли версии для релиза, добавляли больше целевых операционных систем, а также 32- и 64-битные исполняемые файлы.

Вы также можете добавить зависимости библиотеки и пользовательские значки для своих классов в этот файл, но это выходит за рамки данного руководства.

После сохранения файла вернитесь в редактор. Godot должен автоматически загрузить расширение. Ничего не будет видно, поскольку наше расширение регистрирует только новый класс. Чтобы использовать этот класс, добавьте Node2D в качестве корневого узла сцены. Переместите его в центр области просмотра для лучшей видимости. Затем добавьте новый дочерний узел в корневой узел и в диалоговом окне Create New Node найдите "GDExample", имя нашего класса, так как оно должно быть там указано. Если это не так, это означает, что Godot не загрузил расширение должным образом. Попробуйте перезапустить редактор и повторите шаги, чтобы проверить, не упустили ли вы что-нибудь.

Наш пользовательский класс наследуется от Sprite2D, поэтому у него есть свойство Texture в инспекторе. Укажите в нём файл icon.svg, который Godot любезно создал для нас при создании проекта. Сохраните эту сцену как main.tscn и запустите её. Для удобства можно сделать её основной сценой.

../../../_images/gdextension_c_running.webp

Вуаля! У нас есть пользовательский узел, работающий в Godot. Однако он ничего не делает и ничем не отличается от обычного узла Sprite2D. Сейчас мы исправим это, добавив пользовательские методы и свойства.

Пользовательские методы

В расширениях часто используется создание методов для пользовательских классов и предоставление их API Godot. Мы создадим несколько геттеров и сеттеров, которые понадобятся для последующего связывания свойств.

Сначала добавим в нашу структуру новые поля для хранения значений amplitude и speed, которые мы будем использовать позже при создании поведения узла. Добавьте их в файл gdexample.h, изменив структуру GDExample:

...

typedef struct
{
    // Public properties.
    double amplitude;
    double speed;
    // Metadata.
    GDExtensionObjectPtr object; // Stores the underlying Godot object.
} GDExample;

...

В том же файле, добавьте объявление для getters и setters, сразу после destructor.

...

// Destructor for the node.
void gdexample_class_destructor(GDExample *self);

// Properties.
void gdexample_class_set_amplitude(GDExample *self, double amplitude);
double gdexample_class_get_amplitude(const GDExample *self);
void gdexample_class_set_speed(GDExample *self, double speed);
double gdexample_class_get_speed(const GDExample *self);

...

В файле gdexample.c мы инициализируем эти значения в конструкторе и добавим реализации для этих новых функций, которые довольно тривиальны:

void gdexample_class_constructor(GDExample *self)
{
    self->amplitude = 10.0;
    self->speed = 1.0;
}

void gdexample_class_set_amplitude(GDExample *self, double amplitude)
{
    self->amplitude = amplitude;
}

double gdexample_class_get_amplitude(const GDExample *self)
{
    return self->amplitude;
}

void gdexample_class_set_speed(GDExample *self, double speed)
{
    self->speed = speed;
}

double gdexample_class_get_speed(const GDExample *self)
{
    return self->speed;
}

Чтобы эти простые функции работали при вызове Godot, нам понадобятся некоторые оболочки, которые помогут нам правильно преобразовывать данные в движок и из него.

Сначала мы создадим обёртки для ptrcall. Godot использует их, когда типы значений известны как точные, что позволяет избежать использования Variant. Нам понадобятся две обёртки: одна для функций без аргументов, возвращающих double (для геттеров), и другая для функций, принимающих один аргумент double и ничего не возвращающих (для сеттеров).

Добавьте объявления в файл api.h:

void ptrcall_0_args_ret_float(void *method_userdata, GDExtensionClassInstancePtr p_instance, const GDExtensionConstTypePtr *p_args, GDExtensionTypePtr r_ret);
void ptrcall_1_float_arg_no_ret(void *method_userdata, GDExtensionClassInstancePtr p_instance, const GDExtensionConstTypePtr *p_args, GDExtensionTypePtr r_ret);

Эти две функции соответствуют типу GDExtensionClassMethodPtrCall, как определено в gdextension_interface.h. Мы используем здесь имя float, поскольку в Godot тип float имеет двойную точность, поэтому мы придерживаемся этого соглашения.

Затем мы реализуем эти функции в файле api.c:

void ptrcall_0_args_ret_float(void *method_userdata, GDExtensionClassInstancePtr p_instance, const GDExtensionConstTypePtr *p_args, GDExtensionTypePtr r_ret)
{
    // Call the function.
    double (*function)(void *) = method_userdata;
    *((double *)r_ret) = function(p_instance);
}

void ptrcall_1_float_arg_no_ret(void *method_userdata, GDExtensionClassInstancePtr p_instance, const GDExtensionConstTypePtr *p_args, GDExtensionTypePtr r_ret)
{
    // Call the function.
    void (*function)(void *, double) = method_userdata;
    function(p_instance, *((double *)p_args[0]));
}

Аргумент method_userdata — это пользовательское значение, которое мы передаем Godot. В данном случае мы установим его как указатель на функцию, которую хотим вызвать. Поэтому сначала мы преобразуем его в тип функции, а затем просто вызываем её, передавая аргументы при необходимости или устанавливая возвращаемое значение.

Аргумент p_instance содержит пользовательский экземпляр нашего класса, который мы указали с помощью object_set_instance() при создании объекта.

p_args — это массив аргументов. Обратите внимание, что он содержит указатели на значения. Именно поэтому мы разыменовываем его при передаче в наши функции. Количество аргументов будет объявлено при связывании функции (что мы сделаем вскоре), и она всегда будет включать аргументы по умолчанию, если таковые имеются.

Наконец, r_ret — это указатель на переменную, для которой необходимо установить возвращаемое значение. Как и аргументы, тип будет соответствовать объявленному. Для функции, которая не возвращает значение, следует избегать его установки.

Обратите внимание, что типы и количество аргументов указаны точно, поэтому, например, если бы нам понадобились другие типы, нам пришлось бы создать больше обёрток. Это можно автоматизировать с помощью генерации кода, но это выходит за рамки данного руководства.

Хотя функции ptrcall используются, когда типы точны, иногда Godot не может определить это (когда вызов поступает из языка с динамической типизацией, например, GDScript). В таких ситуациях используются обычные функции call, поэтому при связывании необходимо указать и их.

Давайте создадим две новые оболочки в файле api.h:

void call_0_args_ret_float(void *method_userdata, GDExtensionClassInstancePtr p_instance, const GDExtensionConstVariantPtr *p_args, GDExtensionInt p_argument_count, GDExtensionVariantPtr r_return, GDExtensionCallError *r_error);
void call_1_float_arg_no_ret(void *method_userdata, GDExtensionClassInstancePtr p_instance, const GDExtensionConstVariantPtr *p_args, GDExtensionInt p_argument_count, GDExtensionVariantPtr r_return, GDExtensionCallError *r_error);

Они следуют типу GDExtensionClassMethodCall, который немного отличается. Во-первых, вы получаете указатели на варианты вместо конкретных типов. Также есть количество аргументов и структура ошибки, которую можно установить, если что-то пойдёт не так.

Чтобы проверить тип и извлечь взаимодействие с Variant, нам понадобится ещё несколько функций из API GDExtension. Итак, давайте расширим наши структуры-обёртки:

extern struct Constructors {
    ...
    GDExtensionVariantFromTypeConstructorFunc variant_from_float_constructor;
    GDExtensionTypeFromVariantConstructorFunc float_from_variant_constructor;
} constructors;

extern struct API
{
    ...
    GDExtensionInterfaceGetVariantFromTypeConstructor get_variant_from_type_constructor;
    GDExtensionInterfaceGetVariantToTypeConstructor get_variant_to_type_constructor;
    GDExtensionInterfaceVariantGetType variant_get_type;
} api;

Названия говорят сами за себя. У нас есть пара конструкторов для создания и извлечения значения с плавающей точкой из типа Variant. Также у нас есть пара вспомогательных функций для получения этих конструкторов, а также функция для определения типа Variant.

Давайте получим их из API, как мы делали раньше, изменив функцию load_api() в файле api.c:

void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address)
{
    ...

    // API.
    ...
    api.get_variant_from_type_constructor = (GDExtensionInterfaceGetVariantFromTypeConstructor)p_get_proc_address("get_variant_from_type_constructor");
    api.get_variant_to_type_constructor = (GDExtensionInterfaceGetVariantToTypeConstructor)p_get_proc_address("get_variant_to_type_constructor");
    api.variant_get_type = (GDExtensionInterfaceVariantGetType)p_get_proc_address("variant_get_type");
    ...

    // Constructors.
    ...
    constructors.variant_from_float_constructor = api.get_variant_from_type_constructor(GDEXTENSION_VARIANT_TYPE_FLOAT);
    constructors.float_from_variant_constructor = api.get_variant_to_type_constructor(GDEXTENSION_VARIANT_TYPE_FLOAT);
    ...
}

Теперь, когда у нас есть этот набор, мы можем реализовать наши оболочки вызовов в том же файле:

void call_0_args_ret_float(void *method_userdata, GDExtensionClassInstancePtr p_instance, const GDExtensionConstVariantPtr *p_args, GDExtensionInt p_argument_count, GDExtensionVariantPtr r_return, GDExtensionCallError *r_error)
{
    // Check argument count.
    if (p_argument_count != 0)
    {
        r_error->error = GDEXTENSION_CALL_ERROR_TOO_MANY_ARGUMENTS;
        r_error->expected = 0;
        return;
    }

    // Call the function.
    double (*function)(void *) = method_userdata;
    double result = function(p_instance);
    // Set resulting Variant.
    constructors.variant_from_float_constructor(r_return, &result);
}

void call_1_float_arg_no_ret(void *method_userdata, GDExtensionClassInstancePtr p_instance, const GDExtensionConstVariantPtr *p_args, GDExtensionInt p_argument_count, GDExtensionVariantPtr r_return, GDExtensionCallError *r_error)
{
    // Check argument count.
    if (p_argument_count < 1)
    {
        r_error->error = GDEXTENSION_CALL_ERROR_TOO_FEW_ARGUMENTS;
        r_error->expected = 1;
        return;
    }
    else if (p_argument_count > 1)
    {
        r_error->error = GDEXTENSION_CALL_ERROR_TOO_MANY_ARGUMENTS;
        r_error->expected = 1;
        return;
    }

    // Check the argument type.
    GDExtensionVariantType type = api.variant_get_type(p_args[0]);
    if (type != GDEXTENSION_VARIANT_TYPE_FLOAT)
    {
        r_error->error = GDEXTENSION_CALL_ERROR_INVALID_ARGUMENT;
        r_error->expected = GDEXTENSION_VARIANT_TYPE_FLOAT;
        r_error->argument = 0;
        return;
    }

    // Extract the argument.
    double arg1;
    constructors.float_from_variant_constructor(&arg1, (GDExtensionVariantPtr)p_args[0]);

    // Call the function.
    void (*function)(void *, double) = method_userdata;
    function(p_instance, arg1);
}

Эти функции немного длиннее, но просты в использовании. Сначала они проверяют, соответствует ли количество аргументов ожидаемому, и если нет, устанавливают структуру ошибки и возвращают управление. Функция с одним параметром также проверяет корректность типа аргумента. Это важно, поскольку несоответствие типов при извлечении из Variant может привести к сбоям.

Затем он извлекает аргумент с помощью конструктора, который мы создали ранее. Конструктор без аргументов вместо этого устанавливает возвращаемое значение после вызова функции. Обратите внимание, что используется указатель на переменную double, поскольку именно этого и ожидают эти конструкторы.

Прежде чем мы сможем привязать наши методы, нам нужен способ создания экземпляров GDExtensionPropertyInfo. Хотя мы могли бы делать это внутри функций привязки, которые реализуем позже, проще использовать вспомогательный метод, поскольку он понадобится нам несколько раз, в том числе при привязке свойств.

Давайте создадим эти две функции в файле api.h:

// Create a PropertyInfo struct.
GDExtensionPropertyInfo make_property(
    GDExtensionVariantType type,
    const char *name);

GDExtensionPropertyInfo make_property_full(
    GDExtensionVariantType type,
    const char *name,
    uint32_t hint,
    const char *hint_string,
    const char *class_name,
    uint32_t usage_flags);

void destruct_property(GDExtensionPropertyInfo *info);

Первый вариант — это упрощённая версия второго, поскольку обычно нам не нужны все аргументы для свойства, и нас устраивают значения по умолчанию. Кроме того, у нас есть функция для уничтожения PropertyInfo, поскольку нам нужно создать строки и имена строк, которые нужно правильно удалить.

Кстати, нам также нужен способ создания и уничтожения строк, поэтому мы добавим новые структуры в этот же файл. Кроме того, мы получим новую API-функцию для привязки нашего метода.

extern struct Constructors
{
    ...
    GDExtensionInterfaceStringNewWithUtf8Chars string_new_with_utf8_chars;
} constructors;

extern struct Destructors
{
    ...
    GDExtensionPtrDestructor string_destructor;
} destructors;

extern struct API
{
    ...
    GDExtensionInterfaceClassdbRegisterExtensionClassMethod classdb_register_extension_class_method;
} api;

Прежде чем реализовать это, давайте сделаем быструю остановку в файле defs.h и включим размер типа String и несколько перечислений:

// The sizes can be obtained from the extension_api.json file.
#ifdef BUILD_32
#define STRING_SIZE 4
#define STRING_NAME_SIZE 4
#else
#define STRING_SIZE 8
#define STRING_NAME_SIZE 8
#endif

...

typedef struct
{
    uint8_t data[STRING_SIZE];
} String;

// Enums.

typedef enum
{
    PROPERTY_HINT_NONE = 0,
} PropertyHint;

typedef enum
{
    PROPERTY_USAGE_NONE = 0,
    PROPERTY_USAGE_STORAGE = 2,
    PROPERTY_USAGE_EDITOR = 4,
    PROPERTY_USAGE_DEFAULT = PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_EDITOR,
} PropertyUsageFlags;

Хотя он имеет тот же размер, что и StringName, для него понятнее использовать другое имя.

Перечисления здесь — всего лишь вспомогательные элементы для наименования представляемых ими чисел. Информация о них содержится в файле extension_api.json. Здесь мы просто настраиваем те, которые нужны для руководства, чтобы сделать его более лаконичным.

Теперь перейдем к api.c и нам нужно загрузить указатели на новые функции, которые мы добавили в API.

void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address)
{
    ...
    // API
    ...
    api.classdb_register_extension_class_method = (GDExtensionInterfaceClassdbRegisterExtensionClassMethod)p_get_proc_address("classdb_register_extension_class_method");

    // Constructors.
    ...
    constructors.string_new_with_utf8_chars = (GDExtensionInterfaceStringNewWithUtf8Chars)p_get_proc_address("string_new_with_utf8_chars");

    // Destructors.
    ...
    destructors.string_destructor = variant_get_ptr_destructor(GDEXTENSION_VARIANT_TYPE_STRING);
}

Затем мы также можем реализовать функции для создания структуры PropertyInfo.

GDExtensionPropertyInfo make_property(
    GDExtensionVariantType type,
    const char *name)
{

    return make_property_full(type, name, PROPERTY_HINT_NONE, "", "", PROPERTY_USAGE_DEFAULT);
}

GDExtensionPropertyInfo make_property_full(
    GDExtensionVariantType type,
    const char *name,
    uint32_t hint,
    const char *hint_string,
    const char *class_name,
    uint32_t usage_flags)
{

    StringName *prop_name = api.mem_alloc(sizeof(StringName));
    constructors.string_name_new_with_latin1_chars(prop_name, name, false);
    String *prop_hint_string = api.mem_alloc(sizeof(String));
    constructors.string_new_with_utf8_chars(prop_hint_string, hint_string);
    StringName *prop_class_name = api.mem_alloc(sizeof(StringName));
    constructors.string_name_new_with_latin1_chars(prop_class_name, class_name, false);

    GDExtensionPropertyInfo info = {
        .name = prop_name,
        .type = type,
        .hint = hint,
        .hint_string = prop_hint_string,
        .class_name = prop_class_name,
        .usage = usage_flags,
    };

    return info;
}

void destruct_property(GDExtensionPropertyInfo *info)
{
    destructors.string_name_destructor(info->name);
    destructors.string_destructor(info->hint_string);
    destructors.string_name_destructor(info->class_name);
    api.mem_free(info->name);
    api.mem_free(info->hint_string);
    api.mem_free(info->class_name);
}

Простая версия make_property() просто вызывает более полную версию с некоторыми аргументами по умолчанию. Точное значение этих значений выходит за рамки данного руководства. Подробнее о методах и свойствах привязки см. на странице Object class.

Полная версия сложнее. Сначала она создаёт String и StringName для необходимых полей, выделяя память и вызывая их конструкторы. Затем создаёт структуру GDExtensionPropertyInfo и задаёт все поля с помощью предоставленных аргументов. Наконец, функция возвращает созданную структуру.

Функция destruct_property() проста, она просто вызывает деструкторы для созданных объектов и освобождает выделенную им память.

Давайте снова вернемся к заголовочному файлу api.h, чтобы создать функции, которые фактически свяжут методы:

// Version for 0 arguments, with return.
void bind_method_0_r(
    const char *class_name,
    const char *method_name,
    void *function,
    GDExtensionVariantType return_type);

// Version for 1 argument, no return.
void bind_method_1(
    const char *class_name,
    const char *method_name,
    void *function,
    const char *arg1_name,
    GDExtensionVariantType arg1_type);

Затем вернитесь к файлу api.c, чтобы реализовать следующее:

// Version for 0 arguments, with return.
void bind_method_0_r(
    const char *class_name,
    const char *method_name,
    void *function,
    GDExtensionVariantType return_type)
{
    StringName method_name_string;
    constructors.string_name_new_with_latin1_chars(&method_name_string, method_name, false);

    GDExtensionClassMethodCall call_func = call_0_args_ret_float;
    GDExtensionClassMethodPtrCall ptrcall_func = ptrcall_0_args_ret_float;

    GDExtensionPropertyInfo return_info = make_property(return_type, "");

    GDExtensionClassMethodInfo method_info = {
        .name = &method_name_string,
        .method_userdata = function,
        .call_func = call_func,
        .ptrcall_func = ptrcall_func,
        .method_flags = GDEXTENSION_METHOD_FLAGS_DEFAULT,
        .has_return_value = true,
        .return_value_info = &return_info,
        .return_value_metadata = GDEXTENSION_METHOD_ARGUMENT_METADATA_NONE,
        .argument_count = 0,
    };

    StringName class_name_string;
    constructors.string_name_new_with_latin1_chars(&class_name_string, class_name, false);

    api.classdb_register_extension_class_method(class_library, &class_name_string, &method_info);

    // Destruct things.
    destructors.string_name_destructor(&method_name_string);
    destructors.string_name_destructor(&class_name_string);
    destruct_property(&return_info);
}

// Version for 1 argument, no return.
void bind_method_1(
    const char *class_name,
    const char *method_name,
    void *function,
    const char *arg1_name,
    GDExtensionVariantType arg1_type)
{

    StringName method_name_string;
    constructors.string_name_new_with_latin1_chars(&method_name_string, method_name, false);

    GDExtensionClassMethodCall call_func = call_1_float_arg_no_ret;
    GDExtensionClassMethodPtrCall ptrcall_func = ptrcall_1_float_arg_no_ret;

    GDExtensionPropertyInfo args_info[] = {
        make_property(arg1_type, arg1_name),
    };
    GDExtensionClassMethodArgumentMetadata args_metadata[] = {
        GDEXTENSION_METHOD_ARGUMENT_METADATA_NONE,
    };

    GDExtensionClassMethodInfo method_info = {
        .name = &method_name_string,
        .method_userdata = function,
        .call_func = call_func,
        .ptrcall_func = ptrcall_func,
        .method_flags = GDEXTENSION_METHOD_FLAGS_DEFAULT,
        .has_return_value = false,
        .argument_count = 1,
        .arguments_info = args_info,
        .arguments_metadata = args_metadata,
    };

    StringName class_name_string;
    constructors.string_name_new_with_latin1_chars(&class_name_string, class_name, false);

    api.classdb_register_extension_class_method(class_library, &class_name_string, &method_info);

    // Destruct things.
    destructors.string_name_destructor(&method_name_string);
    destructors.string_name_destructor(&class_name_string);
    destruct_property(&args_info[0]);
}

Обе функции очень похожи. Сначала они создают StringName с именем метода. Он создаётся в стеке, так как нам не нужно его сохранять после завершения функции. Затем они создают локальные переменные для хранения call_func и ptrcall_func, указывающие на вспомогательные функции, которые мы определили ранее.

На следующем этапе они немного расходятся. Первый создаёт свойство для возвращаемого значения с пустым именем, поскольку оно не требуется. Второй создаёт массив свойств для аргументов, который в данном случае состоит из одного элемента. Этот же создаёт массив метаданных, который можно использовать, если аргумент обладает какими-то особыми свойствами (например, если значение int имеет длину 32 бита вместо 64 бит по умолчанию).

После этого они создают GDExtensionClassMethodInfo с обязательными полями для каждого случая. Затем они создают StringName для имени класса, чтобы связать метод с этим классом. Затем они вызывают функцию API, чтобы фактически привязать этот метод к классу. Наконец, мы уничтожаем созданные нами объекты, поскольку они больше не нужны.

Примечание

Вспомогательные функции связывания здесь используют созданные нами ранее вспомогательные функции вызова, поэтому обратите внимание, что эти вспомогательные функции вызова принимают только тип Godot FLOAT (эквивалент double в C). Если вы собираетесь использовать это для других типов, вам потребуется проверить тип аргументов и возвращаемого типа, а также выбрать подходящую функцию обратного вызова. Здесь это сделано только для того, чтобы пример не стал ещё длиннее.

Теперь, когда у нас есть возможность привязывать методы, мы можем сделать это в нашем классе. Перейдите в файл gdexample.c и заполните функцию gdexample_class_bind_methods():

void gdexample_class_bind_methods()
{
    bind_method_0_r("GDExample", "get_amplitude", gdexample_class_get_amplitude, GDEXTENSION_VARIANT_TYPE_FLOAT);
    bind_method_1("GDExample", "set_amplitude", gdexample_class_set_amplitude, "amplitude", GDEXTENSION_VARIANT_TYPE_FLOAT);

    bind_method_0_r("GDExample", "get_speed", gdexample_class_get_speed, GDEXTENSION_VARIANT_TYPE_FLOAT);
    bind_method_1("GDExample", "set_speed", gdexample_class_set_speed, "speed", GDEXTENSION_VARIANT_TYPE_FLOAT);
}

Поскольку эта функция уже вызывается процессом инициализации, мы можем остановиться на этом. После создания всей необходимой инфраструктуры эта функция стала гораздо проще. Видно, что реализация функций привязки в коде заняла бы много места и была бы довольно повторяющейся. Это также упростит добавление других методов в будущем.

If you compile the code and reopen the Godot project, nothing will be different at first, since we only added two new methods. To ensure those are registered properly, you can search for GDExample in the editor help and verify they are present in the documentation page.

../../../_images/gdextension_c_methods_doc.webp

Пользовательские свойства

Поскольку теперь у нас уже есть геттер и сеттер для наших свойств, мы можем перейти к созданию реальных свойств, которые будут отображаться в инспекторе редактора Godot.

Учитывая нашу обширную настройку, описанную в предыдущем разделе, для привязки свойств нам потребуется лишь несколько действий. Для начала добавим новую API-функцию в файл api.h:

extern struct API {
    ...
    GDExtensionInterfaceClassdbRegisterExtensionClassProperty classdb_register_extension_class_property;
} api;

Давайте также объявим здесь функцию для привязки свойств:

void bind_property(
    const char *class_name,
    const char *name,
    GDExtensionVariantType type,
    const char *getter,
    const char *setter);

В файле api.c мы можем загрузить новую функцию API:

void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address)
{
    // API
    ...
    api.classdb_register_extension_class_property = (GDExtensionInterfaceClassdbRegisterExtensionClassProperty)p_get_proc_address("classdb_register_extension_class_property");

    ...
}

Затем мы можем реализовать нашу новую вспомогательную функцию в этом же файле:

void bind_property(
    const char *class_name,
    const char *name,
    GDExtensionVariantType type,
    const char *getter,
    const char *setter)
{
    StringName class_string_name;
    constructors.string_name_new_with_latin1_chars(&class_string_name, class_name, false);
    GDExtensionPropertyInfo info = make_property(type, name);
    StringName getter_name;
    constructors.string_name_new_with_latin1_chars(&getter_name, getter, false);
    StringName setter_name;
    constructors.string_name_new_with_latin1_chars(&setter_name, setter, false);

    api.classdb_register_extension_class_property(class_library, &class_string_name, &info, &setter_name, &getter_name);

    // Destruct things.
    destructors.string_name_destructor(&class_string_name);
    destruct_property(&info);
    destructors.string_name_destructor(&getter_name);
    destructors.string_name_destructor(&setter_name);
}

Эта функция похожа на функцию для привязки методов. Главное отличие заключается в том, что нам не нужна дополнительная структура, поскольку мы можем просто использовать GDExtensionPropertyInfo, созданную нашей вспомогательной функцией, что делает её более простой. Она лишь создаёт значения StringName из строк C, создаёт структуру информации о свойстве с помощью нашей вспомогательной функции, вызывает функцию API для регистрации свойства в классе, а затем уничтожает все созданные нами объекты.

После этого мы можем расширить функцию gdexample_class_bind_methods() в файле gdexample.c:

void gdexample_class_bind_methods()
{
    bind_method_0_r("GDExample", "get_amplitude", gdexample_class_get_amplitude, GDEXTENSION_VARIANT_TYPE_FLOAT);
    bind_method_1("GDExample", "set_amplitude", gdexample_class_set_amplitude, "amplitude", GDEXTENSION_VARIANT_TYPE_FLOAT);
    bind_property("GDExample", "amplitude", GDEXTENSION_VARIANT_TYPE_FLOAT, "get_amplitude", "set_amplitude");

    bind_method_0_r("GDExample", "get_speed", gdexample_class_get_speed, GDEXTENSION_VARIANT_TYPE_FLOAT);
    bind_method_1("GDExample", "set_speed", gdexample_class_set_speed, "speed", GDEXTENSION_VARIANT_TYPE_FLOAT);
    bind_property("GDExample", "speed", GDEXTENSION_VARIANT_TYPE_FLOAT, "get_speed", "set_speed");
}

Если вы построите расширение с помощью scons, то в редакторе Godot вы увидите новое свойство, отображаемое не только на странице документации для пользовательского класса, но и в доке Инспектора при выборе узла GDExample.

../../../_images/gdextension_c_inspector_properties.webp

Привязка виртуальных методов

Теперь у нашего пользовательского узла есть свойства, влияющие на его работу, но он по-прежнему ничего не делает. В этом разделе мы привяжем виртуальный метод _process() и заставим наш пользовательский спрайт немного двигаться.

В файле gdexample.h добавим функцию, представляющую пользовательский метод _process():

// Methods.
void gdexample_class_process(GDExample *self, double delta);

Мы также добавим "частное" поле для отслеживания прошедшего времени в нашей пользовательской структуре. Оно "приватное" только в том смысле, что не будет привязано к API Godot, хотя и является публичным в C-версии, поскольку в этом языке отсутствуют модификаторы доступа.

typedef struct
{
    // Private properties.
    double time_passed;
    ...
} GDExample;

В исходном файле gdexample.c нам необходимо инициализировать новое поле в конструкторе:

void gdexample_class_constructor(GDExample *self)
{
    self->time_passed = 0.0;
    self->amplitude = 10.0;
    self->speed = 1.0;
}

Затем мы можем создать простейшую реализацию метода _process:

void gdexample_class_process(GDExample *self, double delta)
{
    self->time_passed += self->speed * delta;
}

Пока что он будет только обновлять созданное нами приватное поле. Мы вернёмся к этому после того, как метод будет правильно привязан.

Виртуальные методы немного отличаются от обычных привязок. Вместо того, чтобы явно регистрировать сам метод, мы зарегистрируем специальную функцию, которую Godot будет вызывать, чтобы проверить, реализован ли конкретный виртуальный метод в нашем расширении. Движок передаст StringName в качестве аргумента, поэтому, следуя духу этого руководства, мы создадим вспомогательную функцию для проверки его эквивалентности строке C.

Давайте добавим объявление в файл api.h:

// Compare a StringName with a C string.
bool is_string_name_equal(GDExtensionConstStringNamePtr p_a, const char *p_b);

Мы также добавим в этот файл новую структуру для хранения указателей функций для пользовательских операторов:

extern struct Operators
{
    GDExtensionPtrOperatorEvaluator string_name_equal;
} operators;

Затем в файле api.c мы загрузим указатель функции из API:

struct Operators operators;

void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address)
{
    // Get helper functions first.
    ...
    GDExtensionInterfaceVariantGetPtrOperatorEvaluator variant_get_ptr_operator_evaluator = (GDExtensionInterfaceVariantGetPtrOperatorEvaluator)p_get_proc_address("variant_get_ptr_operator_evaluator");

    ...

    // Operators.
    operators.string_name_equal = variant_get_ptr_operator_evaluator(GDEXTENSION_VARIANT_OP_EQUAL, GDEXTENSION_VARIANT_TYPE_STRING_NAME, GDEXTENSION_VARIANT_TYPE_STRING_NAME);
}

Как вы можете видеть, нам здесь нужен новый локальный помощник, чтобы получить указатель функции для оператора.

Используя эту удобную возможность, мы можем легко создать нашу функцию сравнения в том же файле:

bool is_string_name_equal(GDExtensionConstStringNamePtr p_a, const char *p_b)
{
    // Create a StringName for the C string.
    StringName string_name;
    constructors.string_name_new_with_latin1_chars(&string_name, p_b, false);

    // Compare both StringNames.
    bool is_equal = false;
    operators.string_name_equal(p_a, &string_name, &is_equal);

    // Destroy the created StringName.
    destructors.string_name_destructor(&string_name);

    // Return the result.
    return is_equal;
}

Эта функция создаёт StringName из аргумента, сравнивает его с другим значением, используя указатель на функцию оператора, и возвращает результат. Обратите внимание, что возвращаемое значение оператора передаётся как ссылка out, что является распространённой практикой в API.

Давайте вернемся к файлу gdexample.h и добавим пару функций, которые будут использоваться в качестве обратных вызовов для API Godot:

void *gdexample_class_get_virtual_with_data(void *p_class_userdata, GDExtensionConstStringNamePtr p_name);
void gdexample_class_call_virtual_with_data(GDExtensionClassInstancePtr p_instance, GDExtensionConstStringNamePtr p_name, void *p_virtual_call_userdata, const GDExtensionConstTypePtr *p_args, GDExtensionTypePtr r_ret);

На самом деле существует два способа регистрации виртуальных методов. Только один из них содержит часть get, в которой вы передаёте Godot правильно сформированный указатель на функцию, которая будет вызвана. Для этого нам пришлось бы создавать отдельный вспомогательный метод для каждого виртуального метода, что не очень удобно. Вместо этого мы используем второй метод, который позволяет нам возвращать любые данные, а затем Godot вызывает второй обратный вызов и возвращает нам эти данные вместе с информацией о вызове. Мы можем просто передать собственный указатель на функцию в качестве пользовательских данных и создать единый обратный вызов для всех виртуальных методов. Хотя в этом примере мы будем использовать его только для одного метода, этот способ проще в расширении.

Итак, давайте реализуем эти две функции в файле gdexample.c:

void *gdexample_class_get_virtual_with_data(void *p_class_userdata, GDExtensionConstStringNamePtr p_name)
{
    // If it is the "_process" method, return a pointer to the gdexample_class_process function.
    if (is_string_name_equal(p_name, "_process"))
    {
        return (void *)gdexample_class_process;
    }
    // Otherwise, return NULL.
    return NULL;
}

void gdexample_class_call_virtual_with_data(GDExtensionClassInstancePtr p_instance, GDExtensionConstStringNamePtr p_name, void *p_virtual_call_userdata, const GDExtensionConstTypePtr *p_args, GDExtensionTypePtr r_ret)
{
    // If it is the "_process" method, call it with a helper.
    if (p_virtual_call_userdata == &gdexample_class_process)
    {
        ptrcall_1_float_arg_no_ret(p_virtual_call_userdata, p_instance, p_args, r_ret);
    }
}

Эти функции также довольно просты после создания всех вспомогательных функций ранее.

В первом случае мы просто проверяем, является ли запрошенное имя функции _process, и если это так, то возвращаем указатель на нашу реализацию. В противном случае мы возвращаем NULL, сигнализируя, что метод не переопределяется. Мы не используем здесь p_class_userdata, поскольку эта функция предназначена только для одного класса, и у нас нет связанных с ней данных.

Второй метод аналогичен. Если это метод _process(), он использует заданный указатель на функцию для вызова вспомогательного метода ptrcall, передавая аргументы вызова вперёд. В противном случае он просто ничего не делает, поскольку у нас нет реализованных других виртуальных методов.

Единственное, чего не хватает, — это использования этих обратных вызовов при регистрации класса. Перейдите в файл init.c и измените инициализацию class_info, включив их, заменив ранее использованное значение NULL:

void initialize_gdexample_module(void *p_userdata, GDExtensionInitializationLevel p_level)
{
    ...

    GDExtensionClassCreationInfo2 class_info = {
        ...
        .get_virtual_call_data_func = gdexample_class_get_virtual_with_data,
        .call_virtual_with_data_func = gdexample_class_call_virtual_with_data,
        ...
    };

    ...
}

This is enough to bind the virtual method. If you build the extension and run the Godot project again, the _process() function will be called. You just won't be able to tell since the function itself does nothing visible. We will solve this now by making the custom node move following a pattern.

Чтобы наш узел работал, нам потребуется вызывать методы Godot. Не только функции API GDExtension, как мы делали до сих пор, но и методы самого движка, как при использовании скриптов. Это, естественно, требует дополнительной настройки.

Сначала давайте добавим Vector2 в наш файл defs.h, чтобы мы могли использовать его в нашем методе:

// The sizes can be obtained from the extension_api.json file.
...
#ifdef REAL_T_IS_DOUBLE
#define VECTOR2_SIZE 16
#else
#define VECTOR2_SIZE 8
#endif

...

// Types.

...

typedef struct
{
    uint8_t data[VECTOR2_SIZE];
} Vector2;

Определение REAL_T_IS_DOUBLE необходимо только в том случае, если ваша версия Godot была создана с поддержкой двойной точности, что не является поддержкой по умолчанию.

Теперь в файле api.h мы добавим несколько вещей в структуры API, включая новую для хранения вызываемых методов движка.

extern struct Constructors
{
    ...
    GDExtensionPtrConstructor vector2_constructor_x_y;
} constructors;

...

extern struct Methods
{
    GDExtensionMethodBindPtr node2d_set_position;
} methods;

extern struct API
{
    ...
    GDExtensionInterfaceClassdbGetMethodBind classdb_get_method_bind;
    GDExtensionInterfaceObjectMethodBindPtrcall object_method_bind_ptrcall;
} api;

Затем в файле api.c мы можем получить указатели функций из Godot:

struct Methods methods;

void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address)
{
    // Get helper functions first.
    ...
    GDExtensionInterfaceVariantGetPtrConstructor variant_get_ptr_constructor = (GDExtensionInterfaceVariantGetPtrConstructor)p_get_proc_address("variant_get_ptr_constructor");

    // API.
    ...
    api.classdb_get_method_bind = (GDExtensionInterfaceClassdbGetMethodBind)p_get_proc_address("classdb_get_method_bind");
    api.object_method_bind_ptrcall = (GDExtensionInterfaceObjectMethodBindPtrcall)p_get_proc_address("object_method_bind_ptrcall");

    // Constructors.
    ...
    constructors.vector2_constructor_x_y = variant_get_ptr_constructor(GDEXTENSION_VARIANT_TYPE_VECTOR2, 3); // See extension_api.json for indices.

    ...
}

Единственный примечательный фрагмент здесь — конструктор Vector2, для которого мы запрашиваем индекс 3. Поскольку существует несколько конструкторов с разными типами аргументов, нам нужно указать, какой из них нам нужен. В данном случае мы получаем тот, который принимает два числа с плавающей точкой в качестве координат x и y, отсюда и название. Этот индекс можно получить из файла extension_api.json. Обратите внимание, что для его получения нам также понадобится новый локальный вспомогательный метод.

Обратите внимание, что для структуры методов здесь ничего не происходит. Это связано с тем, что эта функция вызывается слишком рано в процессе инициализации, поэтому классы ещё не будут должным образом зарегистрированы.

Вместо этого мы будем использовать обратный вызов уровня инициализации для их получения при регистрации нашего пользовательского класса. Добавьте это в файл init.c:

void initialize_gdexample_module(void *p_userdata, GDExtensionInitializationLevel p_level)
{
    if (p_level != GDEXTENSION_INITIALIZATION_SCENE)
    {
        return;
    }

    // Get ClassDB methods here because the classes we need are all properly registered now.
    // See extension_api.json for hashes.
    StringName native_class_name;
    StringName method_name;

    constructors.string_name_new_with_latin1_chars(&native_class_name, "Node2D", false);
    constructors.string_name_new_with_latin1_chars(&method_name, "set_position", false);
    methods.node2d_set_position = api.classdb_get_method_bind(&native_class_name, &method_name, 743155724);
    destructors.string_name_destructor(&native_class_name);
    destructors.string_name_destructor(&method_name);

    ...
}

Здесь мы создаём StringName для класса и метода, которые хотим получить, а затем используем API GDExtension для получения их MethodBind — объекта, представляющего привязанный метод. Метод set_position мы получаем из Node2D, поскольку именно там он был зарегистрирован, хотя мы собираемся использовать его в Sprite2D, производном классе.

Случайное на первый взгляд число для получения привязки на самом деле является хешем сигнатуры метода. Это позволяет Godot сопоставлять запрашиваемый вами метод, даже если в будущей версии Godot эта сигнатура изменится, предоставляя метод совместимости, соответствующий вашему запросу. Это одна из систем, позволяющих движку загружать расширения, созданные для предыдущих версий. Вы можете получить значение этого хеша из файла extension_api.json.

После всего этого мы наконец можем реализовать наш собственный метод _process() в файле gdexample.c:

...

#include <math.h>

...

void gdexample_class_process(GDExample *self, double delta)
{
    self->time_passed += self->speed * delta;

    Vector2 new_position;

    // Set up the arguments for the Vector2 constructor.
    double x = self->amplitude + (self->amplitude * sin(self->time_passed * 2.0));
    double y = self->amplitude + (self->amplitude * cos(self->time_passed * 1.5));
    GDExtensionConstTypePtr args[] = {&x, &y};
    // Call the Vector2 constructor.
    constructors.vector2_constructor_x_y(&new_position, args);

    // Set up the arguments for the set_position method.
    GDExtensionConstTypePtr args2[] = {&new_position};
    // Call the set_position method.
    api.object_method_bind_ptrcall(methods.node2d_set_position, self->object, args2, NULL);
}

После обновления прошедшего времени, масштабированного свойством speed, на его основе создаются значения x и y, также модулированные свойством amplitude. Именно это и создаёт эффект узора. Заголовочный файл math.h необходим для функций sin() и cos(), используемых здесь.

Затем он создаёт массив аргументов для создания Vector2, после чего вызывает конструктор. Он создаёт ещё один массив аргументов и использует его для вызова метода set_position() через ранее полученную нами привязку.

Поскольку здесь не выделяется память, нет необходимости в очистке.

Теперь мы можем снова собрать расширение и снова открыть Godot. Даже в редакторе вы увидите, как движется пользовательский спрайт.

../../../_images/gdextension_c_moving_sprite.gif

Попробуйте изменить свойства Speed и Amplitude и посмотрите, как отреагирует спрайт.

Регистрация и выдача сигнала

Чтобы завершить это руководство, давайте посмотрим, как зарегистрировать собственный сигнал и подавать его при необходимости. Как вы, возможно, догадались, нам понадобится ещё несколько указателей на функции из API и несколько вспомогательных функций.

В файл api.h мы добавляем две вещи: API-функцию для регистрации сигнала и вспомогательную функцию для обёртки привязки сигнала.

extern struct API
{
    ...
    GDExtensionInterfaceClassdbRegisterExtensionClassSignal classdb_register_extension_class_signal;
} api;

...

// Version for 1 argument.
void bind_signal_1(
    const char *class_name,
    const char *signal_name,
    const char *arg1_name,
    GDExtensionVariantType arg1_type);

В этом случае у нас есть версия только для одного аргумента, поскольку именно его мы и собираемся использовать.

Перейдя к файлу api.c, мы можем загрузить этот новый указатель на функцию и реализовать помощника:

void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address)
{
    // API.
    ...
    api.classdb_register_extension_class_signal = (GDExtensionInterfaceClassdbRegisterExtensionClassSignal)p_get_proc_address("classdb_register_extension_class_signal");

    ...
}

void bind_signal_1(
    const char *class_name,
    const char *signal_name,
    const char *arg1_name,
    GDExtensionVariantType arg1_type)
{
    StringName class_string_name;
    constructors.string_name_new_with_latin1_chars(&class_string_name, class_name, false);
    StringName signal_string_name;
    constructors.string_name_new_with_latin1_chars(&signal_string_name, signal_name, false);

    GDExtensionPropertyInfo args_info[] = {
        make_property(arg1_type, arg1_name),
    };

    api.classdb_register_extension_class_signal(class_library, &class_string_name, &signal_string_name, args_info, 1);

    // Destruct things.
    destructors.string_name_destructor(&class_string_name);
    destructors.string_name_destructor(&signal_string_name);
    destruct_property(&args_info[0]);
}

Эта функция очень похожа на функцию для связывания методов. Главное отличие в том, что нам не нужно заполнять ещё одну структуру, мы просто передаём необходимые имена и массив аргументов. 1 в конце означает количество аргументов, передаваемых сигналом.

С помощью этого мы можем привязать сигнал в gdexample.c:

void gdexample_class_bind_methods()
{
    ...
    bind_signal_1("GDExample", "position_changed", "new_position", GDEXTENSION_VARIANT_TYPE_VECTOR2);
}

Чтобы подать сигнал, нам нужно вызвать метод emit_signal() в нашем пользовательском узле. Поскольку это функция vararg (то есть принимающая любое количество аргументов), мы не можем использовать ptrcall. Для обычного вызова нам нужно создать варианты, что требует дополнительных действий.

Сначала в файле defs.h мы создаем определение для Variant:

...

// The sizes can be obtained from the extension_api.json file.
...
#ifdef REAL_T_IS_DOUBLE
#define VARIANT_SIZE 40
#define VECTOR2_SIZE 16
#else
#define VARIANT_SIZE 24
#define VECTOR2_SIZE 8
#endif

...

// Types.

...

typedef struct
{
    uint8_t data[VARIANT_SIZE];
} Variant;

Сначала мы задаём размер Variant вместе с размером Vector2, который мы добавили ранее. Затем мы используем его для создания непрозрачной структуры, достаточной для хранения данных Variant. Мы снова задаём размер для сборок с двойной точностью в качестве запасного варианта, поскольку официально Godot использует одинарную точность.

Функция emit_signal() будет вызываться с двумя аргументами. Первый — это имя испускаемого сигнала, а второй — аргумент, который мы передаём в соединения с сигналом. Это Vector2, как мы объявили при привязке. Поэтому мы создадим вспомогательную функцию, которая сможет вызывать MethodBind с этими типами. Несмотря на то, что она что-то возвращает (код ошибки), нам не нужно с этим разбираться, поэтому пока мы просто проигнорируем его.

В api.h мы добавляем несколько вещей к существующим структурам, а также новую вспомогательную функцию для вызова:

extern struct Constructors
{
    ...
    GDExtensionVariantFromTypeConstructorFunc variant_from_string_name_constructor;
    GDExtensionVariantFromTypeConstructorFunc variant_from_vector2_constructor;
} constructors;

extern struct Destructors
{
    ..
    GDExtensionInterfaceVariantDestroy variant_destroy;
} destructors;

...

extern struct Methods
{
    ...
    GDExtensionMethodBindPtr object_emit_signal;
} methods;

extern struct API
{
    ...
    GDExtensionInterfaceObjectMethodBindCall object_method_bind_call;
} api;

...

// Helper to call with Variant arguments.
void call_2_args_stringname_vector2_no_ret_variant(
    GDExtensionMethodBindPtr p_method_bind,
    GDExtensionObjectPtr p_instance,
    const GDExtensionTypePtr p_arg1,
    const GDExtensionTypePtr p_arg2);

Теперь перейдем к файлу api.c, чтобы загрузить эти новые указатели функций и реализовать вспомогательную функцию.

void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address)
{
    // API.
    ...
    api.object_method_bind_call = (GDExtensionInterfaceObjectMethodBindCall)p_get_proc_address("object_method_bind_call");

    // Constructors.
    ...
    constructors.variant_from_string_name_constructor = api.get_variant_from_type_constructor(GDEXTENSION_VARIANT_TYPE_STRING_NAME);
    constructors.variant_from_vector2_constructor = api.get_variant_from_type_constructor(GDEXTENSION_VARIANT_TYPE_VECTOR2);

    // Destructors.
    ...
    destructors.variant_destroy = (GDExtensionInterfaceVariantDestroy)p_get_proc_address("variant_destroy");

    ...
}

...

void call_2_args_stringname_vector2_no_ret_variant(GDExtensionMethodBindPtr p_method_bind, GDExtensionObjectPtr p_instance, const GDExtensionTypePtr p_arg1, const GDExtensionTypePtr p_arg2)
{
    // Set up the arguments for the call.
    Variant arg1;
    constructors.variant_from_string_name_constructor(&arg1, p_arg1);
    Variant arg2;
    constructors.variant_from_vector2_constructor(&arg2, p_arg2);
    GDExtensionConstVariantPtr args[] = {&arg1, &arg2};

    // Add dummy return value storage.
    Variant ret;

    // Call the function.
    api.object_method_bind_call(p_method_bind, p_instance, args, 2, &ret, NULL);

    // Destroy the arguments.
    destructors.variant_destroy(&arg1);
    destructors.variant_destroy(&arg2);
    destructors.variant_destroy(&ret);
}

Эта вспомогательная функция содержит шаблонный код, но довольно проста. Она устанавливает два аргумента внутри выделенных в стеке вариантов, а затем создаёт массив с указателями на них. Она также создаёт ещё один вариант для хранения возвращаемого значения, которое нам не нужно создавать, поскольку вызов предполагает, что оно не инициализировано.

Затем он фактически вызывает MethodBind, используя предоставленный нами экземпляр и аргументы. NULL в конце будет указателем на структуру GDExtensionCallError. Это можно использовать для обработки потенциальных ошибок при вызове функций (например, неверных аргументов). Для простоты мы не будем обрабатывать это здесь.

В конце нам нужно уничтожить созданные нами варианты. Хотя технически Vector2 не требует уничтожения, проще очистить всё.

Нам также необходимо загрузить MethodBind, что мы сделаем в файле init.c, сразу после загрузки метода set_position, которую мы сделали ранее:

void initialize_gdexample_module(void *p_userdata, GDExtensionInitializationLevel p_level)
{
    ...

    constructors.string_name_new_with_latin1_chars(&native_class_name, "Object", false);
    constructors.string_name_new_with_latin1_chars(&method_name, "emit_signal", false);
    methods.object_emit_signal = api.classdb_get_method_bind(&native_class_name, &method_name, 4047867050);
    destructors.string_name_destructor(&native_class_name);
    destructors.string_name_destructor(&method_name);

    // Register class.
    ...
}

Обратите внимание, что здесь мы повторно используем переменные native_class_name и method_name, поэтому нам не нужно объявлять новые.

Теперь перейдите в файл gdexample.h, куда мы добавим пару полей:

typedef struct
{
    // Private properties.
    ..
    double time_emit;
    ..
    // Metadata.
    StringName position_changed; // For signal.
} GDExample;

Первый будет хранить время, прошедшее с момента последнего сигнала, поскольку мы будем делать это регулярно. Второй — просто кэшировать имя сигнала, чтобы не создавать каждый раз новое StringName.

В исходном файле gdexample.c мы можем изменить конструктор и деструктор для работы с новыми полями:

void gdexample_class_constructor(GDExample *self)
{
    ...
    self->time_emit = 0.0;

    // Construct the StringName for the signal.
    constructors.string_name_new_with_latin1_chars(&self->position_changed, "position_changed", false);
}

void gdexample_class_destructor(GDExample *self)
{
    // Destruct the StringName for the signal.
    destructors.string_name_destructor(&self->position_changed);
}

Важно уничтожить StringName, чтобы избежать утечек памяти.

Теперь мы можем добавить функцию gdexample_class_process() для фактической отправки сигнала:

void gdexample_class_process(GDExample *self, double delta)
{
    ...

    self->time_emit += delta;
    if (self->time_emit >= 1.0)
    {
        // Call the emit_signal method.
        call_2_args_stringname_vector2_no_ret_variant(methods.object_emit_signal, self->object, &self->position_changed, &new_position);
        self->time_emit = 0.0;
    }
}

Это обновляет время, прошедшее с момента излучения сигнала, и, если оно превышает одну секунду, вызывает функцию emit_signal() для текущего экземпляра, передавая имя сигнала и новую позицию в качестве аргументов.

Now we're done with our C GDExtension. Build it once more and reopen the Godot project in the editor.

На странице документации по GDExample вы можете увидеть новый сигнал, который мы привязали:

../../../_images/gdextension_c_signal_doc.webp

Чтобы проверить, как это работает, давайте добавим небольшой скрипт в корневой узел, родительский для нашего пользовательского узла, который выводит позицию на выход каждый раз при получении сигнала:

extends Node2D

func _ready():
    $GDExample.position_changed.connect(on_position_changed)

func on_position_changed(new_position):
    prints("New position:", new_position)

Запустите проект, и вы сможете наблюдать, как значения печатаются в доке вывода в редакторе:

../../../_images/gdextension_c_signal_print.webp

Заключение

В этом руководстве показано базовое расширение с пользовательскими методами, свойствами и сигналами. Хотя оно требует значительного количества шаблонного кода, его можно легко масштабировать, создав вспомогательные функции для решения сложных задач.

Это должно послужить хорошей основой для понимания API GDExtension и отправной точкой для создания собственных генераторов привязок. Фактически, с помощью такого генератора можно создавать привязки для C, что делает код более похожим на файл gdexample.c из этого примера, который довольно прост и не слишком многословен.

Если вы хотите создавать настоящие расширения, предпочтительнее использовать привязки C++, поскольку это избавляет от всего шаблонного кода. Чтобы узнать, как это сделать, ознакомьтесь с godot-cpp documentation.