Idiomatic Zig bindings for Godot 4.
This library is currently undergoing rapid development and refactoring as we figure out the best API to expose. Bugs and missing features are expected until a stable version is released. Issue reports, feature requests, and pull requests are all very welcome.
- zig 0.17.0
- godot 4.7
Note: gdzig targets Godot 4.7 only. Earlier versions are not supported; pin an older gdzig if you need one.
Note: We are targeting stable releases of Zig only. 0.16.x is no longer supported; use a tag prior to the 0.17 migration if you need it.
See the example folder for reference.
zig buildThat downloads a matching Godot automatically. To build against a specific version or an engine you already have:
zig build -Dgodot-version=4.7
zig build -Dgodot-path=/absolute/path/to/godot # must be absoluteIn a project of your own, zig build watch rebuilds on every change and restarts Godot,
clearing the stale ~-prefixed libraries that otherwise block the next load.
zig build test runs the unit and Godot tests; zig build test-all adds the
integration gates that cover the build tooling itself, which is what CI runs.
zig build test-docs compiles the complete Zig examples marked in the guides,
so API changes cannot leave copy-paste examples silently stale.
The default install also puts gdzig in zig-out/bin
B04C
; zig build cli installs
just this command. Add that directory to PATH or use the executable's full path. From a project's
build root, use it to verify the local toolchain, generated descriptor, native
library, and Godot import state before opening the editor:
gdzig doctor # project.godot is beside build.zig
gdzig doctor --project project # split Zig/Godot layout
gdzig doctor --godot /path/to/godotdoctor is read-only and exits nonzero for broken wiring. If Godot is not in
PATH, that is only a warning because gdzig can download the selected engine
during zig build; pass --godot when you want a particular executable checked.
It checks root-level generated descriptors for the current host's debug selector.
Build-source checks are advisory; doctor does not load libraries or verify exports.
For an optional Godot bottom panel with Build, Doctor, and class generation, run
gdzig setup-editor (or gdzig setup-editor --project project) and enable the
addon in Project Settings > Plugins. See editor integration.
For a complete playable introduction, follow Star Catcher:
input, typed scenes, signals, resource ownership, a coroutine countdown, and
desktop export. zig build test-tutorial builds and play-tests its source.
Fastest route: zig build cli in a gdzig checkout installs gdzig; then
gdzig init --name mygame --out mygame scaffolds a working project anywhere.
doc/quickstart.md is that path end to end.
From the generated project's directory:
gdzig build
gdzig run -Dgodot-version=4.7
gdzig editor
gdzig watch -Doptimize=safe
gdzig export -- --preset "Windows Desktop" --output ../exports/v1/Game.exeBuild options are forwarded to zig build. run and editor build and import
before launching the same Godot selected by the dependency. Pass Godot arguments
after --, for example gdzig run -- --headless --quit.
watch forwards build options to each rebuild. Existing projects need the
run, editor, and watch steps from the current scaffold in their build file.
The former standalone scaffolder and its build step have been removed; use gdzig init.
Desktop exports require a matching Godot preset and export templates. The command checks the platform/architecture, imports, and exports into an empty directory outside the project. See exporting for debug mode, limitations, and the build helper for existing projects.
Generate registered classes from the project directory:
gdzig add node Player --base CharacterBody3D
gdzig add resource Inventory
gdzig add plugin ToolsThe command creates the Zig file and adds it to the scaffold's shared classes
tuple. Nodes get _ready and _physics_process; plugins get editor registration
and enter/exit hooks. Existing files are refused. Use --entry src/mygame.zig
if entry detection is ambiguous. Custom registration layouts require manual setup;
gdzig add --help lists supported bases. With selective bindings, include the
chosen base class in your build's class selection.
To build one by hand instead, doc/getting-started.md walks this same setup and finishes with a character you can drive with WASD. What follows is the condensed reference for the files involved.
The extension side is three hand-written files and a generated descriptor. The example is the working reference for all of it.
Your Zig code and a normal Godot project sit side by side, and the build installs the library into the Godot project:
mygame/
build.zig
build.zig.zon
src/main.zig root module: must export `register`
project/project.godot an ordinary Godot project
project/mygame.gdextension generated by `zig build`
project/lib/ the build installs here
Depend on gdzig.
zig fetch --save git+https://github.com/moohaad/gdzigbuild.zig. Three calls: take the dependency, make a module that imports it as godot,
and hand that module to addExtension.
const gdzig_dep = b.dependency("gdzig", .{
.target = target,
.optimize = optimize,
.@"godot-version" = b.option([]const u8, "godot-version", "Godot version to download"),
.@"godot-path" = b.option([]const u8, "godot-path", "Path to a Godot executable"),
// Optional: omit for the full API. Dependencies and ancestors are added.
.classes = b.option([]const u8, "godot-classes", "Godot classes to generate"),
// Absolute because a dependency resolves paths from its own build root.
.godot_project = b.root.joinString(b.allocator, "project") catch @panic("OOM"),
});
const mod = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.imports = &.{.{ .name = "godot", .module = gdzig_dep.module("gdzig") }},
});
const extension = gdzig.addExtension(b, .{
.name = "mygame",
.root_module = mod,
.entry_symbol = "mygame_init",
.target = target,
.optimize = optimize,
// Names every `.tscn` under the project as an import, so
// `Scene(@embedFile("Player.tscn"))` can reach one. Omit if unused.
.godot_project = "project",
}) orelse return;
const install_library = b.addInstallFileWithDir(
extension.output,
.{ .custom = "../project/lib" },
extension.filename,
);
const install_manifest = b.addInstallFileWithDir(
extension.manifest,
.{ .custom = "../project" },
extension.manifest_filename,
);
b.default_step.dependOn(&install_library.step);
b.default_step.dependOn(&install_manifest.step);Passing godot_project to the dependency reads the current Input Map into
godot.input.Action, catalogs .tscn files for cross-scene typing, and enables
compile-time res:// checks. For example,
godot.input.Action.jump exists only while jump is configured in
project.godot; renamed and misspelled actions fail during compilation. The separate
relative godot_project on addExtension makes scenes importable to @embedFile and
runs project preflight checks.
For smaller projects, pass a comma-separated class list to the dependency:
zig build -Dgodot-classes=CharacterBody3D,Camera3D,AnimationPlayerBoth Godot names (GPUParticles2D) and generated gdzig names
(GpuParticles2d) are accepted. Bindgen automatically includes inheritance and
every class referenced by the selected APIs, while builtins, global enums,
utility functions, and native ABI structures remain available. An omitted
class is absent from godot.class, so accidentally using it is a compile error.
Leave the option unset for the complete Godot class surface.
godot.Scene(@embedFile("levels/Main.tscn")) follows inherited scene bases and
uses an instanced scene's root type for its generated Child field. For roots
implemented by your extension rather than Godot, use SceneWith and provide a
namespace such as struct { pub const Player = PlayerNode; }.
project/mygame.gdextension. Generated by addExtension. Its entry symbol comes
from .entry_symbol, and its library selector comes from the resolved target and the
actual output filename, so those values cannot drift apart. Install extension.manifest
beside project.godot, as above. If the library goes somewhere other than lib/, set
.manifest = .{ .library_dir = "path" } on addExtension and install it there.
compatibility_minimum is the engine gdzig itself requires. Claiming an older one does not
buy compatibility: Godot loads the extension and gdzig then refuses it, which is a worse
error than Godot declining in the first place.
The manifest defaults to reloadable = true, which opts into hot reload. Set
.manifest = .{ .reloadable = false } to disable it. Exported properties and the instances
themselves survive; a class whose base changed does not, and says so rather than
crashing. example/ has a harness -- zig build reload-test -- that reloads the
extension and checks what came through.
src/main.zig. gdzig provides the entry point itself; your root module only registers.
register is required, unregister is optional -- the registry unregisters anything left
behind either way.
pub fn register(r: *Registry) void {
r.addClass(PlayerNode, r.allocator, .auto);
}
const godot = @import("godot");
const Registry = godot.extension.Registry;
const PlayerNode = @import("PlayerNode.zig");Then zig build, and open project/ in Godot.
Before writing much, skim doc/what-gdzig-gives-you.md. It is short, and it exists because a real port hand-wrote five things that were already in the box.
Engine calls are safe from any thread; the scene tree is still main-thread only.
See doc/threading.md for the contract and the
callDeferred pattern.
Find us in the gdzig Discord server.