Первое нативное расширение на C17
Полный Windows x64 пример: компиляция, регистрация и проверка ABI.
Этот пример подключает C17-библиотеку, регистрирует абстрактный класс DocsCProbe с базой Entity и две константы, затем проверяет их из C#. Движение спрайта, свойства экземпляров и сигналы здесь не реализуются: сначала получаем небольшой полностью проверяемый ABI-контракт.
Условия и структура
Проверенная цель — Windows x64 с текущим ExtensionSDK 1.0.0, конфигурацией float_64 и MSVC с поддержкой C17. Для проверки запуска нужен C# проект с SDK Reckfell 1.0.0. Само расширение написано на C и не требует C++-обёртки.
Расположите файлы так:
project/
project.reckfell
NativeCChecks.cs
addons/c/
native_c_probe.c
native_c_probe.reckfellextension
native_c_probe.windows.x86_64.dllЗаголовок reckfellextension_interface.h берите из ExtensionSDK/include той сборки, в которой будете запускать DLL. Данные размеров — из парного extension_api.json; в этом примере StringName занимает 8 байт. Не переносите этот размер на другой ABI без проверки.
native_c_probe.c
#include "reckfellextension_interface.h"
// This example is deliberately Windows x64 only, not a general C binding.
typedef struct { void *opaque; } NativeStringName;
_Static_assert(sizeof(NativeStringName) == 8, "Check StringName size against extension_api.json");
_Static_assert(RECKFELLEXTENSION_VARIANT_TYPE_ENTITY_PATH == 22, "Unexpected EntityPath ABI");
static ReckfellExtensionClassLibraryPtr library;
static ReckfellExtensionInterfaceStringNameNewWithUtf8Chars name_new;
static ReckfellExtensionPtrDestructor name_destroy;
static ReckfellExtensionInterfaceClassdbRegisterExtensionClass6 register_class;
static ReckfellExtensionInterfaceClassdbUnregisterExtensionClass unregister_class;
static ReckfellExtensionInterfaceClassdbRegisterExtensionClassIntegerConstant register_constant;
static void free_instance(void *userdata, ReckfellExtensionClassInstancePtr instance) {
(void)userdata;
(void)instance;
}
static void initialize(void *userdata, ReckfellExtensionInitializationLevel level) {
(void)userdata;
if (level != RECKFELLEXTENSION_INITIALIZATION_SCENE) return;
NativeStringName name, parent, enum_name, constant;
name_new(&name, "DocsCProbe");
name_new(&parent, "Entity");
name_new(&enum_name, "");
ReckfellExtensionClassCreationInfo6 info = {0};
info.is_abstract = 1;
info.is_exposed = 1;
info.free_instance_func = free_instance;
register_class(library, &name, &parent, &info);
name_new(&constant, "ProductVersion");
register_constant(library, &name, &enum_name, &constant, 10000, 0);
name_destroy(&constant);
name_new(&constant, "EntityPathVariant");
register_constant(library, &name, &enum_name, &constant, 22, 0);
name_destroy(&constant);
name_destroy(&enum_name);
name_destroy(&parent);
name_destroy(&name);
}
static void deinitialize(void *userdata, ReckfellExtensionInitializationLevel level) {
(void)userdata;
if (level != RECKFELLEXTENSION_INITIALIZATION_SCENE) return;
NativeStringName name;
name_new(&name, "DocsCProbe");
unregister_class(library, &name);
name_destroy(&name);
}
__declspec(dllexport) ReckfellExtensionBool reckfell_docs_c_init(
ReckfellExtensionInterfaceGetProcAddress get_proc,
ReckfellExtensionClassLibraryPtr native_library,
ReckfellExtensionInitialization *initialization) {
library = native_library;
name_new = (ReckfellExtensionInterfaceStringNameNewWithUtf8Chars)get_proc("string_name_new_with_utf8_chars");
register_class = (ReckfellExtensionInterfaceClassdbRegisterExtensionClass6)get_proc("classdb_register_extension_class6");
unregister_class = (ReckfellExtensionInterfaceClassdbUnregisterExtensionClass)get_proc("classdb_unregister_extension_class");
register_constant = (ReckfellExtensionInterfaceClassdbRegisterExtensionClassIntegerConstant)get_proc("classdb_register_extension_class_integer_constant");
ReckfellExtensionInterfaceVariantGetPtrDestructor get_destructor = (ReckfellExtensionInterfaceVariantGetPtrDestructor)get_proc("variant_get_ptr_destructor");
if (!name_new || !register_class || !unregister_class || !register_constant || !get_destructor) return 0;
name_destroy = get_destructor(RECKFELLEXTENSION_VARIANT_TYPE_STRING_NAME);
if (!name_destroy) return 0;
initialization->minimum_initialization_level = RECKFELLEXTENSION_INITIALIZATION_SCENE;
initialization->userdata = NULL;
initialization->initialize = initialize;
initialization->deinitialize = deinitialize;
return 1;
}Точка входа проверяет адреса всех нужных функций до использования. Callback выполняет регистрацию только на уровне Scene, а callback деинициализации снимает её. Каждый созданный временный StringName уничтожается соответствующим деструктором; это не C-строка и не указатель на произвольный UTF-8 буфер.
Info6 обнуляется перед заполнением. Класс абстрактный, поэтому create-instance callback здесь не нужен; free_instance не управляет игровыми экземплярами, так как они не создаются. Для конкретного инстанцируемого класса потребуется отдельная реализация создания, хранения и освобождения состояния. Не копируйте абстрактную заглушку как готовый игровой компонент.
native_c_probe.reckfellextension
[configuration]
entry_symbol = "reckfell_docs_c_init"
compatibility_minimum = "1.0.0"
reloadable = false
[libraries]
windows.x86_64 = "./native_c_probe.windows.x86_64.dll"Относительный путь DLL отсчитывается от этого файла. Символ должен точно совпадать с экспортом reckfell_docs_c_init из C-кода.
Соберите только расширение
Откройте x64 Native Tools Command Prompt от Visual Studio, перейдите в addons/c и замените путь SDK своим:
cl /nologo /TC /std:c17 /LD /W3 /WX /I"C:\Tools\Reckfell\ExtensionSDK\include" native_c_probe.c /Fe:native_c_probe.windows.x86_64.dllОжидается DLL с указанным именем. /TC выбирает компиляцию как C, а не C++; /LD создаёт библиотеку. Эта команда не собирает и не изменяет движок. Файл .c не требуется игре после сборки, но описание и DLL должны быть доступны.
NativeCChecks.cs: проверка из проекта
Создайте этот файл в C# проекте и прикрепите к Entity запускаемого уровня. Скрипт проверяет класс и константы, выгружает учебное расширение и завершает приложение; это диагностический уровень, не основной игровой цикл.
using Reckfell;
using System;
public partial class NativeCChecks : Entity
{
private static void Check(bool ok, string name)
{
if (!ok) throw new Exception(name);
ReckfellApi.Print("NATIVEC_CHECK " + name);
}
public override void _Ready()
{
int code = 0;
try
{
foreach (string build in new[] { "c" })
{
string path = "asset://addons/" + build + "/native_c_probe.reckfellextension";
var status = ReckfellExtensionManager.LoadExtension(path);
Check(status is ReckfellExtensionManager.LoadStatus.Ok or ReckfellExtensionManager.LoadStatus.AlreadyLoaded, build + ":load-or-already-loaded");
Check(ClassDB.ClassExists("DocsCProbe") && ClassDB.GetParentClass("DocsCProbe") == "Entity", build + ":registered-entity-derived-class");
Check(!ClassDB.CanInstantiate("DocsCProbe"), build + ":abstract-class-not-instantiable");
Check(ClassDB.ClassGetIntegerConstant("DocsCProbe", "ProductVersion") == 10000 && ClassDB.ClassGetIntegerConstant("DocsCProbe", "EntityPathVariant") == 22,
build + ":registered-constants-cross-abi");
Check(ReckfellExtensionManager.IsExtensionLoaded(path) && Array.IndexOf(ReckfellExtensionManager.GetLoadedExtensions(), path) >= 0, build + ":loaded-path-visible");
Check(ReckfellExtensionManager.LoadExtension(path) == ReckfellExtensionManager.LoadStatus.AlreadyLoaded, build + ":repeat-load-status");
Check(ReckfellExtensionManager.UnloadExtension(path) == ReckfellExtensionManager.LoadStatus.Ok, build + ":unload-ok");
Check(!ClassDB.ClassExists("DocsCProbe") && !ReckfellExtensionManager.IsExtensionLoaded(path), build + ":unload-removes-class-and-path");
}
}
catch (Exception e) { ReckfellApi.PushError(e.ToString()); code = 1; }
if (code == 0) ReckfellApi.Print("NATIVEC_PASS");
GetTree().Quit(code);
}
}Ожидается восемь строк NATIVEC_CHECK и затем NATIVEC_PASS. Константы должны равняться 10000 и 22. Первый статус может быть AlreadyLoaded, если редактор уже автоматически загрузил описание. После выгрузки класс исчезает из ClassDB; его нельзя создавать или использовать дальше.
Разбор ошибок
| Этап | Что проверить |
|---|---|
| Компилятор не находит заголовок | Путь /I к текущему SDK |
| Ошибка имени C-типа или поля Info6 | Заголовок соответствует коду и целевой сборке |
| LoadExtension вернул Failed | Журнал, путь описания, архитектура DLL и экспорт символа |
| Класс не зарегистрирован | Найдены все функции, callback Scene выполнен, имя класса совпадает |
| Константа отличается | Загружена нужная DLL, а не старая копия; имена и значения совпадают |
Проверено: компиляция опубликованного C17-файла, загрузка в движке, регистрация класса/констант, повторная загрузка и удаление регистрации. Не проверено этим примером: конкретные экземпляры, методы/свойства/сигналы, hot reload, экспорт и другие платформы. Для их добавления расширяйте пример по одному контракту и проверяйте вызов, владение и очистку.
Далее: описание загрузки, данные интерфейса, API менеджера.