How gdext Handles Memory Management and Reference Counting for Godot Objects
gdext delegates all reference counting to Godot's engine while maintaining a thin Nim wrapper that binds to engine objects via object_set_instance_binding and cleans up exactly once when the engine frees the object.
gdext-nim is a Nim language binding for the Godot 4 game engine that mirrors Godot's native memory model without introducing additional overhead. Understanding how gdext handles memory management and reference counting for Godot objects is essential for preventing leaks and ensuring deterministic object lifetimes in your Nim game scripts. The implementation stores a raw ObjectPtr in a thin wrapper and coordinates with the engine through method binds and lifecycle callbacks.
Engine-Side Reference Counting via RefCounted
Godot's RefCounted class manages object lifetimes through an internal counter that the engine increments and decrements. In src/gdext/classes/gdrefcounted.nim, gdext exposes four key methods that forward directly to the engine via method binds:
initRef()– Creates a fresh reference countreference()– Increments the engine-side counterunreference()– Decrements the engine-side countergetReferenceCount()– Returns the current count asint32
The actual method bind pointers are retrieved in src/gdext/private/native.nim. For example, RefCounted_get_reference_count is loaded using interface_ClassDB_getMethodBind and cached for performance. When your Nim code calls reference() on a wrapper, gdext invokes the engine's implementation immediately without caching the count locally.
import gdext/classes/gdRefCounted
let tex = ImageTexture.new()
assert tex.getReferenceCount() == 1
tex.reference()
assert tex.getReferenceCount() == 2
Nim-to-Engine Instance Binding
Every Godot object accessible from Nim requires an instance binding that allows the engine to call back into Nim code for virtual methods. This binding is established through two core GDNative functions loaded in src/gdext/gen/gdextensioninterfaceapi.nim:
interfaceObjectSetInstanceBinding– Registers the Nim wrapper with the engineinterfaceObjectFreeInstanceBinding– Removes the binding when the object dies
During instantiation in src/gdext/private/internalbridge.nim, the instantiate_internal template calls objectPtr.setInstanceBinding(result, addr T.callbacks). This stores the address of the Nim instance alongside a pointer to the class-specific callback table, enabling the engine to invoke Nim virtual methods while the object remains allocated.
User-Class Lifecycle Management
For script-extension classes (user-defined Nim classes inheriting from Godot objects), gdext registers a set of callbacks in the ClassCreationInfo structure defined in src/gdext/private/internalbridge.nim:
| Callback | Purpose |
|---|---|
create_instance_func |
Allocates the Nim wrapper, binds it, and returns the engine pointer |
free_instance_func |
Destroys the wrapper when the engine's reference count reaches zero |
reference_func / unreference_func |
Optional debug hooks that log reference count changes |
recreate_instance_func |
Handles hot-reloading by re-creating the Nim instance |
When the engine determines an object should be freed, it triggers free_instance_func. This function casts the pointer back to the Nim type, runs user-defined cleanup via onDestroy, executes Nim's destructor =destroy, and finally calls dealloc to release the wrapper. This guarantees the Nim wrapper is destroyed exactly once, regardless of how many Nim references exist.
proc free_instance_func[T: SomeUserClass](
p_userdata: pointer; p_instance: pointer) {.gdcall.} =
let class = cast[T](p_instance)
debugFree(class)
onDestroy class
`=destroy` class[]
dealloc class
Optional Debug Logging
When compiling with Dev.debugCallbacks, gdext enables detailed memory tracking in src/gdext/private/debugging.nim. The system logs instantiation, reference increments, and destruction events to a .callbacks.log file using helpers like debugInstantiate, debugReference, and debugFree.
The debug system queries current reference counts via hook_getReferenceCount, which calls the engine method bind directly:
proc hook_getReferenceCount(o: ObjectPtr): int32 {.raises: [].} =
var ret: Int
interface_Object_methodBindPtrCall(
RefCounted_get_reference_count, o, nil, addr ret)
return int32 ret
Complete Working Examples
Managing Built-in RefCounted Objects
import gdext/classes/gdRefCounted
var texture = ImageTexture.new()
echo "Initial count: ", texture.getReferenceCount() # 1
texture.reference()
echo "After manual ref: ", texture.getReferenceCount() # 2
texture.unreference()
# Count returns to 1; engine will free when count hits 0
Creating a Custom Node Class
import gdext/classes/gdNode
type
MyNode = ref object of Node
health: int
method _ready(self: MyNode) =
self.health = 100
echo "MyNode initialized"
gdexport(MyNode)
# Usage:
let node = MyNode.new()
getTree().root.addChild(node)
# When the scene removes this node, free_instance_func cleans up the Nim wrapper
Debugging Reference Counts
import gdext/classes/gdRefCounted
when defined(debugCallbacks):
let refObj = SomeRefCounted.new()
echo "Count: ", refObj.getReferenceCount()
# Check .callbacks.log for detailed lifecycle traces
Summary
- Engine owns the memory: Godot's
RefCountedsystem performs all reference counting; gdext never duplicates this logic. - Thin wrappers: Nim objects store only an
ObjectPtrand a binding, minimizing overhead. - Single cleanup: The
free_instance_funccallback ininternalbridge.nimensures Nim wrappers are destroyed exactly once when the engine frees the object. - Debug visibility: Optional logging via
debugging.nimtracks every instantiate, reference, and free event for leak detection.
Frequently Asked Questions
Does gdext use Nim's garbage collector to manage Godot objects?
No. Godot objects are managed exclusively by the engine's reference counting system. The Nim wrapper holds a raw pointer and is freed deterministically via the free_instance_func callback when the engine destroys the object, independent of Nim's garbage collector cycles.
How do I check the current reference count of a Godot object in gdext?
Call getReferenceCount() on any object inheriting from RefCounted. This method forwards to the engine via the method bind defined in src/gdext/classes/gdrefcounted.nim and retrieved in src/gdext/private/native.nim.
What prevents Nim from accessing a freed Godot object?
The engine guarantees that free_instance_func is called only when the reference count reaches zero and no engine references remain. Once this callback executes, the Nim wrapper is deallocated, making dangling pointer access impossible through normal gdext usage.
How can I track object creation and destruction in gdext?
Compile your project with Dev.debugCallbacks defined. This enables the logging system in src/gdext/private/debugging.nim, which writes every create_instance_func, reference_func, and free_instance_func event to a .callbacks.log file along with current reference counts retrieved via hook_getReferenceCount.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →