How to Implement and Emit Custom Signals in GDExt-Nim Extension Classes
To implement custom signals in GDExt-Nim, declare a proc with your class as the first parameter, return type Error, and the .{.gdsync, signal.} pragmas, then call that proc directly to emit the signal to Godot.
GDExt-Nim bridges Nim and Godot 4.x by allowing you to define Godot-compatible custom signals directly in Nim-written extension classes. This guide walks through the exact mechanics used in the godot-nim/gdext-nim repository to register, emit, and connect signals using compile-time macros and the Godot ClassDB API.
Signal Declaration Requirements
Signal procs in GDExt-Nim must follow a strict signature to satisfy the syncSignal macro validation found in src/gdext/private/userclass/signals.nim.
- First parameter: Must be the user-class type (the
selfobject). - Return type: Must be
Error(lines 60–66 of signals.nim enforce this at compile time). - Pragmas: Add
.{.gdsync, signal.}for synchronous signals, or.{.gdasync, signal.}for asynchronous variants.
proc signal_arg0*(self: GDExtNode): Error {.gdsync, signal.}
proc signal_arg1*(self: GDExtNode; what: string): Error {.gdsync, signal.}
The signal pragma triggers the syncSignal macro, which transforms this declaration into a registered Godot signal and generates the emit implementation.
How the Signal Registration Works
The syncSignal Macro Expansion
When the Nim compiler encounters the signal pragma, the syncSignal macro (defined at the bottom of signals.nim) invokes either syncSignalGlobal or syncSignalLocal. Both paths call contractSignal, which registers the signal with the engine:
ClassDB.registerExtensionClassSignal(
className(`arg0_T`), &`gdname`, parseParams(`params`))
This call (line 52 of signals.nim) adds your signal to Godot's ClassDB, making it visible to the editor and other GDScript or C++ code.
The emitSignal Wrapper
The macro also generates a procedure body (makebody) that forwards to Object.emitSignal. The generated code resembles:
var signalName {.global.}: StringName
once:
signalName = newStringName `gdname`
self.emitSignal(signalName, variantArrDef)
The emitSignal method is defined in src/gdext/classes/gdobject.nim (lines 34–40) as a thin wrapper around Godot's C API emit_signal function.
Emitting Custom Signals
Once registered, you can emit signals using two equivalent syntax styles. The direct call is type-checked at compile time, while the generic string syntax allows dynamic names.
Direct call (preferred):
discard self.signal_arg0() # No arguments
discard self.signal_arg1("Hello") # With arguments
Generic string-based syntax:
self.signal"signal_arg_0"()
self.signal"signal_arg_1"("Hello")
Both approaches invoke the macro-generated body that calls self.emitSignal, forwarding the arguments to the Godot runtime.
Connecting to Signals
Connect signals using the standard Godot connect API. The test file testproject/runtime/nim/src/classes/gdextnode.nim (lines 31–35) demonstrates this pattern:
check self.connect("signal_arg_0", self.callable"listen_0") == ok
check self.connect("signal_arg_1", self.callable"listen_1") == ok
The callable syntax creates a first-class function reference to your handler method, compatible with Godot's Callable system.
Complete Implementation Example
The following self-contained snippet demonstrates defining, connecting, and emitting custom signals in a complete GDExt-Nim class:
import gdext
# ------------------------------------------------------------
# 1️⃣ Define a class that inherits from a Godot type (e.g. Node)
# ------------------------------------------------------------
type MyNode* = ptr object of Node
initialized: bool
# ------------------------------------------------------------
# 2️⃣ Register custom signals
# ------------------------------------------------------------
proc ping*(self: MyNode): Error {.gdsync, signal.}
proc pong*(self: MyNode; payload: string): Error {.gdsync, signal.}
# ------------------------------------------------------------
# 3️⃣ Define handler methods that will react to the signals
# ------------------------------------------------------------
proc onPing(self: MyNode) {.gdsync.} =
echo "Ping received!"
proc onPong(self: MyNode; payload: string) {.gdsync.} =
echo "Pong received with payload: ", payload
# ------------------------------------------------------------
# 4️⃣ Hook to set everything up (e.g. in _ready)
# ------------------------------------------------------------
method ready(self: MyNode) {.gdsync.} =
# Connect signals to the handlers
discard self.connect("ping", self.callable"onPing")
discard self.connect("pong", self.callable"onPong")
# Emit the signals
discard self.ping() # direct call
discard self.pong("hello world") # direct call with arg
self.signal"ping"() # generic syntax
self.signal"pong"("nim rocks!") # generic syntax
Under the hood, ping and pong are transformed by syncSignal → contractSignal → ClassDB.registerExtensionClassSignal. Their bodies become calls to Object.emitSignal, while connect registers the listener with the Godot engine.
Summary
- Declare signal procs with
selfas the first argument and return typeError, marked with.{.gdsync, signal.}. - The
syncSignalmacro insrc/gdext/private/userclass/signals.nimautomatically registers the signal viaClassDB.registerExtensionClassSignaland generates the emit logic. - Call the signal proc directly (e.g.,
self.signalName()) or use the generic syntaxself.signal"signalName"()to emit. - Connect listeners using
self.connect("signalName", self.callable"handler")as implemented in the Godot API wrapper.
Frequently Asked Questions
What is the required return type for signal procs in GDExt-Nim?
Signal procs must return Error. The syncSignal macro enforces this constraint at compile time (lines 60–66 of src/gdext/private/userclass/signals.nim) to ensure compatibility with the Godot extension protocol.
Can I use asynchronous signals in GDExt-Nim?
Yes. Replace the .{.gdsync, signal.} pragma with .{.gdasync, signal.}. The macro system supports both synchronous and asynchronous variants through the syncSignal and gdasync dispatch paths.
Where is the signal registration handled in the source code?
Registration occurs in src/gdext/private/userclass/signals.nim via the contractSignal helper, which calls ClassDB.registerExtensionClassSignal. The actual emission wrapper lives in src/gdext/classes/gdobject.nim at lines 34–40.
How do I pass arguments when emitting signals dynamically?
Use the generic string-based syntax: self.signal"signal_name"(arg1, arg2). The macro-generated body automatically packs these arguments into a Variant array and forwards them to Object.emitSignal, matching the signature defined in your proc declaration.
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 →