Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

GDExtension C 示例

介绍

这是一个关于如何在 C 代码中直接使用 GDExtension 的简单示例。请注意,相关 API 并不是为直接使用而设计的,所以注定会相当冗长,即便是小型示例也需要大量步骤。不过,该示例可以用作绑定其他语言的参考。如果你愿意,仍然可以直接使用这些 API,这在只用来绑定第三方库的情况下可能比较方便。

在本示例中,我们将创建一个自定义节点,它会根据用户参数在屏幕上移动精灵。虽然非常简单,但它展示了 GDExtension 的使用方法,比如注册带有方法、属性和信号的自定义类,让你能够对 GDExtension API 有一个初步的了解。

设置项目

你需要具备以下几个前置条件:

  • 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

我们还需要复制一份 Godot 源码中的 gdextension_interface.h 头文件,可以通过运行以下命令直接从 Godot 可执行文件中获取:

godot --dump-gdextension-interface

这样就会在当前文件夹中创建这个头文件,你可以直接将其复制到示例项目的 src 文件夹中。

最后,我们还需要参考另一个信息来源,那就是包含 Godot API 参考的 JSON 文件。代码不会直接使用这个文件,我们只会手动从中提取一些信息。

要获取这个 JSON 文件,只需调用 Godot 可执行文件:

godot --dump-extension-api

生成的 extension_api.json 文件会保存在当前文件夹中。你可以将此文件复制到示例文件夹以方便查看。

备注

本扩展的目标平台是 Godot 4.2,但应该也能在其他更高版本上工作。如果你想针对不同的最低版本,请确保从目标版本的 Godot 中获取对应的头文件和 JSON 文件。

构建系统

使用构建系统可以大幅简化 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)

这会包含 src 文件夹中的所有 C 文件,因此在添加新的源文件时就不需要更改此文件了。

初始化扩展

第一段代码负责初始化扩展,这使得 Godot 能够知道我们的 GDExtension 提供哪些功能,例如类和插件。

请在 src 文件夹中创建名为 init.h 的文件,内容如下:

#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);

这里声明的函数具有 GDExtension API 所期望的签名。

注意其中包含了 defs.h 文件。这是我们用来简化编写扩展代码的辅助工具之一。目前它仅包含 GDE_EXPORT 的定义,这个宏可以将函数在共享库中导出,以便 Godot 正确调用。这个宏有助于抽象各个编译器的差异。

请在 src 文件夹中创建名为 defs.h 的文件,内容如下:

#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,标准头文件也会被一并引入。

现在来实现刚刚声明的函数吧。在 src 文件夹中创建名为 init.c 的文件,并添加以下代码:

#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

请在 src 文件夹中创建名为 gdexample.h 的文件,内容如下:

#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 创建一个已知类型的对象并将我们的扩展附加到它上面。举例来说,在调用父类的方法时会需要引用此类对象。

请为这个头文件创建对应的源文件。在 src 文件夹中创建名为 gdexample.c 的文件,并添加如下代码:

#include "gdexample.h"

void gdexample_class_constructor(GDExample *self)
{
}

void gdexample_class_destructor(GDExample *self)
{
}

void gdexample_class_bind_methods()
{
}

暂时我们还用不到这些函数,先保持为空。

下一步是注册我们的类。为此,我们需要创建一个 StringName,而这要求我们必须从 GDExtension API 中获取一个函数。由于我们会多次需要该操作以及其他一些功能,我们来创建一个封装 API 来简化这类重复性工作吧。

封装 API

首先我们在 src 文件夹中创建 api.h 文件:

#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);

随着我们用实际功能完善扩展,这个文件也会逐步引入许多其他辅助函数。目前它就仅有三个函数指针:一个用于根据 C 字符串(Latin-1 编码)创建 StringName;另一个用于析构 StringName,避免内存泄漏;还有就是我们最初的目标,用于注册类的函数。

我们在这里还保留了对 class_library 的引用。这是 Godot 在初始化扩展时提供给我们的内容,当注册我们创建的内容时需要用到它,这样才能让 Godot 识别是哪个扩展发出的调用。

还有一个函数的作用是从 GDExtension API 中加载这些函数指针。

