Understanding Static Events in gdext-nim: Compile-Time Initialization Callbacks
Static events in gdext-nim are a compile-time mechanism that uses macros to register and execute initialization callbacks before the Godot engine starts your extension, ensuring zero-overhead registration through the execon and expandEvent macros.
Static events are the foundation of initialization in the godot-nim/gdext-nim bindings. These compile-time callbacks allow you to hook into Godot's extension lifecycle—such as initialize_core and initialize_scene—without writing manual registration boilerplate or incurring runtime overhead.
What Are Static Events in gdext-nim?
Static events are compile-time sequences defined as Event = CacheSeq in src/gdext/private/staticevents.nim. They store callback identifiers during compilation and vanish at runtime, leaving behind only the generated initialization code.
The Core Data Structure
In src/gdext/private/staticevents.nim at lines 7–8, the event helper creates these compile-time containers:
proc event*(name: string): Event = Event name
This constructor turns a string identifier into a static event handle. Because Event is a CacheSeq, it accumulates callbacks during the compilation phase but resolves to a flat list of procedure calls in the final binary.
The Macro Pipeline: expandEvent and execon
Two macros form the engine of this system according to src/gdext/private/staticevents.nim:
expandEvent (lines 14–23) takes a static Event and a code block, then expands that block into a series of calls—one for each action recorded in the event. It guards against double expansion to ensure idempotent initialization.
execon (lines 41–53) generates a uniquely-named procedure, registers it with the given static event, and optionally emits debug information. The body you provide becomes the callback that runs when the event expands.
How Static Events Execute During Extension Loading
Static events bridge the gap between Nim's macro system and Godot's extension lifecycle. The binding uses the GDExtension_EntryPoint template defined in src/gdext.nim (lines 88–106) to orchestrate this.
Compile-Time Expansion
When you compile your extension, the entry point binds expandEvent and declares execution procedures for each built-in lifecycle stage:
template GDExtension_EntryPoint*: untyped =
bind expandEvent
proc exec_initialize_core {.expandEvent: initialize_core.}
# ... additional initialization levels
Lines 94–101 in src/gdext.nim expand built-in events including initialize_core and initialize_scene. During compilation, the expandEvent macro replaces these placeholders with actual calls to every procedure registered via execon.
Runtime Execution Flow
At runtime, Godot invokes the generated exec_initialize_* procedures when the corresponding initialization level is reached. For example, exec_initialize_core() is called from the initializer when p_level == Initialization_Core. These procedures, in turn, invoke your attached callbacks in the order they were registered.
How to Use Static Events for Initialization Callbacks
You interact with static events through the execon macro for simple callbacks or by defining contracts for complex class registration.
Attaching Callbacks to Built-in Events
Use the execon macro to attach procedures directly to gdext-nim's built-in lifecycle events. In src/gdext/private/staticevents.nim, the execon macro (lines 41–53) handles the registration automatically.
# src/my_extension.nim
import gdext
import gdext/private/staticevents
proc myCoreSetup() {.execon: staticevents.initialize_core.} =
## Runs automatically during Core initialization
echo "Extension registered at core init"
When GDExtension_EntryPoint expands initialize_core, it generates a call to myCoreSetup() without any manual registration code.
Custom Static Events for Class Contracts
For user-defined classes, the invoke template (lines 62–78 in src/gdext/private/staticevents.nim) automates registration of enums, virtual methods, and properties. This pattern is implemented in src/gdext/private/internalbridge.nim and you can replicate it for your own contracts:
# src/my_user_class.nim
import gdext/private/staticevents
type MyContract* = Contract[void]
template myEnums = event $MyContract.T & "::contract::enums"
template myVirtual = event $MyContract.T & "::contract::virtual"
template invoke*(contract: typedesc[MyContract]) =
proc register_enums {.expandEvent: contract.myEnums.}
proc register_virtual {.expandEvent: contract.myVirtual.}
register_enums()
register_virtual()
static: invoked.incl $contract.T
proc registerMyEnums() {.execon: MyContract.myEnums.} =
echo "Registering enums"
proc registerMyVirtual() {.execon: MyContract.myVirtual.} =
echo "Registering virtual methods"
# Trigger the contract
invoke MyContract
This ensures that registerMyEnums and registerMyVirtual execute exactly once when the contract is invoked, with the compiler enforcing single expansion through the invoked static set.
Summary
- Static events are compile-time
CacheSeqcontainers defined insrc/gdext/private/staticevents.nimthat store initialization callbacks. - The
execonmacro attaches your procedures to specific lifecycle points without runtime overhead. expandEventgenerates the actual call sequence during compilation, guarding against double expansion.- Built-in events like
initialize_coreandinitialize_sceneare expanded insrc/gdext.nimvia theGDExtension_EntryPointtemplate. - For complex classes, the
invoketemplate automates registration of multiple callbacks through custom contracts, as seen insrc/gdext/private/internalbridge.nim. - All static event processing happens at compile time; runtime sees only flat procedure calls.
Frequently Asked Questions
What is the difference between static events and runtime callbacks?
Static events are resolved entirely during compilation in src/gdext/private/staticevents.nim using Nim's macro system. They generate direct procedure calls in the binary, eliminating the need for runtime registration tables or dynamic dispatch. Runtime callbacks would require manual registration and storage in global lists that persist during execution.
When should I create a custom static event instead of using a built-in one?
Create custom static events when you need class-specific initialization stages that don't map to Godot's core lifecycle. According to the implementation in src/gdext/private/internalbridge.nim, custom contracts allow you to group related callbacks—such as enum registration and virtual method binding—into logical units that fire together when you invoke the contract template.
How does the system prevent duplicate initialization?
The expandEvent macro in src/gdext/private/staticevents.nim (lines 14–23) contains guards that prevent double expansion of the same event. Additionally, the invoke template tracks processed contracts in a static invoked set (line 78), ensuring that register_* procedures execute exactly once per compilation unit even if the contract is referenced multiple times.
Can I use static events outside of GDExtension_EntryPoint?
While static events are designed for the extension initialization sequence defined in src/gdext.nim, you can use expandEvent and execon in any compile-time context. However, the callbacks will only execute if the event is explicitly expanded via expandEvent or triggered through a contract's invoke template, as the macros rely on static code generation rather than runtime discovery.
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 →