Zig¶
Pier's Zig binding writes a mod as a Zig dynamic library, for Zig 0.15.2. The package is
pier, in bindings/zig, and the module it exports is levilamina, so code writes
@import("levilamina"); the smallest working mod is examples/hello-pier-zig.
The package name names Pier, the ABI it belongs to, and the module name what you are
writing, a LeviLamina mod, as the Rust binding's package pier-rs exposes the crate
levilamina.
How it is put together¶
translate-c reads abi.h itself. The build runs translate-c over the header, so the
types, slots and constants are the C compiler's own reading of it; nothing is mirrored by
hand. The package carries a copy of the header, because a dependency sees only its own
directory, and a check keeps the copy identical to the original.
Zig calls the slots directly. levilamina.slot("name") is the function pointer of a slot,
or null when the host's table is too short to hold it or the slot is NULL: both gates of
the contract in one call, for every slot of the table. Everything else is built on it.
zig build test checks the binding against the header. Its test makes the compiler
analyze every function of the binding against the table translate-c produced, so a slot
renamed in abi.h, or an argument of the wrong type, fails the build; the CI runs it.
What you need¶
- Zig 0.15.2.
- Nothing else: Zig cross-compiles the Windows DLL from any system, and the example's
default target,
x86_64-windows-gnu, needs no C runtime beyond the system's.
The API¶
| Area | Functions |
|---|---|
| Lifecycle | exportMod, and load, enable, disable, unload on the mod type |
| Logging | log, logf, Context.info, warn, err |
| Tasks | schedule, scheduleAfter, cancel, pendingTasks |
| Events | subscribe, Event.cancel, Event.writeBack, Listener.unsubscribe |
| Commands | registerCommand, executeCommand |
| Cross-mod | registerService, callService, serviceCaller, listServicesJson, busSubscribe, busPublish, busPublishVetoable |
| Properties and verbs | one method per constant on Player, Entity, BlockAt and Item, such as Player.level, Player.setLevel, Entity.isOnFire |
| Data | nbt.parse, with get, getString, getInt, getFloat, getBool |
| Everything else | slot("name"), with the types and constants of levilamina.c |
Errors are levilamina.Error: NotProvided for a slot the host does not have, Refused for a
call the host refused, and NoAnswer for a read it could not answer: an unreadable value
comes back as that error, and no zero or false stands in for it. A function that returns text takes an allocator and gives the caller the
memory.
Talking to other mods¶
A Zig mod talks to mods of every language, and to native plugins through the bridge, under the same rules as the Rust and Go bindings:
- A service provider sends its reply and returns true, or sends the reason and returns
false; the caller receives that reason unchanged, as
callService'sprovider_errorand its body.serviceCallernames the calling mod, and is null for a native plugin. - A bus subscriber returns its veto: true refuses a vetoable publish. A plain publish
ignores it, and
busPublishreturning 0 means nobody listens, which is no error. - Lanes are not offered: they pair mods built by one toolchain, and a mod that publishes one also provides the service a Zig mod uses.
Rules worth knowing¶
- Threads. Most of the host is server-thread only. Lifecycle steps, event and command callbacks run there; a service provider runs on its caller's thread and a bus subscriber on the publisher's.
- Borrowed views. An
Event'ssnbt, anInvocation'sargsand a provider'srequestare valid during the callback only; copy what you keep. - Panics. Zig does not unwind: a panic, such as a failed safety check in a ReleaseSafe build, ends the server process. Return errors instead; the binding logs an error from a lifecycle step and refuses the step.
Your first Zig mod walks through building and installing one.