我们来编写这个头文件对应的源文件吧。在 src 文件夹中创建名为 api.c 的文件,并添加如下代码:

#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。这是来自 GDExtension API 的一个函数,会在初始化时传递进来。你可以使用这个函数通过名称向 API 请求特定函数。我们在此处缓存结果,这样就不必在各个地方都保存对 p_get_proc_address 的引用,而是可以直接使用我们的封装。

一开始我们请求的是 variant_get_ptr_destructor() 函数。它不会被此函数外的代码使用,所以我们不将它添加到我们的封装中,仅在本地缓存。这个强制类型转换是必要的,用于消除编译器警告。

接着我们获取的是之前提到的用于根据 C 字符串创建 StringName 的函数。我们将其存储在我们定义的 constructors 结构体中。

接下来,我们使用刚刚获取的 variant_get_ptr_destructor() 函数来查询 StringName 的析构函数,参数是 gdextension_interface.h API 中的枚举值。其他类型的析构函数也可以通过类似的方式获取,但这里仅限制在示例所需范围。

最后,我们获取 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_funcfree_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 属性下面。这里假设我们使用的是 64 位版本的 Godot,因此永远不会定义 BUILD_32;但如果你确有需要,可以在 SConstruct 文件中加入 env.Append(CPPDEFINES=["BUILD_32"])

// 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;

然后修改 api.c 中的 load_api() 函数以获取这些新函数:

...
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 对象,因为引擎会自行处理。

演示项目

现在我们已经可以创建和释放自定义对象了,应该可以尝试在实际项目中使用了。为此,你需要打开 Godot 并在 project 文件夹中新建一个项目。如果之前已经编译过这个扩展,项目管理器可能会警告该文件夹不为空,这次你可以安全地忽略这个警告。

如果还没编译扩展的话,现在是时候了。打开终端或命令提示符,进入扩展的根目录并运行 scons。由于这个扩展非常简单,编译应该会很快完成。

接着,在 project 文件夹里创建一个名为 gdexample.gdextension 的文件。这是一个 Godot 资源,用于描述扩展以让引擎正确加载它。将如下内容放入该文件:

[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,因为我们目标是 4.2。这在更高的 Godot 版本上也仍应正常工作。如果你正在使用更高版本的 Godot 并依赖其新功能,则需要同步提高此版本号到包含所有你用到的特性的版本。详细信息请参阅版本兼容性

[libraries] 部分中,我们设置了不同平台上共享库的路径。这里只有调试版本,因为我们在这个示例中使用的是调试版本。可以使用功能标记 "微调此项设置,从而增加发布版本、添加更多的目标平台以及 32位和 64 位的二进制文件。

你还可以在这个文件中添加库依赖和你自定义类的图标,但这不在本教程范围内。

保存文件后,回到编辑器。Godot 会自动加载扩展。因为我们的扩展只注册了一个新类,此时还看不到任何效果。要使用这个类,先添加一个 Node2D 作为场景的根节点,并把它移动到视窗的中央以获得更好的可见性。然后向根节点添加一个新子节点,并在创建新节点对话框中搜索“GDExample”(即我们所注册的自定义类的名字),它会显示在列表里。如果没有显示,说明 Godot 未能正确加载这个扩展,请尝试重启编辑器并重走之前的步骤,看看是否有遗漏。

自定义类由 Sprite2D 派生而来,所以在检查器中有一个 Texture 属性。将其设置为 Godot 在生成项目时为我们顺手创建的 icon.svg 文件。将场景另存为 main.tscn 然后运行它。为了方便,你可能想将其设置为主场景。

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

好了!我们已经创建了一个可以在 Godot 中运行的自定义节点。但目前它还不能做任何事情,也没有任何不同于普通 Sprite2D 节点的地方。接下来,我们将通过添加自定义方法和属性来解决这个问题。

自定义方法

在扩展中,一个常见的做法是为自定义类创建方法并将其暴露给 Godot API。我们将创建一些 getter 和 setter,这在后续绑定属性时是必需的。

首先,在结构体中添加一些新字段来保存 amplitudespeed 的值,稍后在为节点创建行为时将用到。将它们添加到 gdexample.h 文件中,修改 GDExample 结构体:

...

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

...

在同一文件中,紧接着析构函数之后为 getter 和 setter 添加声明。

...

// 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 的函数(对应 getter),另一个针对接受单个 double 参数、无返回值的函数(对应 setter)。

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);

