How Virtual Methods Work in GDExt‑Nim: Overriding from Nim and GDScript

GDExt‑Nim maps Godot virtual methods to Nim procedures using the {.gdsync.} pragma and a compile-time macro that generates emitters capable of dispatching to either Nim implementations or GDScript overrides.

GDExt‑Nim bridges Godot’s GDExtension API with Nim, allowing developers to define engine-callable virtual methods—such as _ready or _process—directly in systems language code. This article explains the three-tier registration mechanism that connects Nim method declarations to Godot’s virtual method table, and demonstrates how to override these methods from both Nim subclasses and attached GDScript files.

The Three Components of Virtual Method Dispatch

GDExt‑Nim implements virtual method support through tightly-coupled compile-time and runtime systems.

Method Declarations with {.gdsync.}

You declare a virtual method by defining a Nim method with the {.gdsync.} pragma. Adding the {.base.} pragma marks the declaration as the root implementation in your class hierarchy.

type VirtualNode01* {.gdsync.} = ptr object of Node

method virtualMethod*(self: VirtualNode01; str: string): string {.gdsync, base.} =
  "virtualMethod of VirtualNode01 is called " & str

Source: testproject/runtime/nim/src/classes/gdvirtualnode01.nim lines 4–8.

The sync_virtualDef Macro

When the compiler encounters a virtual method declaration, the sync_virtualDef macro—located in src/gdext/private/userclass/virtuals.nim (lines 70–71)—rewrites the definition at compile time. This macro parses the signature into a MiddleExp, generates an emitter that forwards calls to GDScript if a script override exists, and emits a registration call to ClassDB.registerExtensionClassVirtualMethod.

Godot’s Virtual Method Table

At runtime, Godot queries a class-specific table of function pointers stored in the GodotClassMeta struct. The bridge code in src/gdext/private/internalbridge.nim (lines 104–119) implements call_virtual_with_data_func, which retrieves the stored pointer by method name and dispatches the call to the Nim-generated thunk.

Registration Flow from Nim to Godot

The path from source code to engine registration follows four distinct steps:

  1. Declaration. You write a Nim method with {.gdsync, base.}.
  2. Macro Expansion. sync_virtualDef processes the AST, builds the emitter, and constructs the virtualMethodInfo metadata.
  3. ClassDB Registration. The macro emits a call to ClassDB.registerExtensionClassVirtualMethod, inserting the method into Godot’s extension class virtual table.
  4. Runtime Resolution. When Godot calls the virtual method, it invokes the pointer stored in GodotClassMeta.virtualMethods, which targets call_virtual_with_data_func in internalbridge.nim. This function casts the userdata to ClassCallVirtual and executes the generated emitter.

Overriding Virtual Methods from Nim

To override a virtual method in a Nim subclass, inherit the base type and redeclare the method without the {.base.} pragma. The macro treats this as an override and updates the virtual method table entry for the new class.

type VirtualNode02* {.gdsync.} = ptr object of VirtualNode01

method virtualMethod*(self: VirtualNode02; str: string): string {.gdsync.} =
  "virtualMethod of VirtualNode02 is called " & str

Source: testproject/runtime/nim/src/classes/gdvirtualnode02.nim lines 5–9.

Because the registration step replaces the virtual-method table entry for VirtualNode02 with the new Nim thunk, any engine call dispatches to this implementation unless a GDScript override is present.

Overriding Virtual Methods from GDScript

GDScript can override Nim-defined virtual methods by implementing a function whose name matches Godot’s internal snake_case convention (typically prefixed with an underscore). The emitter generated by sync_virtualDef checks self.hasScriptMethod(namesym) before executing the Nim body.

extends VirtualNode01

func _virtual_method(str: String) -> String:
    return "virtualMethod of InheritedNode01 is called " + str

Source: testproject/runtime/inherited_node01.gd lines 1–5.

If the attached script defines _virtual_method, the emitter’s callScriptMethod logic redirects execution to GDScript. If no script override exists, the call falls back to the Nim implementation defined in the base class or subclass.

Runtime Dispatch Architecture

The full call path demonstrates how GDExt‑Nim maintains compatibility with Godot’s scripting system:

  1. The engine requests a call to VirtualNode02._virtual_method.
  2. GodotClassMeta looks up virtualMethods["virtual_method"] and finds the pointer to call_virtual_with_data_func.
  3. The bridge function casts the stored userdata to ClassCallVirtual and invokes it.
  4. The generated thunk (created by sync_virtualDef) executes the emitter logic:
    • If hasScriptMethod returns true, the emitter calls the GDScript override.
    • Otherwise, it executes the Nim method body.

This architecture allows virtual methods to behave as first-class citizens, supporting pure Nim implementations, pure GDScript overrides, or mixed usage where GDScript calls super via self.callScriptMethod.

Configuring Virtual Method Naming

By default, GDExt‑Nim uses the identifier as-is when registering virtual methods. To automatically convert Nim camelCase names to Godot’s internal _snake_case format, configure the formatter in src/gdext/nameformats.nim (lines 176–186):

import gdext/nameformats

proc set_formatters {.execon: EntryPoint.} =
  nameformats.defaultVirtualMethodFormatter = nameformats.toGodotInternalFuncCase

Setting defaultVirtualMethodFormatter to toGodotInternalFuncCase prepends an underscore and snake-cases the identifier, ensuring that a Nim method named virtualMethod registers as _virtual_method in Godot’s virtual table.

Summary

  • Declare base virtual methods in Nim using the {.gdsync, base.} pragma pair.
  • Override in Nim subclasses by redeclaring the method with {.gdsync.} but omitting {.base.}.
  • Override in GDScript by defining a function with the internal snake_case name (e.g., _virtual_method); the generated emitter automatically detects and dispatches to the script.
  • The sync_virtualDef macro in src/gdext/private/userclass/virtuals.nim handles compile-time registration, while src/gdext/private/internalbridge.nim manages runtime dispatch via function pointers stored in GodotClassMeta.
  • Use defaultVirtualMethodFormatter in src/gdext/nameformats.nim to automate naming convention conversion.

Frequently Asked Questions

What pragmas are required to expose a Nim method as a Godot virtual method?

You must annotate the method with {.gdsync.}. If the class is the base definition of the virtual method, add the {.base.} pragma as well. The sync_virtualDef macro detects these pragmas and generates the necessary registration code and runtime emitters.

Can a GDScript attached to a Nim node completely replace the Nim implementation?

Yes. The emitter generated for every virtual method checks self.hasScriptMethod before executing the Nim body. If the attached GDScript defines the corresponding snake_case method (e.g., _virtual_method), the call redirects to the script. If no script override exists, execution falls back to the Nim implementation.

How does GDExt‑Nim resolve the naming mismatch between Nim camelCase and Godot’s underscore-prefixed snake_case?

The library provides the defaultVirtualMethodFormatter variable in src/gdext/nameformats.nim. By default it uses asIs, but you can assign toGodotInternalFuncCase (typically during EntryPoint execution) to automatically prepend underscores and convert camelCase identifiers to snake_case when registering with Godot’s virtual method table.

Where is the virtual method table stored at runtime?

Godot stores the virtual method pointers in the GodotClassMeta struct associated with each registered class. The bridge code in src/gdext/private/internalbridge.nim (lines 104–119) implements call_virtual_with_data_func, which retrieves the correct pointer by name from this metadata and dispatches the call to the Nim-generated thunk.

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 →