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 self object).
  • 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 self as the first argument and return type Error, marked with .{.gdsync, signal.}.
  • The syncSignal macro in src/gdext/private/userclass/signals.nim automatically registers the signal via ClassDB.registerExtensionClassSignal and generates the emit logic.
  • Call the signal proc directly (e.g., self.signalName()) or use the generic syntax self.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:

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 →