这两个函数遵循 gdextension_interface.h 中定义的 GDExtensionClassMethodPtrCall 类型。这里之所以使用 float 作为名称是因为 Godot 中的 float 类型实际上是双精度(double)类型的,因此遵循这一惯例。

然后在 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 的指针,而不是确切的类型。此外,还有参数数量和出现错误时你可以设置的错误结构体。

为了检查类型并与 Variant 进行交互,我们需要从 GDExtension API 中获取另外几个函数。请扩展我们的封装结构体:

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 提取和创建浮点值。我们还有一些辅助函数来实际获取这些构造函数,以及一个用于确定 Variant 类型的函数。

跟之前一样,通过修改 api.c 文件中的 load_api() 函数从 API 获取这些数据:

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,因为我们需要创建需要适当处理的 String 和 StringName。

说到这里,我们还需要一种创建和销毁 String 的方法,因此我们将在同一个文件中为现有结构体添加内容。我们还将获得一个新的 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 类的页面以获取有关绑定方法和属性的更多详细信息。

完整版本更为复杂。首先,它通过分配内存并调用构造函数,为所需字段创建 StringStringName。然后,它创建一个 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_funcptrcall_func,指向我们之前定义的辅助函数。

在接下来的步骤中,它们会有一些不同。第一个为返回值创建了一个属性,由于不需要所以名称留空。另一个则为参数创建了一个属性数组,在这个例子中只有一个元素。它还包含一个元数据数组,如果参数有特殊之处(例如,如果一个 int 值是 32 位长而不是默认的 64 位),就可以使用它。

随后,它们为每种情况创建了包含所需字段的 GDExtensionClassMethodInfo。然后为类名创建一个 StringName,以便将方法与类关联起来。接着,它们调用 API 函数实际将该方法绑定到类。最后,我们销毁了之前创建的对象,因为它们不再需要了。

备注

这里的绑定辅助函数使用了我们之前创建的调用辅助函数,因此请注意这些调用辅助函数仅接受 Godot 的 FLOAT 类型(相当于 C 语言中的 double)。如果你打算将其用于其他类型,你需要检查参数和返回值的类型,并选择合适的函数回调。这里为了避免示例变得过长,我们省略了这部分处理。

现在我们有了绑定方法的工具,可以在自定义类中实际进行绑定操作。打开 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);
}

由于该函数已经在初始化过程中被调用,我们可以到此为止。在我们创建了所有基础设施使其工作后,这个函数变得更加简单明了。你可以看到,在这里内联实现绑定函数会占用一些空间,而且会相当重复。这也使得将来添加另一个方法变得更加容易。

如果你编译代码并重新打开 Godot 项目,一开始不会有任何变化,因为我们只添加了两个新方法。为了确保这些方法已正确注册,你可以在编辑器帮助中搜索 GDExample,并确认它们是否出现在文档页面中。

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

自定义属性

由于我们现在已经绑定了属性的 getter 和 setter,我们可以继续创建将在 Godot 编辑器检查器中显示的实际属性。

在前一节中我们已经进行了大量的设置,现在只需要做一些小调整就可以实现属性绑定。首先,让我们在 api.h 文件中添加一个新的 API 函数:

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,所以更加直接。它只会从 C 字符串创建 StringName 值,使用我们的辅助函数创建属性信息结构体,调用 API 函数在类中注册属性,然后销毁我们创建的所有对象。

完成这些后,我们可以在 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_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);

我们还会添加一个“私有”字段来记录自定义结构体中经过的时间。这里的“私有”仅表示它不会绑定到 Godot API,在 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 文件,添加几个将用作 Godot API 回调的函数:

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,
        ...
    };

    ...
}

这就足以绑定虚方法了。如果你构建扩展并再次运行 Godot 项目,_process() 函数将会被调用。只是你无法察觉,因为这个函数本身没有做任何可见的事情。我们现在将通过让自定义节点按照某种模式移动来解决这个问题。

为了让我们的节点做点事情,我们需要调用 Godot 的方法。不仅仅是像我们之前一直在使用的 GDExtension API 函数,还包括实际的引擎方法,就像我们使用脚本时那样。这自然需要一些额外的设置。

首先,让我们将 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。由于存在多个具有不同类型参数的构造函数,我们需要指定我们想要的是哪一个。在这种情况下,我们获取的是接受两个浮点数作为 xy 坐标的构造函数,因此得名。这个索引可以从 extension_api.json 文件中检索到。注意我们还需要一个新的本地辅助函数来获取它。

请注意,我们无法在此处获取 methods 结构体的任何内容。这是因为该函数在初始化过程中调用得过早,类尚未正确注册。

相反,我们将在注册自定义类时使用初始化级别的回调来获取这些内容。将此添加到 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,然后使用 GDExtension API 来检索它们的 MethodBind,这是一个表示绑定方法的对象。我们从 Node2D 中获取 set_position 方法,因为这是它被注册的地方,即使我们将在派生类 Sprite2D 中使用它。

获取绑定的看似随机数实际上是方法签名的哈希值。这使得 Godot 能够在未来版本中即使这个签名发生变化,也能通过提供与你请求匹配的兼容方法来匹配你请求的方法。这是允许引擎加载为先前版本制作的扩展的系统之一。你可以从 extension_api.json 文件中获取这个哈希值。

有了这些准备,我们终于可以在 gdexample.c 文件中实现自定义的 _process() 方法了:

...

#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 属性缩放更新时间后,它会基于该时间生成 xy 值,这些值也会受到 amplitude 属性的调制。这就是产生图案效果的原因。这里使用的 sin()cos() 函数需要包含 math.h 头文件。

然后它会设置一个参数数组来构造一个 Vector2,随后调用构造函数。接着它会设置另一个参数数组,并使用之前获取的绑定来调用 set_position() 方法。

由于这里没有分配任何内存,因此不需要进行清理。

现在我们可以再次构建扩展并重新打开 Godot。即使在编辑器中,你也能看到自定义精灵在移动。

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

尝试修改 SpeedAmplitude 属性,观察精灵的反应。

信号的注册和触发

为了完成本教程,让我们看看如何注册自定义信号并在适当的时候触发它。正如你可能已经猜到的,我们需要从 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。要进行常规调用,我们必须创建 Variant,这需要更多的步骤来完成。

首先,在 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);
}

这个辅助函数虽然包含了一些样板代码,但实现起来相当直接。它在栈上分配了两个 Variant 变量来设置参数,然后创建了一个包含这些变量指针的数组。它还设置了另一个 Variant 来保存返回值,由于调用期望它是未初始化的,所以我们不需要构造它。

然后它实际上会使用我们提供的实例和参数来调用 MethodBind。末尾的 NULL 是一个指向 GDExtensionCallError 结构体的指针。这可以用于处理调用函数时可能出现的错误(例如错误的参数)。为了简单起见,我们这里不处理这个。

最后,我们需要销毁我们创建的 Variant。虽然从技术上讲,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_namemethod_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;
    }
}

这将更新信号发射的时间,如果超过 1 秒,它将在当前实例上调用 emit_signal() 函数,并将信号名称和新位置作为参数传递。

现在我们已经完成了 C GDExtension 的编写。再次构建它,并在编辑器中重新打开演示项目。

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

总结

本教程展示了如何创建一个包含自定义方法、属性和信号的基础扩展。虽然这需要编写大量样板代码,但只要通过创建辅助函数来处理繁琐的任务,这种方式具有良好的扩展性。

这应该能很好地帮助你理解 GDExtension API,并为你创建自定义绑定生成器提供一个起点。实际上,你可以使用这种类型的生成器为 C 语言创建绑定,使实际编码看起来更像本示例中的 gdexample.c 文件,这种方式非常直接且并不冗长。

如果你想创建实际的扩展,建议使用 C++ 绑定,因为它可以消除代码中的所有样板文件。查看 godot-cpp 文档了解如何实现这一点。