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 count
  • reference() – Increments the engine-side counter
  • unreference() – Decrements the engine-side counter
  • getReferenceCount() – Returns the current count as int32

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 engine
  • interfaceObjectFreeInstanceBinding – 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 RefCounted system performs all reference counting; gdext never duplicates this logic.
  • Thin wrappers: Nim objects store only an ObjectPtr and a binding, minimizing overhead.
  • Single cleanup: The free_instance_func callback in internalbridge.nim ensures Nim wrappers are destroyed exactly once when the engine frees the object.
  • Debug visibility: Optional logging via debugging.nim tracks 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →