Your first native extension in C17
Complete Windows x64 example: compilation, registration and ABI checks.
This example loads a C17 library, registers an abstract Entity-derived DocsCProbe class and two constants, then checks them from C#. It does not implement sprite movement, instance properties or signals: the first result is a small, completely checkable ABI contract.
Requirements and layout
The checked target is Windows x64 with current ExtensionSDK 1.0.0, float_64 configuration and C17-capable MSVC. Runtime checks use a C# project with Reckfell SDK 1.0.0. The extension itself is C and needs no C++ wrapper.
Use this layout:
project/
project.reckfell
NativeCChecks.cs
addons/c/
native_c_probe.c
native_c_probe.reckfellextension
native_c_probe.windows.x86_64.dllObtain reckfellextension_interface.h from the target build’s ExtensionSDK/include. Use its paired extension_api.json for type sizes; StringName occupies 8 bytes here. Do not assume that size on another ABI without checking.
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;
}The entry point checks all required function addresses before use. Initialization registers only at Scene level, and deinitialization unregisters the class. Every temporary StringName is destroyed with its matching destructor; it is neither a C string nor a pointer to an arbitrary UTF-8 buffer.
Info6 is zero-initialized before filling. This class is abstract, so it needs no create-instance callback here; free_instance manages no game instances because none are created. A concrete instantiable class needs separate creation, state storage and cleanup implementations. Do not copy the abstract stub as a finished game component.
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"The DLL path is relative to this file. The symbol must exactly match the C export reckfell_docs_c_init.
Build only the extension
Open Visual Studio’s x64 Native Tools Command Prompt, change to addons/c and substitute your SDK path:
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.dllExpect a DLL with that name. /TC compiles as C rather than C++; /LD produces a library. This command neither builds nor modifies the engine. The game does not need the .c file after building, but the descriptor and DLL must remain available.
NativeCChecks.cs: project-side verification
Create this file in the C# project and attach it to an Entity in the startup level. It checks the class/constants, unloads the tutorial extension and quits the application; use a diagnostic level rather than the main game loop.
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);
}
}Expect eight NATIVEC_CHECK lines followed by NATIVEC_PASS. The constants must be 10000 and 22. First load may return AlreadyLoaded if the editor already loaded the descriptor automatically. After unloading, the class disappears from ClassDB and must no longer be instantiated or used.
Diagnose failures
| Stage | Check |
|---|---|
| Compiler cannot find the header | /I path to the current SDK |
| Unknown C type or Info6 field | Header matches the code and target build |
| LoadExtension returns Failed | Log, descriptor path, DLL architecture and exported symbol |
| Class is not registered | Required functions resolved, Scene callback ran, class name matches |
| A constant differs | Correct DLL rather than an old copy; matching names and values |
Checked: the published C17 file compiles and loads in the engine; class/constants register, repeated loading is detected and unloading removes registration. This example does not validate concrete instances, methods/properties/signals, hot reload, exports or other platforms. Add those one contract at a time and check calls, ownership and cleanup.
Continue with loading descriptors, interface data, manager API.