Introduction
Zig++ is to Zig what TypeScript is to JavaScript: a superset that adds the
features people kept asking for. Every valid Zig program is a valid Zig++
program, unless it names something priv (write @"priv" instead).
Like Zig, Zig++ is a general-purpose programming language and toolchain for maintaining robust, optimal, and reusable software. Unlike Zig, it has private fields, it will never drop LLVM, it welcomes AI, and its BDFL is Matthew Neel. Zig++ is a fork of Zig, and nearly all of the compiler was written by upstream Zig contributors.
The source is at github.com/mattneel/zigpp,
where this book’s sources live as doc/book/.
What Zig++ adds
- Private fields. A struct or union field marked
privcan only be named from the file that declares its type. See Private fields. - LLVM forever, and a blessed path to GPUs:
std.gpuruns Zig++ and its standard library on NVIDIA, AMD, and Apple GPUs. See LLVM is forever and GPU Programming. - AI in the toolchain. See AI Policy and Governance.
- Any Zig version, automatically. A project’s
build.zig.zoncan pin the exact compiler version it is built with, andzigruns that version instead of itself, downloading it on first use. See Any Zig Version. - A BDFL and one rule: talk about code. See Governance.
Questions people ask
Does Zig++ compile to Zig, the way TypeScript compiles to JavaScript? No. It compiles to machine code, C, WebAssembly, PTX, AMD GPU code objects, and Metal libraries.
Is Zig++ stable? Zig++ follows semantic versioning exactly as closely as
TypeScript does. The compiler is at 0.17.0-dev, every push to master is a
release, and Versions and Releases explains what the version
string means.
What version is Zig++? None. Every push to master is a release, and every
release is kept forever: you pin the build you use. A build is named for the
upstream version it tracks, its commit height, and its commit, as in
0.17.0-dev.2469+zigpp.04926fc36. See Live at Head.
When is 1.0? 1.0 is a milestone, not a version. The milestone closes when Zig++ gets its borrow checker, and the build that closes it ships like every other build. There is no Zig++ 1.0.
Can upstream Zig build Zig++? No. Zig++ changed std.lang.Type, and an
upstream Zig binary cannot compile against it. Use the CMake build,
bootstrap.c, or an existing Zig++ binary; see
Building from Source.
Which LLVM does Zig++ use? LLVM, Clang, and LLD 23.1.2.
Where is the standard library documentation? At /std/,
generated from the newest release, and the language reference is at
/langref.html. Both are also served for the version you
installed: doc/langref.html is in every release archive, and zig std serves
the autodocs and opens a browser tab.
Where to start
- Installing — download a release, check its SHA256SUMS, and
run
zig. - What Zig++ Adds — private fields, LLVM,
std.gpu. - GPU Programming — kernels, the CUDA, HIP, and Metal host APIs, and the standalone test that runs them.
- Building from Source — the CMake build, the bootstrap compiler, and the devkits that CI uses.
Installing
Installing Zig++ is one line. On Linux and macOS:
curl -fsSL https://zigpp.lol/ppup | sh
On Windows, in PowerShell:
irm https://zigpp.lol/ppup.ps1 | iex
Both run ppup, Zig++’s installer and toolchain manager. It unpacks the newest
release, makes it the default zig, installs ppup itself, and puts it on your
PATH so you can install more versions later. From then on:
zig version
0.17.0-dev.2380+zigpp.add158e97
ppup
| Command | What it does |
|---|---|
ppup | Install the newest release as the default toolchain, install ppup, and add it to PATH |
ppup update | Install the newest release and make it the default |
ppup install <version|latest> | Install a version, e.g. ppup install 0.17.0-dev.2380+zigpp.add158e97 |
ppup default [<version>] | Show the default toolchain, or make a version the default |
ppup list | List the installed toolchains, marking the default |
ppup uninstall <version> | Remove a toolchain; the default one refuses until another is the default |
ppup self update | Replace ppup with the newest one from zigpp.lol |
ppup self uninstall | Remove every toolchain, ppup, and its PATH entry |
ppup help | List the commands, and ppup --version prints ppup’s own version |
Where the toolchains live:
| Linux, macOS | ~/.zigpp: toolchains/<version>/ holds each release, bin/zig is the default compiler, bin/ppup is ppup |
| Windows | %LOCALAPPDATA%\zigpp: toolchains\<version>\ holds each release, current is a directory junction to the default one, bin\ppup.ps1 (and a bin\ppup.cmd shim) is ppup |
ppup adds two lines to the profile of your shell—~/.zshrc, ~/.bashrc,
~/.config/fish/conf.d/ppup.fish, or ~/.profile: a # Zig++ (ppup) comment
and the line that puts its bin directory on PATH.
ppup self uninstall removes them again. The environment variables
PPUP_HOME and PPUP_NO_MODIFY_PATH=1 (the --no-modify-path option does the
same) move the installation and keep ppup out of your profile:
curl -fsSL https://zigpp.lol/ppup | sh -s -- --no-modify-path
PPUP_HOME=/opt/zigpp curl -fsSL https://zigpp.lol/ppup | sh
The installed ppup keeps to the home it is installed in, so PPUP_HOME is
needed only for the install.
Zig++ publishes releases for these four targets; anything else (an
x86_64 Mac, say) fails with a message listing them:
| Target | Archive |
|---|---|
| x86_64-linux | zig-x86_64-linux-<version>.tar.xz |
| aarch64-linux | zig-aarch64-linux-<version>.tar.xz |
| aarch64-macos | zig-aarch64-macos-<version>.tar.xz |
| x86_64-windows | zig-x86_64-windows-<version>.zip |
The Windows build is x86_64 only, so on ARM64 Windows ppup installs that one
and says that it runs under emulation. The
Release workflow
publishes a release on every push to master, and the Downloads
chapter lists the archives of every release with their SHA-256 checksums;
Versions and Releases explains what <version> looks like.
There is no package manager for Zig++ and no pre-built binary from anywhere but
these releases: ppup, or unpacking an archive by hand, are the two ways in. A
compiler built from source is the other option; see
Building from Source.
Unpack an archive anywhere
A Zig installation is two things: the zig executable, and the lib/
directory. At runtime, the executable searches up the file system for lib/,
relative to itself:
lib/lib/zig/../lib/../lib/zig/- and so on
In other words, you can unpack a release of Zig++ anywhere and begin using it
immediately, with no installation step, although the search also supports
installing it globally (/usr/bin/zig and /usr/lib/zig/). This is all
ppup does: it unpacks toolchains/<version>/ and points bin/zig at it.
A release archive holds:
zig-x86_64-linux-0.17.0-dev.2361+zigpp.5b96e6d21/
zig the compiler
lib/ the standard library, builtin headers, and build system
doc/langref.html the language reference
LICENSE
README.md
On Windows the executable is zig.exe, and the archive is a .zip.
tar -xJf zig-x86_64-linux-0.17.0-dev.2361+zigpp.5b96e6d21.tar.xz
./zig-x86_64-linux-0.17.0-dev.2361+zigpp.5b96e6d21/zig version
0.17.0-dev.2361+zigpp.5b96e6d21
Verify the download
Each release has a SHA256SUMS asset: the SHA-256 of every archive and of
index.json, in the format sha256sum reads. Download the release’s assets
into one directory, then check them:
sha256sum -c SHA256SUMS
If you downloaded only the archive for your target, check that one line and skip the rest:
sha256sum -c --ignore-missing SHA256SUMS
zig-aarch64-linux-0.17.0-dev.2361+zigpp.5b96e6d21.tar.xz: OK
zig-aarch64-macos-0.17.0-dev.2361+zigpp.5b96e6d21.tar.xz: OK
zig-x86_64-linux-0.17.0-dev.2361+zigpp.5b96e6d21.tar.xz: OK
zig-x86_64-windows-0.17.0-dev.2361+zigpp.5b96e6d21.zip: OK
index.json: OK
ppup checks the same sums itself, and unpacks nothing that fails the check.
The same hashes, with the size of each archive, are in
index.json, which uses the format of
ziglang.org’s download index, so
tools that read that one can read this one.
Documentation for the version you have
-
The language reference is
doc/langref.htmlinside the archive, and this site serves the newest release’s copy at /langref.html. -
The standard library documentation is served by the compiler itself:
zig stdIt generates the autodocs for the
lib/next to the executable and opens a browser tab. This site serves the newest release’s copy at /std/.
Next
- What Zig++ Adds for the language features.
- GPU Programming to run Zig++ on an NVIDIA or AMD GPU.
- Any Zig Version for pinning a version per project.
- Building from Source for targets without a
release, for the
zig2bootstrap compiler, and for LLVM 23 development.
What Zig++ Adds
Zig++ is a superset of Zig: every valid Zig program is a valid Zig++ program,
unless it names something priv (write @"priv" instead). This chapter covers
what it adds to the language and to the toolchain.
Private fields
Struct fields are public by default, meaning that they can be accessed from any
file. A field declared with the priv keyword is private: it can only be
accessed by name from within the file which declares the struct, just like a
declaration which is not marked pub. Private fields allow a type to protect
its invariants, and communicate which fields are implementation details rather
than part of its API.
/// A fixed-capacity buffer. `len` is private, so code in other files cannot
/// break the invariant that `len` never exceeds the capacity of `bytes`.
pub const Buffer = struct {
bytes: [16]u8 = undefined,
priv len: usize = 0,
pub fn append(buf: *Buffer, byte: u8) error{Overflow}!void {
if (buf.len == buf.bytes.len) return error.Overflow;
buf.bytes[buf.len] = byte;
buf.len += 1;
}
pub fn slice(buf: *const Buffer) []const u8 {
return buf.bytes[0..buf.len];
}
};
Code in the file that declares the type names len normally, including in
tests. Code in any other file cannot name it, whether to read it, write it,
take its address, call it, or specify its value in an initialization
expression:
const Buffer = @import("test_private_fields.zig").Buffer;
test "access a private field from another file" {
var buf: Buffer = .{};
try buf.append('a');
buf.len = 100;
}
// error: field 'len' of struct 'test_private_fields.Buffer' is private
Private fields are still a part of every value of the type. If a private field has no default value, the struct can only be initialized within the file which declares it, typically by a public initialization function.
Field privacy only applies to syntax which names a field. @typeInfo reports
private fields along with all other fields, indicating their privacy with the
@"priv" field attribute, and builtins which name fields with strings, such as
@field, @FieldType, and @offsetOf, can be used with any field. This
allows generic code, such as formatting, comparison, hashing, and
serialization, to operate on all types:
const info = @typeInfo(Buffer).@"struct";
try expect(std.mem.eql(u8, info.field_names[1], "len"));
try expect(info.field_attrs[1].@"priv");
// Builtins which name fields with strings are not subject to field privacy.
var buf: Buffer = .{};
try buf.append('z');
try expect(@field(buf, "len") == 1);
A type created with @Struct or @Union has private fields where the
@"priv" field attribute is set; these fields are private to the file
containing the reification builtin call.
Fields of unions can be marked priv as well. Outside of the file which
declares the union, a private field cannot be accessed, initialized, or have
its payload captured by a switch prong which names it. The tag of a union is
not affected by field privacy, so it can still be compared against and switched
on, and else and inline else prongs can capture any payload.
Enum fields and tuple fields cannot be marked priv.
priv precedes comptime in a field declaration, so the grammar rule for a
container field is:
ContainerField <- doc_comment? KEYWORD_priv? KEYWORD_comptime? (IDENTIFIER COLON)? TypeExpr ByteAlign? (EQUAL Expr)?
Because priv is a keyword, an identifier with that name is written
@"priv". That is the whole of the incompatibility with Zig: a Zig program
that declares, or names, something priv needs quotes around it, and one that
expects to reach private fields from other files needs a different design.
Upstream closed the proposal for private
fields as not planned. Zig++’s
release smoke test compiles a struct with a private field, runs it, and checks
that naming that field from another file fails with a diagnostic that mentions
private.
The full rules, with the tests above as runnable examples, are in the language reference: Private Fields.
LLVM is forever
Upstream Zig plans to drop its dependency on the LLVM libraries, and to
eliminate its dependency on the LLVM library API
calls. Zig++ will never phase out
LLVM. In package.json terms, LLVM stays in dependencies.
Zig++ builds with LLVM, Clang, and LLD 23.1.2. The CMake build requires the
23.x development libraries, and refuses a llvm-config older than 23 or newer
than 24.
The LLVM-less build path exists only to bootstrap: cc -o bootstrap bootstrap.c && ./bootstrap produces a zig2 that is a stage2 build without LLVM
extensions, and it lacks release-mode optimizations, some ELF, COFF/PE, and
WebAssembly linking features, the ability to create static archives from object
files, the ability to compile assembly files, and the ability to compile C,
C++, Objective-C, and Objective-C++. It is enough to run ./zig2 build and
produce a real Zig++ compiler. See
Building from Source.
LLVM, and MLIR above it, are how Zig++ goes the final stretch on GPUs: the blessed path lowers Zig++ directly to PTX, to AMD GPU code objects, and to Metal libraries, with first-class GPU intrinsics.
std.gpu
std.gpu is a port of ugpu into the
standard library. Kernels are plain Zig functions, and they can use the rest of
the standard library as long as they avoid operating system services:
- Kernels are exported functions with the
.kernelcalling convention, andstd.gpuhas CUDA’s indexing (threadIdx,blockIdx,blockDim,gridDim,globalId),syncThreads(the new@workGroupBarrierbuiltin), warp shuffles, votes and reductions, atomics, fast math approximations, andprint. - The standard library runs on the GPU:
std.fmt,std.json,std.mem,std.base64, hash maps, and array lists, with allocators for shared memory and for the CUDA device heap instd.gpu.allocators. A panic in a kernel reports its message to the host. - Every NVPTX and AMDGPU module carries the compiler-rt routines that it calls,
so
@sin,@exp,@log,f128, and float parsing work in kernels, with the same results as on the host, bit for bit. Upstream Zig crashes LLVM on@sinfor NVPTX. std.gpu.cudaloads the CUDA driver at run time, so programs build without the CUDA toolkit, and launches kernels from the host.std.gpu.hipdoes the same with the HIP runtime of AMD GPUs, on Linux and on Windows, where it needs no libc.std.gpu.metalloads the Metal framework at run time and runs kernels that Zig++ compiles for theair64target into a.metallib, in process, with no Xcode, Metal toolchain, or macOS SDK. Apple GPUs have nof64, noprint, and only relaxed atomics, which are compile errors in their kernels.
GPU Programming covers the device API, the host APIs, and how to build and run the kernels. Still to come: MLIR lowering for tensor cores and kernel fusion.
std.Io.Threadz
std.Io.Threadz is an Io implementation whose unit of work is a task: a
function on its own stack that parks at every Io call that has to wait, while
its worker thread runs the next task. Code written against std.Io runs on it
unchanged, so moving a program between Io.Threaded and Io.Threadz changes
one declaration:
var threadz: std.Io.Threadz = undefined;
try threadz.init(gpa, .{});
defer threadz.deinit();
const io = threadz.io();
- The thread that calls
initbecomes the first worker and runs as the main task. More workers start as work arrives, one per CPU by default;setWorkerLimitlowers the count, and-jsets it in a compiler built with-Dio-mode=evented. - Every task gets its own mapping with a guard page below the stack, and the OS
commits the stack as the task touches it. Tasks reserve 256 KiB unless
InitOptions.stack_sizesays otherwise. - A task’s affinity decides where it runs whenever it becomes runnable.
.sticky, the default, resumes it on the worker that last ran it, and another worker takes it only from a worker with more than one task waiting..pinnedkeeps it on one worker, for tasks that own that worker’s resources..freelets whichever worker is idle take it.concurrentWithandgroupConcurrentWithtake the affinity and the stack size of one task. - A task may complete 128
Iooperations that nobody has to wait for before the next one goes to the back of its worker’s queue, with its affinity unchanged. A worker takes the task it will run next, and a stuck worker’s slot for that task is the watchdog’s to hand to the shared queue, so that a worker pays no locked instruction for its own: the watchdog has the backend issue a process-wide barrier, re-reads what that worker wrote at its last switch, and takes the slot only when the worker has not switched since. Counting operations without parking bounds what one task can take from a worker without ever blocking on it, and the count starts over at every park or yield.scheduler.budgetis the 128. - Every task has an id, unique among the tasks the program made, and a name:
the function it was spawned with, from
Io.spawnedName. A log line and, in step 7, the task dump use them. Io.blocking(fn, args)is for a call that blocks without making anIocall of its own, such as a C library call or a raw syscall. It returnsfn’s result, errors and all. On Threadz it runs on a dirty pool, anIo.Threadedinstance the Threadz instance starts with the first such call, while the calling task parks; the arguments and the result stay on the task’s stack, where the pool thread reads them and writes the result back. Sixteen calls that each block for 200 ms finish in 205 ms, and the workers go on running tasks while they block. The call is not cancelable once it has started: a cancelation request takes effect at the calling task’s next cancelation point after it returns.Io.Threaded, and the implementations with no pool of their own, make the call on the calling thread.- One watchdog thread per instance, started with the first task the instance
makes or runs, samples every worker every 10 ms. A worker whose current task
has not switched out for 100 ms is stuck. Two rounds ten milliseconds apart,
and the worker thread’s CPU time between them, tell the kinds apart: a thread
that used less than a fifth of the wall time is blocked, in the kernel or in
a C call that never said it blocks, and its task is named in one
std.logwarning scoped.threadz, with its id, the function it was spawned with, and how long it has run; its queued tasks, including the ones others made runnable for it and the one in its slot for the next task, become takeable by every worker whatever their number; and one replacement worker is started for it, so that parallelism stays, which stops once it has nothing to run and the stuck worker switches out again. A thread that kept using its time is computing, which is what a compiler task is: its queued work is taken just the same, but no worker is replaced —-jand the worker limit mean what they say — and nothing is logged. An instance whose worker limit is one is covered for a blocked task, whose replacement is where the tasks behind it run; a computing task holds the only worker, which is what a limit of one means. Pinned tasks on a stuck worker wait for the worker, which they own the resources of.Threadz.stats()reads the counters: workers running, replacements, workers stuck now, the episodes of each kind, the rounds of samples the watchdog made, and the task and kind of the last episode. A worker pays one store per switch for the watchdog, of the task it runs, and one load of a flag when it takes the task in its slot. The watchdog waits without a timeout while every worker is parked, so an idle program is not woken to sample workers that are all asleep. - Threads outside the pool can use the synchronization primitives, whose
futexes they wait on in the kernel. Other
Iocalls from those threads are not supported yet. io.arena()returns an allocator whose memory belongs to the task: it is created at the first call, out of the implementation’s own allocator, and released in one step when the task returns, whether the task freed it or not. It is where a task puts what it builds up and throws away, such as one request’s parsed headers, and a value that outlives the task must not point into it: move what crosses a task boundary byownedtransfer, into another allocator, or by copying it. The main thread has one too, released bydeinit, andio.blockingruns on a thread of its own, which sees neither the calling task’s arena nor its scopes.io.Scoped(T)is the task-local value. The key is declared once at container level,const current = std.Io.Scoped(u32), bound around a call withcurrent.run(io, value, function, args), and read withcurrent.get(io)- in that call, and in every task spawned inside it, a child of a child as much as a child, each by its own copy, until it returns. Outside the call the binding is gone.threadlocalcannot serve this on Threadz: a task migrates at a park point, and the fiber switch does not move the thread’s TLS base, sothreadlocalis worker-local here, not task-local, andIo.Scopedis what a per-task value uses. The compiler’s per-thread ids and a logger’s context are the intended movers.std.Io.Supervisoris a group of tasks with OTP’s restart policy. Children are added withadd- the function they run and its arguments, which are copied - before the firstrun.one_for_onerestarts the child that failed,one_for_allcancels and restarts every child, andrest_for_onethe child that failed and the ones added after it. A child that returns normally is not restarted, which is OTP’stransientand the only restart type in this cut, and a panic stays fatal. More restarts thanintensitywithinperiod- three in five seconds by default - cancel every child and returnerror.RestartIntensityExceeded, with the error it gave up on inlastChildError. Cancelingruncancels the children and returnserror.Canceled.Io.Queuetakes its overflow policy atinitWithOptions:.block, the default and the behaviour a queue always had;.drop, where a put that finds the queue full drops what does not fit, counts it, and never waits, for a mailbox that keeps the newest sample; and.drop_and_resync, which does the same and makes the nextgetreturnerror.Resynconce, for a consumer that must notice a drop rather than act on stale data.droppedElementscounts what was dropped. Thegeterror set haserror.Resyncin it, which only adrop_and_resyncqueue returns.
On Linux, Threadz runs on io_uring, with a ring per worker: sockets, DNS,
processes, timers, futexes, and file reads are ring operations. The file
operations io_uring can only finish on a kernel thread of its own, such as
writes to a file, statx, and creating a file, are made on the worker instead,
because the trip to that thread and back costs more than the call.
A task that drives io_uring itself, such as a server’s event loop, can borrow
its worker’s ring instead of making one of its own. acquireRing lends it to a
task pinned to that worker. The task queues SQEs on the ring with
ring_owner_bit set in their user_data, and Ring.waitCqes parks it until
one of its completions arrives. The worker handles every other completion as
before and runs its other tasks while the loop is parked. fromIo finds the
Threadz behind an Io, and workerLimit says how many workers a program may
pin tasks to.
zix’s io_uring servers for HTTP/1 and WebSocket, HTTP/2, gRPC and HTTP/3 run on Threadz this way, each loop a task pinned to its own worker, and keep their multishot receives, buffer rings and batched submissions. On a 32-thread laptop, across the 45 load tiers of their HttpArena profiles, they served at a geometric mean of 1.01 times the throughput of the same servers on threads of their own, with single tiers from 0.83 to 1.23 times, inside the laptop’s own run-to-run spread.
zig build test-threadz runs the Io tests on Threadz, and the test runner’s
--io=threadz runs any test on it. The design, and the steps still to come
(the kqueue and IOCP cores, arenas and supervisors, the task dump and the
scheduler gauges, and the async/await keywords), are in
the Threadz proposal.
AI in the toolchain
Upstream Zig bans LLMs from issues, patches, and bug tracker comments. Zig++ welcomes them, and it is putting AI on both sides of compilation: in the programs it builds and in the build itself; see AI Policy and Governance.
GPU Programming
std.gpu runs Zig++ on NVIDIA, AMD and Apple GPUs. This chapter is written
from the documentation comments in lib/std/gpu.zig, lib/std/gpu/*.zig, and
from the end-to-end test of everything described here,
test/standalone/gpu.
// kernels.zig
const gpu = @import("std").gpu;
export fn wave(data: [*]f32, amplitude: f32, n: u32) callconv(.kernel) void {
const i = gpu.globalId(.x);
if (i < n) data[i] = amplitude * @sin(data[i]);
}
An exported function with the .kernel calling convention is a kernel. Kernels
can use the rest of the standard library as long as they avoid operating system
services: std.fmt, std.json, std.mem, std.base64, hash maps, array
lists, and the device allocators all work on the GPU, up to the limits that a
target has (see “What is not there yet” at the end of this chapter).
The device API
The device-side functions in std.gpu are implemented for NVPTX, AMDGPU and
air64, the target of Apple GPUs. The indexing functions and syncThreads use
builtins that also exist for SPIR-V.
Indexing
pub const Dim = enum(u2) { x, y, z };
pub inline fn threadIdx(comptime dim: Dim) u32
pub inline fn blockIdx(comptime dim: Dim) u32
pub inline fn blockDim(comptime dim: Dim) u32
pub inline fn gridDim(comptime dim: Dim) u32
pub inline fn globalId(comptime dim: Dim) u32
These are CUDA’s indexing functions. globalId(dim) is
blockIdx(dim) * blockDim(dim) + threadIdx(dim): the index of the calling
thread within the whole grid. threadIdx, blockIdx, and blockDim are
@workItemId, @workGroupId, and @workGroupSize. On Apple GPUs the hardware
provides gridDim and globalId directly, as the air.threadgroups_per_grid
and air.thread_position_in_grid builtins, one component per dimension that
the kernel asks for.
Synchronization
pub inline fn syncThreads() void
Waits until every thread of the block has reached this call, and makes the
memory writes that each thread made before the call visible to the others.
Every thread of the block must reach the same call; anything else is undefined
behavior. It is @workGroupBarrier().
Warps
pub const warp_size: comptime_int
pub const WarpMask = @Int(.unsigned, warp_size);
pub inline fn laneId() u32
warp_size is 32, except on AMD: 64 before GFX10, and 32 from GFX10 unless
wavefrontsize64 or wavefrontsize32 selects otherwise. laneId() is the
index of the calling thread within its warp, from 0 to warp_size - 1.
An Apple GPU runs a thread in a SIMD group of 32 lanes, which is what a warp is
there: warp_size is 32, and laneId() is the thread’s position in its SIMD
group. Apple’s ballot is a 64-bit word with a bit per lane, of which the lanes
of the calling group are the low 32 bits, and that is what ballot returns.
pub inline fn shflDown(value: anytype, delta: u32) @TypeOf(value)
pub inline fn shflUp(value: anytype, delta: u32) @TypeOf(value)
pub inline fn shflXor(value: anytype, lane_mask: u32) @TypeOf(value)
pub inline fn shflBroadcast(value: anytype, src_lane: u32) @TypeOf(value)
Warp shuffles. Every thread of the warp that has not exited must call them
together, and a value read from a thread that has exited is undefined. shflUp
and shflDown read delta lanes below or above, and yield the caller’s own
value when that lane is outside the warp; shflXor reads laneId() ^ lane_mask; shflBroadcast reads src_lane modulo warp_size. Shuffles
support values of at most 64 bits.
pub inline fn all(predicate: bool) bool
pub inline fn any(predicate: bool) bool
pub inline fn uniform(predicate: bool) bool
pub inline fn ballot(predicate: bool) WarpMask
pub inline fn popcount(predicate: bool) u32
pub fn warpReduceSum(value: anytype) @TypeOf(value)
pub fn warpReduceMax(value: anytype) @TypeOf(value)
pub fn warpReduceMin(value: anytype) @TypeOf(value)
Votes and reductions. all and any are true when every, or any, lane’s
predicate is true; uniform is true when the predicate has the same value for
every lane; ballot has a set bit for each true lane, and popcount counts
them. The reductions return the result to every lane, and integer overflow is
checked like +, except on Apple GPUs: there each reduction of an f32, i32
or u32 is one hardware instruction for the whole SIMD group, and the integer
form of the sum wraps rather than reporting an overflow the way the checked +
of the other targets does.
Atomics
pub inline fn atomicAdd(ptr: anytype, operand: @TypeOf(ptr.*)) @TypeOf(ptr.*)
pub inline fn atomicExchange(ptr: anytype, operand: @TypeOf(ptr.*)) @TypeOf(ptr.*)
pub inline fn atomicCAS(ptr: anytype, expected: @TypeOf(ptr.*), new_value: @TypeOf(ptr.*)) @TypeOf(ptr.*)
pub inline fn atomicMin(ptr: anytype, operand: @TypeOf(ptr.*)) @TypeOf(ptr.*)
pub inline fn atomicMax(ptr: anytype, operand: @TypeOf(ptr.*)) @TypeOf(ptr.*)
Each returns the value the location held before the operation. They use relaxed
ordering, visible to the whole device, and ptr may be a global, shared, or
generic pointer. For other orderings, use @atomicRmw and @cmpxchgStrong.
On Apple GPUs, the operations are exactly the ones the Metal language has: only
the relaxed orderings, .monotonic and .unordered, exist, and a stronger one
is a compile error. The compare-exchange is the weak form that AIR has, which
@cmpxchgStrong and @cmpxchgWeak both lower to. An atomic through a pointer
without an address space, which is a pointer into function-local memory, is a
compile error as well: give it addrspace(.global) or addrspace(.shared).
Fast math
pub const fast = struct {
pub inline fn sin(x: f32) f32
pub inline fn cos(x: f32) f32
pub inline fn sqrt(x: f32) f32
pub inline fn fma(a: f32, b: f32, c: f32) f32
};
Hardware approximations: PTX sin.approx.f32, cos.approx.f32, and
sqrt.approx.f32, AMD v_sin_f32 and v_cos_f32, or Apple’s air.fast_sin,
air.fast_cos, and air.fast_sqrt. They are faster than @sin, @cos, and
@sqrt, and their error is bounded in absolute terms rather than relative to
the input. AMD’s square root is not one of them: its instruction is accurate to
one unit in the last place, so fast.sqrt is a compile error there.
fast.fma(a, b, c) is a * b + c with a single rounding, the fused
multiply-add that each of the three GPUs has an instruction for. It is the one
function of the namespace whose result is exact: air.fma.f32 on Apple GPUs,
and @mulAdd on the others, which is LLVM’s llvm.fma.f32, fma.rn.f32 on
PTX and v_fma_f32 on AMD.
Printing and panics
pub fn print(comptime fmt: []const u8, args: anytype) void
pub fn assertFail(message: []const u8) noreturn
print formats like std.fmt and writes to the host’s standard output,
truncating at a 256-byte stack buffer. NVIDIA prints at the next host/device
synchronization. AMD writes into a 1 MiB buffer that hip.Context.loadModule
connects and hip.Context.synchronize drains, dropping output beyond that; a
code object loaded by another host prints nothing.
assertFail stops the launch and reports the message with the block and thread
that failed, truncated to 255 bytes. std.debug.defaultPanic calls it for
.cuda and .amdhsa targets, so a panic in a kernel reports its message to
the host. After a failed assertion, CUDA’s next Context.synchronize returns
error.Assert and the context cannot run more kernels, while HIP’s next
synchronization writes to stderr, returns error.Assert, and leaves the
context usable.
On Apple GPUs, both functions are compile errors, and the error names the
reason: Metal has neither printf nor __assertfail. The default panic
namespace of an air64 module is therefore std.debug.no_panic: a failed
safety check traps without formatting a message, which stops the launch and
leaves its results unwritten.
Allocators
pub const device_heap: std.mem.Allocator
pub fn BumpAllocator(comptime size: usize) type
device_heap is the CUDA device heap: memory that outlives a block, freed with
free. Its default size is 8 MiB, unless the host changes
cuda.Limit.malloc_heap_size before the heap is first used. It is implemented
with NVPTX-only syscalls, and is a compile error on other architectures.
BumpAllocator(size) carves allocations out of block-shared memory, which is
gone when the block finishes:
var heap: [16 * 1024]u8 addrspace(.shared) = undefined;
export fn kernel() callconv(.kernel) void {
var bump = std.gpu.allocators.BumpAllocator(heap.len).init(&heap);
if (std.gpu.threadIdx(.x) == 0) {
var list: std.ArrayList(u32) = .empty;
list.append(bump.allocator(), 42) catch return;
}
}
Every thread of the block must call init, with the same buffer, before any
of them allocates: it waits at a barrier for the thread that writes the offset
of the unused memory. used() reports how much has been allocated,
allocations may race with each other, and only the most recent allocation can
be returned to the allocator. Buffers are limited to 4 GiB.
For memory used by a single thread, std.heap.FixedBufferAllocator needs no
synchronization. All three implement std.mem.Allocator, so the containers in
std work in kernels.
Compiling kernels
For NVIDIA, compile the kernel module to PTX with zig build-obj, then load
the PTX with cuda.Context.loadModule:
zig build-obj -target nvptx64-cuda -mcpu=sm_75 -O ReleaseFast -fno-emit-bin -femit-asm=kernels.ptx kernels.zig
PTX for an older -mcpu runs on newer GPUs, because the driver compiles it for
the GPU that loads it.
For AMD, compile it to a code object with zig build-lib -dynamic, then load
it with hip.Context.loadModule:
zig build-lib -dynamic -target amdgcn-amdhsa -mcpu=gfx1036 -O ReleaseFast kernels.zig
A code object only runs on the architecture that -mcpu names;
hip.Device.archName reports it, as gfx1036, or with features as
gfx90a:sramecc+:xnack-, whose compiler spelling is
-mcpu=gfx90a+sramecc-xnack. On AMD, the first shared variable can live at
address 0, so index shared variables or @addrSpaceCast them to generic
pointers instead of building a shared pointer with @ptrFromInt(0).
For Apple GPUs, compile the kernel module to the .metallib container that the
Metal runtime loads, then load the container with metal.Context.loadModule:
zig build-obj -target air64-macos -O ReleaseFast -femit-bin=kernels.metallib kernels.zig
The object file of the air64-macos target is the library itself, so no
linker, no Metal toolchain, and no macOS SDK are part of the pipeline: the GPU’s
driver compiles the AIR of the module when the host builds a pipeline.
-femit-llvm-bc writes the downgraded AIR bitcode that goes into the
container, and -femit-asm and -femit-llvm-ir are errors, because there is
no AIR assembly and no textual IR that this compiler could write.
A Metal library carries the AIR version, the Metal language version, and the
container version of one macOS release, which the deployment target of the
compile selects: macOS 13 gives AIR 2.5, Metal 3.0, and container 1.2.7; 14
gives 2.6, 3.1, and 1.2.7; 15 gives 2.7, 3.2, and 1.2.8; 26 gives 2.8, 4.0, and
1.2.9; and 27 gives 2.9, 4.1, and 1.2.9. Name the release as the version of the
OS in the target, as in -target air64-macos.26.0, or as .os_version_min in a
Target.Query.
An Apple kernel’s parameters are the ones a Metal host binds by index: a
pointer into the device, constant, or threadgroup address space is a buffer,
such as [*]addrspace(.global) const f32, which the host passes with
setBuffer:offset:atIndex:, and a scalar or an aggregate is passed as bytes
with setBytes:length:atIndex:, which the compiler loads through a constant
pointer. Threadgroup memory is a module-level variable in the shared address
space, whose size must be known at compile time, because the runtime takes it
from the pipeline: 32 KiB on current Apple GPUs, 16 KiB on some older families.
Dynamic threadgroup memory is not part of the API. The position of the calling
thread is not a parameter; the dispatch supplies it.
In a build script, compile kernels with b.addObject and embed
getEmittedAsm() for PTX, or b.addLibrary with .linkage = .dynamic and
embed getEmittedBin() for a code object:
const kernels = b.addObject(.{
.name = "kernels",
.root_module = b.createModule(.{
.root_source_file = b.path("kernels.zig"),
.target = kernel_target,
.optimize = .fast,
}),
});
exe.root_module.addAnonymousImport("kernels.ptx", .{
.root_source_file = kernels.getEmittedAsm(),
});
An Apple container is an emitted binary as well: b.addObject with an
air64-macos target, whose getEmittedBin() is the .metallib to embed.
test/standalone/gpu/build.zig
builds all three kinds, in a debug and a fast variant: the CUDA and HIP images,
with the AMD code objects for a set of architectures given as -Damdgpu-arch
(comma-separated, default gfx1030), and the containers of metal_kernels.zig
for air64-macos, with a macOS 26 deployment target.
The host APIs
std.gpu.cuda is the CUDA driver API and std.gpu.hip is the HIP runtime
library. They expose the same names, and a program switches between them by
changing the import. std.gpu.metal is the Metal framework, whose objects and
calls are Metal’s own rather than a third copy of the other two:
std.gpu.cuda | std.gpu.hip | std.gpu.metal | |
|---|---|---|---|
| Loads at run time | libcuda.so.1 | libamdhip64.so.7, .so.6, .so, or amdhip64_7.dll, _6.dll | Metal.framework, /usr/lib/libobjc.A.dylib, /usr/lib/libSystem.B.dylib |
| Platform | Linux, with libc | Linux with libc, and Windows without it (ntdll.LdrLoadDll) | macOS, with libc |
| Toolkit needed to build | none | none | none |
| Module image | PTX, null-terminated: [:0]const u8 | a code object for one architecture: []const u8 | a .metallib: []const u8 |
| Allocation failure | error.OutOfDeviceMemory | error.OutOfMemory | error.OutOfMemory |
The CUDA runtime library, NVRTC, events, and graphs are not part of
std.gpu.cuda; events, graphs, the memory pools of stream-ordered allocation,
and the rest of the runtime are not part of std.gpu.hip; and the rest of the
Metal framework, the Metal Shading Language compiler, and Metal’s own runtime
libraries are not part of std.gpu.metal, which sends its messages to the
framework’s Objective-C objects with objc_msgSend. All three require the
Driver to stay open, and not to move, while objects that point to it are in
use. The Metal framework is loaded with dlopen, which is part of libc, so a
program that uses std.gpu.metal links libc and needs neither the macOS SDK
nor an Objective-C compiler.
The CUDA and HIP namespaces export the same Driver, Device, Context,
Module, Function, Buffer(T), Stream, LaunchConfig, DevicePtr,
Version, ComputeCapability, Attribute, Limit, ModuleOptions, Error,
and OpenError, and the sequence for a kernel launch is the same in both:
var driver = try cuda.Driver.open();
defer driver.close();
const context = try (try driver.device(0)).retainPrimaryContext();
defer context.release();
const module = try context.loadModule(@embedFile("kernels.ptx"), .{});
defer module.unload();
var data: [1000]f32 = undefined;
for (&data, 0..) |*x, i| x.* = @floatFromInt(i);
const buffer = try context.alloc(f32, data.len);
defer buffer.free();
try buffer.copyFromHost(&data);
const wave = try module.function("wave");
try wave.launch(cuda.LaunchConfig.linear(data.len, 256), .{ buffer, @as(f32, 2), @as(u32, data.len) });
try context.synchronize();
try buffer.copyToHost(&data);
Driver.open() returns error.DriverNotFound when the library is absent and
error.IncompatibleDriver when it lacks a function Zig++ needs.
retainPrimaryContext makes the context current. Module.function looks up a
kernel by the name it was exported with, and takes a null-terminated name.
std.gpu.metal is the same shape where Metal matches the other two, and its
own where it does not. Driver.open reports error.FrameworkNotFound on any
system other than macOS, error.NoDevice when the framework finds no GPU, and
error.NotSupported on an Intel Mac, where the MTLSize of a message passes
and returns by other rules than the arm64 ABI that the messages of this
namespace are built for. Driver.device() is the GPU of the machine, and
Device.createContext makes the context that holds the command queue, because
Metal has no primary context. Context.loadModule takes the bytes of a
.metallib; Module.function finds a kernel by the name it was exported with;
and Function.pipeline is where the Metal compiler runs, so it is the call
that reports a kernel the compiler refused, with what it said in
Options.error_log. Pipeline.launch encodes the dispatch:
var driver = try metal.Driver.open();
defer driver.close();
const context = try driver.device().createContext();
defer context.release();
const module = try context.loadModule(@embedFile("kernels.metallib"), .{});
defer module.unload();
const wave = try module.function("wave");
defer wave.release();
const pipeline = try wave.pipeline(.{});
defer pipeline.release();
var data: [1000]f32 = undefined;
for (&data, 0..) |*x, i| x.* = @floatFromInt(i);
const buffer = try context.alloc(f32, data.len);
defer buffer.free();
buffer.copyFromHost(&data);
try pipeline.launch(metal.LaunchConfig.linear(data.len, 256), .{ buffer, @as(f32, 2), @as(u32, data.len) });
try context.synchronize();
buffer.copyToHost(&data);
A Buffer of std.gpu.metal is unified memory: values() is the memory as a
slice of T, the same bytes the kernels read and write, and copyFromHost,
copyToHost, and zero copy and fill it with @memcpy and @memset rather
than calling into the framework, so no copy of the framework’s is between the
host and the memory. A launch binds its arguments in parameter order: a Buffer
with setBuffer:offset:atIndex:, and anything else with
setBytes:length:atIndex:, at the index of the argument, from which the
compiler loads a scalar through a constant pointer; the position of the
calling thread comes from the dispatch. A threadgroup of no threads, or one
larger than the kernel allows, is error.InvalidValue before anything is
encoded, and a kernel that fails on the GPU is reported by Context.synchronize,
which is why the launch is asynchronous.
The errors of the framework are named after what happened rather than after the
numeric code: the codes of the MTLLibraryErrorDomain and
MTLCommandBufferErrorDomain become error.InvalidLibrary,
error.CompileFailure, error.FunctionNotFound, error.PageFault, and the
rest, and a failure of a domain or a code that this version does not know is
reported as the failure of the call. Options.error_log receives what the
framework said, as the domain, the code, and the message.
Launch configuration
pub const Dim3 = struct { x: u32 = 1, y: u32 = 1, z: u32 = 1 };
pub const LaunchConfig = struct {
grid: Dim3 = .{},
block: Dim3 = .{},
shared_memory: u32 = 0,
stream: ?Stream = null,
};
shared_memory is dynamic shared memory per block, on top of any the kernel
declares statically, and a null stream selects the null stream.
LaunchConfig.linear(n, block_size) covers n threads with blocks of
block_size, rounding the grid up; block_size must not be zero, and n == 0
produces no blocks, which launch rejects with error.InvalidValue.
std.gpu.metal’s LaunchConfig has grid and block only: Metal has no
dynamic shared memory, and a context has one command queue rather than a choice
of streams.
The launch arguments are passed in parameter order, one per kernel parameter:
- a
Buffer(T)passes the device address it holds; - a
DevicePtr(anenum(u64) { _ }, a device memory address) passes unchanged; - integers, floats, booleans, enums, vectors, and
externorpackedstructs pass by value.
A slice or a host pointer as a kernel argument, an untyped compile-time number,
a value with an unsupported layout, or a non-tuple argument list is a compile
error. Scalars need an explicit type: @as(u32, data.len).
std.gpu.metal binds the arguments the same way, with one difference: it has no
DevicePtr, and every argument that is not a Buffer binds its bytes at the
index of the parameter.
pub const DevicePtr = enum(u64) { _ };
pub fn Buffer(comptime T: type) type
Buffer(T) has the methods free, copyFromHost, copyToHost, and zero in
all three namespaces, and CUDA’s and HIP’s have the fields driver, ptr, and
len. Copies may be shorter than the buffer, never longer. Stream has
destroy and synchronize, and HIP’s stream synchronization does not itself
flush the GPU print and assert output: that happens at Context.synchronize.
Attributes, limits, and error sets
Attribute and Limit are enums of the vendor’s numeric codes: Attribute
has the same tags in std.gpu.cuda and std.gpu.hip, with different numeric
values, so use the tags. Limit is stack_size, printf_fifo_size, and
malloc_heap_size, and HIP’s runtime reports error.UnsupportedLimit for
printf_fifo_size, which is not the buffer that std.gpu.print uses.
OpenError is error{ DriverNotFound, IncompatibleDriver } plus Error.
Beyond the difference in allocation failure, HIP’s set has
EccNotCorrectable, SetOnActiveProcess, and no PTX-JIT or profiler codes;
CUDA’s has EccUncorrectable, PrimaryContextActive, and the PTX and profiler
codes. Both have error.Assert, which is what a kernel assertion turns into at
the next synchronization.
Running and testing on a GPU
test/standalone/gpu
is the end-to-end test: a port of the examples of the
ugpu project, plus kernels that cover the
rest of std.gpu. Its host program computes the expected results on the CPU
and compares them with what the GPU produced, and each of the twenty test
groups — vector_add, reduce, histogram, warp, matrix_mul,
convolution, stencil, stdlib, hashmap, base64, string_search,
json, dynamic, printf, hello_gpu, builtin_math, f128,
parse_float, device_heap, and bump_allocator — reports its launches,
checks, and failures, and exits non-zero if any check fails.
cd test/standalone/gpu
zig build test
The CUDA variant is built on Linux, and the HIP variant on Linux and Windows.
Each kernel image is built in a debug and a fast variant, and the host program
runs once normally and once with assert as an argument, which launches a
kernel that indexes out of bounds and checks that the reported message names
the index, the length, and the failing thread.
The Metal variant is built wherever the suite is. build.zig compiles
metal_kernels.zig for air64-macos with the compiler under test, in a debug
and a fast variant, installs the two containers next to the host program
gpu_metal_host, and runs the host on each of them, so the suite exercises the
air64 target on any host. The host runs a vector add, a reduction whose
partial sums go through threadgroup memory and a device atomic counter, a
kernel with scalar parameters, an index computed with a usize multiply (which
a debug build checks for overflow), the 128-bit products and overflow flags of
64-bit multiplies, the overflow flags of adds and subtracts of every width,
tables of integers and of structs in the constant address
space, a table of strings, @errorName, std.fmt.parseFloat, and allocations
through std.mem.Allocator, checking the results against the CPU bit for bit
and reporting the SIMD width of the kernels;
-Dmetallib=a.metallib,b.metallib adds the containers that Apple’s own metal
compiler or the spike built, to compare Apple’s code with this one’s. A machine
without the Metal framework skips every library it was given, a library without
one of the kernels skips its test, and a run with no library at all skips
everything: all three exit 0. The suite therefore builds its containers and its
host on any host, and only a Mac runs the kernels. On macOS, a framework that
will not open and a library that will not load are failures.
A machine without the driver or without a device is not a failure: the test
reports it and passes, for error.DriverNotFound, error.NoDevice, a driver
that sees no devices, or an AMD code object that matches no device (it suggests
the -Damdgpu-arch value to build for). Everything else fails.
CI runs it as part of the standalone tests:
zig build test-standalone -Dskip-non-native -Dskip-release
CI’s Linux machine runs the CUDA variant on an RTX 5090 Laptop GPU, and its Mac runs the Metal variant on an M4. On a machine with no GPU driver, the suite compiles its kernels for all three vendors and then skips itself.
What is not there yet
- MLIR lowering for tensor cores and kernel fusion.
- Warp shuffles of values wider than 64 bits, and
std.gpu.allocators.device_heapon architectures other than NVPTX, both of which are compile errors. - On Apple GPUs,
f64is a compile error, the atomics are the relaxed ones only,printandassertFailare compile errors, and threadgroup memory must be a static variable sized by the pipeline’s 32 KiB. Metal has no generic address space, so a pointer into constant data (a string literal, a table) that meets a pointer to thread memory is a compile error naming the function, and a checked multiplication ofi128values is supported only where both operands fit in 64 bits. - On Apple GPUs, a table of function pointers, like an allocator’s vtable,
works only where inlining makes the calls through it direct, as it does for
std.heap.FixedBufferAllocatorthroughstd.mem.Allocator. A call through such a table that stays indirect is a compile error that names the table, because Metal has no indirect calls and Apple’s toolchain relocates no address in constant data. - The AIR version, the Metal language version, and the container version of a Metal library are the ones of the macOS release row that the deployment target selects, not a choice of the program.
- On HIP,
ModuleOptions.error_logis ignored, while CUDA’s PTX JIT fills it in when a module fails to load.
Live at Head
Live at Head. Zig++ has no versions. Every push to master is a release, and every release is kept forever. You pin the build you use, the way you pin every dependency, and you upgrade when you are ready. Zig++ does not number its releases; upstream does it for us.
The rules
-
Every push to master is a release. The Release workflow publishes one for every push, whatever the push changes, and no push’s run cancels another’s. Versions and Releases says what a release holds.
-
Builds are forever. Every published build stays downloadable at a stable URL with its checksums. It is never deleted and never replaced: a release is published once, with all of its files, and never changed after. GitHub enforces this for every release published from 26 September 2026 on, which are immutable: their files and their tags cannot be changed. The twelve published before, 0.17.0-dev.2373 to 0.17.0-dev.2469, are kept by the same rule without that lock, because GitHub makes a release immutable only as it is published.
-
Master is append-only. No force pushes, no rewritten history, no retagging. A pin is a commit, and commits do not move. GitHub enforces this too: master refuses a push that is not a fast-forward and refuses to be deleted, and no tag in the repository can be moved or deleted.
-
Version identity comes from upstream tags. Zig++ does not create bare semantic-version tags such as
1.0.0.zigpp-*tags identify individual Zig++ builds and releases. They are immutable artifact locators and are excluded from version derivation.zigpp-*tags never move and never disappear.A build’s version is upstream’s version in development, the number of commits since upstream’s last release tag, and the commit it was built from:
0.17.0-dev.2469+zigpp.04926fc36.build.zigderives it withgit describe --match "*.*.*" --exclude "zigpp-*", so only upstream’s tags take part, and the build tag of that build iszigpp-0.17.0-dev.2469. -
The root project’s pin wins. Dependencies state minimums. A pin older than a dependency’s minimum is an error that names the dependency and the build to move to. The root pin wins today:
zigruns the build that the nearestbuild.zig.zonnames and reads no dependency’s pin. The minimum check is planned: #39. -
Breakage is announced at the upgrade, not by a number. Every commit that breaks existing code carries a
Breaking:trailer saying what breaks and what to write instead:Breaking: std.zig.Server.serveErrorBundle takes the message tag first. Write serveErrorBundle(.error_bundle, bundle).Merges of upstream Zig are the largest source of breakage, so the merge commit carries
Breaking:trailers for upstream’s changes in the merged range. The trailers are in force today. Collecting them into the download index is planned in #37, and printing them when a project upgrades in #38. -
There are no LTS branches. Security fixes land on head, and advisories name the affected range of builds. The advisory format, and a warning on every compile with an affected build, are planned: #40.
-
Docs travel with the pin. The build you run serves its own std docs and language reference.
zig stdserves the standard library documentation of the build that runs it, from that build’s own sources, and from 0.17.0-dev.2476 on it serves that build’s language reference at/langref.htmltoo. Every release archive carries the language reference atdoc/langref.html. -
Upstream Zig and every vendored dependency are tracked continuously and pinned the same way. Upstream Zig is merged into master, never rebased onto, and the libraries Zig++ carries in
lib/come with it. LLVM is pinned by the devkit Zig++ builds against. The packages Zig++ blesses follow the same rule: raylibz pins its raylib by commit, from a fork that merges raylib’s master. -
1.0 is a milestone, not a version. The build that closes it ships like every other build. There is no Zig++ 1.0, and the build system enforces it.
build.zigderives every version from upstream’s tags, so a bare1.0.0tag on master would stop every build from then on: the tagged commit fails with the first error, and every commit after it with the second.error: zig version "0.17.0" does not match Git tag "1.0.0" error: zig version 0.17.0 must be greater than tagged ancestor "1.0.0"
Pinning a build
A project pins the build it is built with in its build.zig.zon:
.minimum_zig_version = "0.17.0-dev.2469+zigpp.04926fc36",
Any zig command in the project then runs that build, downloading it the
first time; see Any Zig Version. To install a build by
hand, ppup install 0.17.0-dev.2469+zigpp.04926fc36, or take its archive from
Downloads and check it against the release’s SHA256SUMS.
Upgrading today
-
Pick a newer build on Downloads, or read
.master.versionfrom /download/index.json. -
Read what broke between your build and that one. The version of each ends in its commit, so in a clone of Zig++:
git log --grep='^Breaking:' --format='%h %s%n%(trailers:key=Breaking,valueonly,separator=%x0A)%n' 04926fc36..70dc1eaec -
Move the pin, build, and fix what the notes name.
zig upgrade will do the first two steps for you. It is planned: #38.
Where the tooling stands
| Rule | Today |
|---|---|
| Every push to master is a release | Enforced by the Release workflow. |
| Builds are forever | Enforced: immutable releases from 26 September 2026 on. |
| Master is append-only | Enforced by the repository’s rules. |
| Version identity comes from upstream tags | Enforced by build.zig and CMakeLists.txt. |
| The root project’s pin wins | Works. Dependency minimums: planned, #39. |
Breaking: trailers | In force. Collected notes and zig upgrade: planned, #37 and #38. |
| No LTS branches, advisories | No LTS branches. Advisories and compile warnings: planned, #40. |
| Docs travel with the pin | Works: std docs always, the language reference from 0.17.0-dev.2476. |
| Upstream and vendored code tracked and pinned | By practice: merges of upstream, a pinned LLVM devkit. |
| 1.0 is a milestone | Enforced by build.zig. |
Credit, and prior art
“Live at Head” is Abseil’s phrase, from Titus Winters’ CppCon 2017 talk “C++ as a ‘Live at Head’ Language”.
- Mach’s nominated Zig versions: every six weeks Mach nominates one nightly Zig for its whole ecosystem to pin.
- Go’s toolchain directive:
go.modnames the toolchain a module builds with, and thegocommand downloads and runs it when it is newer than itself. - Nix’s
flake.lock: every input pinned to a revision and a hash, moved only when you ask. - Dated Rust nightly toolchains:
a
rust-toolchain.tomlnames a nightly by its date, andrustupinstalls exactly that build.
Versions and Releases
What a version says
0.17.0-dev.2361+zigpp.5b96e6d21
└────┬───┘ └─┬─┘ └──────┬──────┘
│ │ └─ build metadata: zigpp, and the commit it was built from
│ └─ commits since the 0.16.0 tag
└─ the upstream version it is based on
The base version, 0.17.0, is upstream’s version in development: build.zig
names it, and it must be greater than the last upstream release this commit
descends from, 0.16.0. That release’s tag is where the commit count starts,
so dev.2361 is 2361 commits after it. The count comes from git describe,
which considers only upstream’s tags, and the commit is its abbreviated hash. A
checkout where git describe cannot answer is built as the plain base version.
Zig++ numbers nothing else; see Live at Head.
The version is what zig version prints, what zig env reports, and what names
the archives of a release. It is also what tells two Zig++ builds apart: unlike
0.17.0-dev, it is impossible for two different Zig++ compilers to report the
same version.
The builder takes -Dversion-string to set it by hand, which the release
workflow does not need: it reads the version out of the compiler it just built.
Rolling releases
The Release workflow publishes a release on every push to master, for four targets:
| Target | Runner that checks it |
|---|---|
| x86_64-linux | ubuntu-24.04 |
| aarch64-linux | ubuntu-24.04-arm |
| aarch64-macos | macos-latest |
| x86_64-windows | windows-2025 |
The jobs are: build a bootstrap compiler (with the newest release, or from
source alone when the release cannot build it) and, with it, the one language
reference every archive carries, pack an archive for each target, unpack and
smoke-test each archive on a runner of that architecture, and publish. The
smoke test runs zig version and zig env, compiles and runs a program that
uses private fields, checks that naming a private field from another file is
rejected, and compiles and runs C with the Clang and libc the archive carries.
Every push to master is a release, including one that changes only the book, and no push’s run cancels another’s: a burst of pushes publishes one release for each of them. Their runs can finish in any order.
A pull request that changes the release machinery builds the archives and checks them, and publishes nothing.
The release’s tag is the version without its build metadata —
zigpp-0.17.0-dev.2361 — because + is legal in a tag but awkward in URLs.
build.zig and CMakeLists.txt exclude zigpp-* tags when they derive the
version, so the tag itself does not change it.
Each release carries:
- one archive per target, named
zig-<arch>-<os>-<version>.tar.xz, or.zipfor Windows; index.json, the download index described below;SHA256SUMS, the SHA-256 of every archive and ofindex.json.
A published release is never changed. The assets go up to a draft, and the draft is published once they are all there, so a run that fails partway leaves a draft that nobody could download: running it again replaces the draft’s files and publishes it. Running it again after it published changes nothing. The newest version is the latest release, whichever run finishes last.
The download index
/download/index.json collects every release in one
document, in the format of
ziglang.org’s own download index, so
tools that read that one can read this one. master is the newest release, and
every release also has a key of its own:
{
"master": {
"version": "0.17.0-dev.2361+zigpp.5b96e6d21",
"date": "2026-09-24",
"x86_64-linux": {
"tarball": "https://github.com/mattneel/zigpp/releases/download/zigpp-0.17.0-dev.2361/zig-x86_64-linux-0.17.0-dev.2361%2Bzigpp.5b96e6d21.tar.xz",
"shasum": "e3b0c442...",
"size": "52345678"
},
"aarch64-linux": { "...": "..." },
"aarch64-macos": { "...": "..." },
"x86_64-windows": { "...": "..." }
},
"0.17.0-dev.2361+zigpp.5b96e6d21": { "...": "same as master" }
}
Each target key holds the URL of its archive, its SHA-256, and its size in
bytes. Every release has the same four in the same place, so a script that
wants the newest Linux build for its architecture reads
.master["x86_64-linux"].tarball and checks it against .shasum.
Because every push to master is a release, master in this index is a real
build, not a nightly snapshot: it is the newest published release.
What the site serves
The standard library documentation at /std/ and the language reference at /langref.html are generated from the newest release, so they describe the compiler you download from Downloads rather than an unreleased master.
Until the first release is published, both are stand-ins that say so, and the Downloads chapter explains what a release will contain.
Any Zig Version
A project can pin the exact compiler it is built with, in its
build.zig.zon:
.minimum_zig_version = "0.15.1",
When it does, every zig command run in that project, or in any directory
below it, runs that version instead of the zig that was invoked: zig build,
zig test, zig env, and zig cc alike. The first time a version is needed,
Zig++ downloads it into the global cache and verifies it; after that, it only
reads the project’s build.zig.zon and starts the installed compiler.
zig init writes the version of the compiler that ran it, so a new project
keeps building with the compiler that made it.
The version names the exact compiler, the way anyzig reads it, not a lower bound. It can be:
| Version | Where it comes from | Verified with |
|---|---|---|
a Zig++ release, 0.17.0-dev.2361+zigpp.5b96e6d21 | the GitHub release zigpp-0.17.0-dev.2361 | the SHA-256 in the release’s index.json |
an upstream Zig release, 0.15.1 | ziglang.org/download | the SHA-256 in upstream’s download index, and the minisign signature |
an upstream dev build, 0.16.0-dev.1234+abcdef012 | ziglang.org/builds | the minisign signature |
Every upstream archive is checked against the minisign signature that ziglang.org publishes next to it, made with the Zig Software Foundation’s key, and the signature has to name that archive. ziglang.org keeps only its recent dev builds, so an archive it no longer has comes from one of the community mirrors, tried in a random order, whose copy has to pass the same check. An archive that fails its check is never installed.
Running a version by hand
zig any 0.15.1 version # run that exact version, installing it if needed
zig any list # the installed versions, one per line
ZIG_ANY=off zig version # the zig that was invoked, whatever the project pins
zig any <version> takes any command after the version, the same way zig
does.
Where versions go
Each version lives in <global cache>/any/<version>/, next to the packages
that zig fetch downloads; zig env prints the global cache directory. An
install downloads and unpacks in a temporary directory and moves the result
into place in one step, so a version is either complete or absent, even if the
download is interrupted. Two zig commands that need the same new version at
the same time end up with one install of it.
Deleting a version’s directory uninstalls it.
What the other version sees
The other version runs with the same arguments and the same environment,
except that ZIG_LIB_DIR is removed: it points at the standard library of the
zig that was invoked.
A compiler that runs from <global cache>/any/<version>/ never dispatches to
that version again, so an installed Zig++ does not restart itself. Every other
zig still dispatches: a build step that runs zig from PATH gets the pinned
version too, and a project nested inside another one, pinning a different
version, gets its own. zig any <version> starts its compiler with
ZIG_ANY=off, so the version asked for runs even inside a project that pins
another one.
If no source has the version, zig says what it tried, and that ZIG_ANY=off
runs the compiler that was invoked instead.
A project created by a Zig++ that was never released, such as one you built
from source, pins a version that has nothing to download. Build it with
ZIG_ANY=off, or change minimum_zig_version to a released version.
Blessed Packages
Zig++ blesses one way to use a C library from Zig++: fetch the library at a pinned commit, build it with its own build script, translate its headers with translate-c, and put a Zig face on the translation. A blessed package is a package made that way, which Zig++ maintains and tests. The first is raylibz.
raylibz
raylibz is the Zig++-sanctioned wrapper for raylib, the library for making games by Ramon Santamaria (raysan). Thank you to raysan and raylib’s contributors, who have given raylib away under the zlib license since 2013. If raylibz gets you a window, please consider sponsoring raysan.
raylibz is a thin, one-to-one wrapper. Every function of raylib.h has
raylib’s name in Zig case, InitWindow is initWindow, and raymath’s
functions are methods on the vector and matrix types: Vector2Add(a, b) is
a.add(b). The C declarations it wraps are raylib’s own translation, and they
are always there as raylibz.c. raylib’s
cheatsheet is raylibz’s
documentation too.
A window, from nothing
These steps take a machine with nothing on it to a raylib window, on Linux, macOS or Windows.
-
Install Zig++. On Linux and macOS:
curl -fsSL https://zigpp.lol/ppup | shOn Windows, in PowerShell:
irm https://zigpp.lol/ppup.ps1 | iexThen open a new terminal, so that
zigis on yourPATH. See Installing. -
On Linux, install the headers raylib’s window layer builds against. On Debian and Ubuntu:
sudo apt install libx11-dev libxcursor-dev libxrandr-dev libxinerama-dev libxi-dev libxext-dev libxfixes-dev libgl-devmacOS and Windows need nothing more.
-
Make a project.
mkdir hello-raylib cd hello-raylib zig initzig initwritesbuild.zig,build.zig.zonandsrc/. Thebuild.zig.zonit writes pins the Zig++ build that made it, inminimum_zig_version, so the project keeps building with that build whicheverzigruns it. See Any Zig Version. -
Fetch raylibz, pinned to a commit. raylibz has no tags. Pick a commit; the newest on main is a good one:
zig fetch --save https://github.com/mattneel/raylibz/archive/bc0ec551a3e7de06739c60dd67b1760e9f432870.tar.gzThat records the URL, and the hash of what it downloaded, in
build.zig.zonunder the nameraylibz. -
Give your program the module. In
build.zig, after the line that startsconst exe = b.addExecutable(, and after the call it opens has closed, add:const raylibz = b.dependency("raylibz", .{ .target = target, .optimize = optimize }); exe.root_module.addImport("raylibz", raylibz.module("raylibz")); -
Write the program. Replace everything in
src/main.zigwith:const rl = @import("raylibz"); pub fn main() void { rl.initWindow(800, 450, "hello, raylib"); defer rl.closeWindow(); rl.setTargetFPS(60); while (!rl.windowShouldClose()) { rl.beginDrawing(); defer rl.endDrawing(); rl.clearBackground(rl.Color.raywhite); rl.drawText("Congrats! You created your first window!", 190, 200, 20, rl.Color.lightgray); } } -
Run it.
zig build runThe first build takes a minute or more, because it compiles Zig++’s build runner and raylib. A window opens; close it, or press Escape.
What just happened
zig fetchdownloaded raylibz.zig buildthen fetched raylibz’s own dependency, raylib: mattneel/raylib, raysan’s raylib with its build scripts ported to Zig++’s build API and one fix in the gamepad library it vendors, following raylib’s master.- raylib’s own
build.zigcompiled raylib’s C with the Clang inside Zig++, and chose raylib’s window layer: GLFW, on X11 on Linux. Every one of raylib’s build options passes through raylibz unchanged: add.platform = .rgfwto theb.dependencycall from step 5, and the program runs on RGFW instead. - The same build script ran translate-c over
raylib.h,raymath.handrlgl.h. Zig++’s translator reads C headers and writes Zig declarations for them, and raylib’s build script publishes those as modules. - raylibz imported the translations and put the Zig face on them:
initWindowtakes a string literal as it is,Color.raywhiteis a Zig value, andloadTexturereturnserror{LoadFailed}!Texture2D, where C returns a texture you must remember to check. raylibz’s tests prove the face against the translation when it compiles: every struct’s layout, every enum value, and that no raylib function is missing. - raylib’s own declarations stay one step away:
rl.c.InitWindow(800, 450, "hello")calls the translation directly.
Your own C library
A library whose build script does not translate its headers gets the same
treatment from your build.zig:
const mylib_h = b.addTranslateC(.{
.root_source_file = b.path("vendor/mylib/include/mylib.h"),
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("mylib", mylib_h.createModule());
@import("mylib") is then the translated header. Link the library itself as
its build script produces it, for example with exe.root_module.linkLibrary
on the artifact of its package, and wrap what you use. raylibz’s
source shows each of its rules at work.
Downloads
Zig++ publishes a release on every push to master, for x86_64-linux, aarch64-linux, aarch64-macos, and x86_64-windows. The newest is 0.17.0-dev.2531+zigpp.5f8c0c6bc, published 2026-09-27 05:12 UTC.
What a release contains
An archive is named zig-<arch>-<os>-<version>.tar.xz, or .zip on Windows. It holds the
zig executable, lib/, doc/langref.html, LICENSE, and README.md. The
Installing chapter covers how the compiler finds the lib/ it carries.
SHA256SUMS lists the SHA-256 of every archive and of index.json.
0.17.0-dev.2531+zigpp.5f8c0c6bc
2026-09-27 05:12 UTC · zigpp-0.17.0-dev.2531
| Target | Archive | Size |
|---|---|---|
| x86_64-linux | zig-x86_64-linux-0.17.0-dev.2531+zigpp.5f8c0c6bc.tar.xz | 60.1 MiB |
| aarch64-linux | zig-aarch64-linux-0.17.0-dev.2531+zigpp.5f8c0c6bc.tar.xz | 55.3 MiB |
| aarch64-macos | zig-aarch64-macos-0.17.0-dev.2531+zigpp.5f8c0c6bc.tar.xz | 56.3 MiB |
| x86_64-windows | zig-x86_64-windows-0.17.0-dev.2531+zigpp.5f8c0c6bc.zip | 102.2 MiB |
SHA256SUMS:
d61b0353aeeb6ea72bb6598de5dfc1c7a6882e490a6c842a1f9e4aad713370f9 zig-aarch64-linux-0.17.0-dev.2531+zigpp.5f8c0c6bc.tar.xz
af610074761d49e83c4dd6244f931737b44fd97c4414c8eb10528d3b1c9ac56b zig-aarch64-macos-0.17.0-dev.2531+zigpp.5f8c0c6bc.tar.xz
69552cda2ebfbcb3456eb529973c4388a57a31254edcb5029089ac7b0232b7e3 zig-x86_64-linux-0.17.0-dev.2531+zigpp.5f8c0c6bc.tar.xz
3a8ef82747b494efb8dc790b94276e89d2a8f2a3afae642476e121e3bc59e137 zig-x86_64-windows-0.17.0-dev.2531+zigpp.5f8c0c6bc.zip
0.17.0-dev.2529+zigpp.5ebb0ae8a
2026-09-27 04:45 UTC · zigpp-0.17.0-dev.2529
| Target | Archive | Size |
|---|---|---|
| x86_64-linux | zig-x86_64-linux-0.17.0-dev.2529+zigpp.5ebb0ae8a.tar.xz | 60.1 MiB |
| aarch64-linux | zig-aarch64-linux-0.17.0-dev.2529+zigpp.5ebb0ae8a.tar.xz | 55.3 MiB |
| aarch64-macos | zig-aarch64-macos-0.17.0-dev.2529+zigpp.5ebb0ae8a.tar.xz | 56.3 MiB |
| x86_64-windows | zig-x86_64-windows-0.17.0-dev.2529+zigpp.5ebb0ae8a.zip | 102.2 MiB |
SHA256SUMS:
537f62c35e5d47b9abc8143872ce210f7f43bb2338cce6af0d8dc76fe1d25a48 zig-aarch64-linux-0.17.0-dev.2529+zigpp.5ebb0ae8a.tar.xz
f525c98ad2b2d1b5ba1f59fb6b1c9945c15fab5e5d1fbda3374d0d0efae8a3f4 zig-aarch64-macos-0.17.0-dev.2529+zigpp.5ebb0ae8a.tar.xz
735efd6b610c7be6b71d96fcaa2ebab9deb213e8923233885875d0761804f50e zig-x86_64-linux-0.17.0-dev.2529+zigpp.5ebb0ae8a.tar.xz
3318919a59658c94c3c2cbeb69a2f0399a62b32970e9bb3a0b85ae143fb85ebe zig-x86_64-windows-0.17.0-dev.2529+zigpp.5ebb0ae8a.zip
0.17.0-dev.2527+zigpp.959fb520b
2026-09-27 03:59 UTC · zigpp-0.17.0-dev.2527
| Target | Archive | Size |
|---|---|---|
| x86_64-linux | zig-x86_64-linux-0.17.0-dev.2527+zigpp.959fb520b.tar.xz | 60.1 MiB |
| aarch64-linux | zig-aarch64-linux-0.17.0-dev.2527+zigpp.959fb520b.tar.xz | 55.3 MiB |
| aarch64-macos | zig-aarch64-macos-0.17.0-dev.2527+zigpp.959fb520b.tar.xz | 56.3 MiB |
| x86_64-windows | zig-x86_64-windows-0.17.0-dev.2527+zigpp.959fb520b.zip | 102.2 MiB |
SHA256SUMS:
289eed2dcc9280ac38c136e6996ba031990bb1895ba41cd16ceb4b32ed600916 zig-aarch64-linux-0.17.0-dev.2527+zigpp.959fb520b.tar.xz
00506a4997c5d12d09ce749ba5d798943cf437d589d118f8278a35250fa21ead zig-aarch64-macos-0.17.0-dev.2527+zigpp.959fb520b.tar.xz
0c662698955ab16fb2650449460b02930e13f313f9a993579c66e75b912f55aa zig-x86_64-linux-0.17.0-dev.2527+zigpp.959fb520b.tar.xz
4be5205a7f367a653d2afc705ecd913903055905be085581533233e9d460d75b zig-x86_64-windows-0.17.0-dev.2527+zigpp.959fb520b.zip
0.17.0-dev.2525+zigpp.61e078103
2026-09-27 03:57 UTC · zigpp-0.17.0-dev.2525
| Target | Archive | Size |
|---|---|---|
| x86_64-linux | zig-x86_64-linux-0.17.0-dev.2525+zigpp.61e078103.tar.xz | 60.1 MiB |
| aarch64-linux | zig-aarch64-linux-0.17.0-dev.2525+zigpp.61e078103.tar.xz | 55.3 MiB |
| aarch64-macos | zig-aarch64-macos-0.17.0-dev.2525+zigpp.61e078103.tar.xz | 56.3 MiB |
| x86_64-windows | zig-x86_64-windows-0.17.0-dev.2525+zigpp.61e078103.zip | 102.2 MiB |
SHA256SUMS:
bb861e262dbde6e709ef63253b019141623006b137df9af965b26b924c7e94c8 zig-aarch64-linux-0.17.0-dev.2525+zigpp.61e078103.tar.xz
86532c6e3411a2791e096fb5f227f83b1d089d5a527d9748c1b3c3d89d037ba1 zig-aarch64-macos-0.17.0-dev.2525+zigpp.61e078103.tar.xz
8072e412d2d2608e49f1101014361ff81a45aaef588e833a589d81f729fd55c9 zig-x86_64-linux-0.17.0-dev.2525+zigpp.61e078103.tar.xz
1a6231345805a823c1b7e202dc1dcad796b9f22519531c507c3826ee57864951 zig-x86_64-windows-0.17.0-dev.2525+zigpp.61e078103.zip
0.17.0-dev.2514+zigpp.d1be2e3f0
2026-09-27 03:11 UTC · zigpp-0.17.0-dev.2514
| Target | Archive | Size |
|---|---|---|
| x86_64-linux | zig-x86_64-linux-0.17.0-dev.2514+zigpp.d1be2e3f0.tar.xz | 60.1 MiB |
| aarch64-linux | zig-aarch64-linux-0.17.0-dev.2514+zigpp.d1be2e3f0.tar.xz | 55.3 MiB |
| aarch64-macos | zig-aarch64-macos-0.17.0-dev.2514+zigpp.d1be2e3f0.tar.xz | 56.2 MiB |
| x86_64-windows | zig-x86_64-windows-0.17.0-dev.2514+zigpp.d1be2e3f0.zip | 102.2 MiB |
SHA256SUMS:
ebad84f8de57a4a553bff9a543777beaf3100227cd4155ac8d3bd7142d9f7589 zig-aarch64-linux-0.17.0-dev.2514+zigpp.d1be2e3f0.tar.xz
f4c975641b7c2f7d09c55e050f5ee8605b61680e3387d39232cdbdc30835a1ae zig-aarch64-macos-0.17.0-dev.2514+zigpp.d1be2e3f0.tar.xz
89905779d5bc1e1e8984645ac0f2481ae196d9504cc8f6cb2d1f137c5b72f530 zig-x86_64-linux-0.17.0-dev.2514+zigpp.d1be2e3f0.tar.xz
5ff35f0eb2fd124d15310aeacca97354b64d79113f069b9a857152c1e6d7ce93 zig-x86_64-windows-0.17.0-dev.2514+zigpp.d1be2e3f0.zip
0.17.0-dev.2512+zigpp.f7be783f7
2026-09-27 03:08 UTC · zigpp-0.17.0-dev.2512
| Target | Archive | Size |
|---|---|---|
| x86_64-linux | zig-x86_64-linux-0.17.0-dev.2512+zigpp.f7be783f7.tar.xz | 60.1 MiB |
| aarch64-linux | zig-aarch64-linux-0.17.0-dev.2512+zigpp.f7be783f7.tar.xz | 55.3 MiB |
| aarch64-macos | zig-aarch64-macos-0.17.0-dev.2512+zigpp.f7be783f7.tar.xz | 56.2 MiB |
| x86_64-windows | zig-x86_64-windows-0.17.0-dev.2512+zigpp.f7be783f7.zip | 102.2 MiB |
SHA256SUMS:
9ce21f8d71498bb2a8452413ac183fb22f239e90a87329f8cb1632897dd3a8db zig-aarch64-linux-0.17.0-dev.2512+zigpp.f7be783f7.tar.xz
27fef4b41026ffe177297702c991bab38fcc01a36127d04951a9a044d5d0bafe zig-aarch64-macos-0.17.0-dev.2512+zigpp.f7be783f7.tar.xz
1ea04bf32e24b8fdba46d05ad6de71c05f199fea0a1ba087098b33406c27f20f zig-x86_64-linux-0.17.0-dev.2512+zigpp.f7be783f7.tar.xz
3081568fdf722276fccaad56ccfd59ecf86a91f94b506d08eec161efd32fc3ba zig-x86_64-windows-0.17.0-dev.2512+zigpp.f7be783f7.zip
0.17.0-dev.2510+zigpp.60a1816dd
2026-09-26 22:52 UTC · zigpp-0.17.0-dev.2510
| Target | Archive | Size |
|---|---|---|
| x86_64-linux | zig-x86_64-linux-0.17.0-dev.2510+zigpp.60a1816dd.tar.xz | 60.1 MiB |
| aarch64-linux | zig-aarch64-linux-0.17.0-dev.2510+zigpp.60a1816dd.tar.xz | 55.3 MiB |
| aarch64-macos | zig-aarch64-macos-0.17.0-dev.2510+zigpp.60a1816dd.tar.xz | 56.2 MiB |
| x86_64-windows | zig-x86_64-windows-0.17.0-dev.2510+zigpp.60a1816dd.zip | 102.2 MiB |
SHA256SUMS:
a2e16786e85bf001740ce77e88d8c1e45d7a0b97c113513a34500260377f7edc zig-aarch64-linux-0.17.0-dev.2510+zigpp.60a1816dd.tar.xz
b84bd66dfb3a2bcdb78f52918e35737de4774b7ed2422a541fd13cbb0acfe77d zig-aarch64-macos-0.17.0-dev.2510+zigpp.60a1816dd.tar.xz
ffb0738c7ecf0957a0e2dffe3e0ab5782895044522efdf32368b6bba413ae7d7 zig-x86_64-linux-0.17.0-dev.2510+zigpp.60a1816dd.tar.xz
afc900c728c9a4e5e94da6e1df583d85da1ae8885604e69ff43eb8cacd26872a zig-x86_64-windows-0.17.0-dev.2510+zigpp.60a1816dd.zip
0.17.0-dev.2508+zigpp.091bb7527
2026-09-26 22:48 UTC · zigpp-0.17.0-dev.2508
| Target | Archive | Size |
|---|---|---|
| x86_64-linux | zig-x86_64-linux-0.17.0-dev.2508+zigpp.091bb7527.tar.xz | 60.1 MiB |
| aarch64-linux | zig-aarch64-linux-0.17.0-dev.2508+zigpp.091bb7527.tar.xz | 55.3 MiB |
| aarch64-macos | zig-aarch64-macos-0.17.0-dev.2508+zigpp.091bb7527.tar.xz | 56.2 MiB |
| x86_64-windows | zig-x86_64-windows-0.17.0-dev.2508+zigpp.091bb7527.zip | 102.2 MiB |
SHA256SUMS:
7007ab5db61d50dbe3d7c9720dbfd51a722632405f40b52fcde6bea2e3fef40b zig-aarch64-linux-0.17.0-dev.2508+zigpp.091bb7527.tar.xz
128791a0a58edf839266f83616a3a262381215407f59b3d1afdd1b9dc6cda8ba zig-aarch64-macos-0.17.0-dev.2508+zigpp.091bb7527.tar.xz
ac9ee21ae0b2aa7f8c910d35699cf792fcdcec64cb45b6ab735de4e59fb94645 zig-x86_64-linux-0.17.0-dev.2508+zigpp.091bb7527.tar.xz
aa511126f817cab0de28e9138a2e1e099f8b1b43f0390724cef5efa5736dceb2 zig-x86_64-windows-0.17.0-dev.2508+zigpp.091bb7527.zip
0.17.0-dev.2506+zigpp.e75228e99
2026-09-26 21:18 UTC · zigpp-0.17.0-dev.2506
| Target | Archive | Size |
|---|---|---|
| x86_64-linux | zig-x86_64-linux-0.17.0-dev.2506+zigpp.e75228e99.tar.xz | 60.1 MiB |
| aarch64-linux | zig-aarch64-linux-0.17.0-dev.2506+zigpp.e75228e99.tar.xz | 55.3 MiB |
| aarch64-macos | zig-aarch64-macos-0.17.0-dev.2506+zigpp.e75228e99.tar.xz | 56.2 MiB |
| x86_64-windows | zig-x86_64-windows-0.17.0-dev.2506+zigpp.e75228e99.zip | 102.2 MiB |
SHA256SUMS:
51459fd3ccd426d0463d8fa54c5481d04f6a091c7a0564af85d531c10d4d9c51 zig-aarch64-linux-0.17.0-dev.2506+zigpp.e75228e99.tar.xz
549136e9bb47172ef03fff0e85e3649ba1709aa2a395c84a0ade2941009283d4 zig-aarch64-macos-0.17.0-dev.2506+zigpp.e75228e99.tar.xz
848c3836716135246d5990be0e9aaa620d030972f58b348c26f5492793793204 zig-x86_64-linux-0.17.0-dev.2506+zigpp.e75228e99.tar.xz
c3b31abf8760471b7c7a337c79925f68ee2d9a80152dba9699cea1fb3bcced02 zig-x86_64-windows-0.17.0-dev.2506+zigpp.e75228e99.zip
0.17.0-dev.2491+zigpp.e03a07b67
2026-09-26 17:45 UTC · zigpp-0.17.0-dev.2491
| Target | Archive | Size |
|---|---|---|
| x86_64-linux | zig-x86_64-linux-0.17.0-dev.2491+zigpp.e03a07b67.tar.xz | 60.1 MiB |
| aarch64-linux | zig-aarch64-linux-0.17.0-dev.2491+zigpp.e03a07b67.tar.xz | 55.3 MiB |
| aarch64-macos | zig-aarch64-macos-0.17.0-dev.2491+zigpp.e03a07b67.tar.xz | 56.2 MiB |
| x86_64-windows | zig-x86_64-windows-0.17.0-dev.2491+zigpp.e03a07b67.zip | 102.2 MiB |
SHA256SUMS:
d403d43a210e9b34ebcf4ee36d7eb3478493dc61fbe3afd18a5590ed168f3b8b zig-aarch64-linux-0.17.0-dev.2491+zigpp.e03a07b67.tar.xz
f6043e99d24e94170baa5b2e487e3ab7c61bec1ede15c4441cc031c4816c015b zig-aarch64-macos-0.17.0-dev.2491+zigpp.e03a07b67.tar.xz
d11e037b2f204fc9e0b637e06cdb2c6d2fb79015becb8a978448e78c80882256 zig-x86_64-linux-0.17.0-dev.2491+zigpp.e03a07b67.tar.xz
dbde986ca8a07417a3c1c3b91d0aae70b13a168eea5260830eab07900b6edb76 zig-x86_64-windows-0.17.0-dev.2491+zigpp.e03a07b67.zip
0.17.0-dev.2489+zigpp.255932694
2026-09-26 17:21 UTC · zigpp-0.17.0-dev.2489
| Target | Archive | Size |
|---|---|---|
| x86_64-linux | zig-x86_64-linux-0.17.0-dev.2489+zigpp.255932694.tar.xz | 60.0 MiB |
| aarch64-linux | zig-aarch64-linux-0.17.0-dev.2489+zigpp.255932694.tar.xz | 55.3 MiB |
| aarch64-macos | zig-aarch64-macos-0.17.0-dev.2489+zigpp.255932694.tar.xz | 56.2 MiB |
| x86_64-windows | zig-x86_64-windows-0.17.0-dev.2489+zigpp.255932694.zip | 102.2 MiB |
SHA256SUMS:
156cf2d9bf7ca2e58074df97805ffad059848760f53d762acef2d6ea9bd899fc zig-aarch64-linux-0.17.0-dev.2489+zigpp.255932694.tar.xz
1ab23d136e18bfecd88f03be41031a63d256f81392625c3fa4c1fe47c88a4655 zig-aarch64-macos-0.17.0-dev.2489+zigpp.255932694.tar.xz
1a48b18a032cfacb91a4b737ba9ee4d16b5862a5a3c04e046bbf5065303645bf zig-x86_64-linux-0.17.0-dev.2489+zigpp.255932694.tar.xz
56d017354a36e2b0c44c3321421b8adc63d7cfb63bb93cb7a6587d86469dbd74 zig-x86_64-windows-0.17.0-dev.2489+zigpp.255932694.zip
0.17.0-dev.2487+zigpp.cd699eced
2026-09-26 17:15 UTC · zigpp-0.17.0-dev.2487
| Target | Archive | Size |
|---|---|---|
| x86_64-linux | zig-x86_64-linux-0.17.0-dev.2487+zigpp.cd699eced.tar.xz | 60.1 MiB |
| aarch64-linux | zig-aarch64-linux-0.17.0-dev.2487+zigpp.cd699eced.tar.xz | 55.3 MiB |
| aarch64-macos | zig-aarch64-macos-0.17.0-dev.2487+zigpp.cd699eced.tar.xz | 56.2 MiB |
| x86_64-windows | zig-x86_64-windows-0.17.0-dev.2487+zigpp.cd699eced.zip | 102.2 MiB |
SHA256SUMS:
59b465536056118b20fef7b8bba5ce92a5b37b8852f5f35104754142d3b93cc7 zig-aarch64-linux-0.17.0-dev.2487+zigpp.cd699eced.tar.xz
02bbead55739159af50522b2176ceb407321411559e6526b86806a7b1c3dbc50 zig-aarch64-macos-0.17.0-dev.2487+zigpp.cd699eced.tar.xz
46713161033959e52911589d6bd6269b86c7ff99f5d18560119e3734a238b30f zig-x86_64-linux-0.17.0-dev.2487+zigpp.cd699eced.tar.xz
d67b6c6d56769fac22f5f339108e08780cdaf33f2b2ae3f06c950129af2b3891 zig-x86_64-windows-0.17.0-dev.2487+zigpp.cd699eced.zip
0.17.0-dev.2485+zigpp.753de4bb5
2026-09-26 16:44 UTC · zigpp-0.17.0-dev.2485
| Target | Archive | Size |
|---|---|---|
| x86_64-linux | zig-x86_64-linux-0.17.0-dev.2485+zigpp.753de4bb5.tar.xz | 60.1 MiB |
| aarch64-linux | zig-aarch64-linux-0.17.0-dev.2485+zigpp.753de4bb5.tar.xz | 55.3 MiB |
| aarch64-macos | zig-aarch64-macos-0.17.0-dev.2485+zigpp.753de4bb5.tar.xz | 56.2 MiB |
| x86_64-windows | zig-x86_64-windows-0.17.0-dev.2485+zigpp.753de4bb5.zip | 102.2 MiB |
SHA256SUMS:
e22f6e4f47aafc16591de7f4b89c8855acf36225485f8a5f5409fe30bfd10a6b zig-aarch64-linux-0.17.0-dev.2485+zigpp.753de4bb5.tar.xz
6bcbb5fe1682cdbf8fea092baeb08fad1de7bb6448b09d0cc4f0ab7cb7b18449 zig-aarch64-macos-0.17.0-dev.2485+zigpp.753de4bb5.tar.xz
89d8cd11d5233e2c993c4cef9f23e8469070954028cd707b119e2ea5494dc081 zig-x86_64-linux-0.17.0-dev.2485+zigpp.753de4bb5.tar.xz
6f8b609a2df584257f5f1bd9c572a3866a57def4617a74e4dcc4c2c40014c60f zig-x86_64-windows-0.17.0-dev.2485+zigpp.753de4bb5.zip
0.17.0-dev.2483+zigpp.0099b5739
2026-09-26 16:23 UTC · zigpp-0.17.0-dev.2483
| Target | Archive | Size |
|---|---|---|
| x86_64-linux | zig-x86_64-linux-0.17.0-dev.2483+zigpp.0099b5739.tar.xz | 60.1 MiB |
| aarch64-linux | zig-aarch64-linux-0.17.0-dev.2483+zigpp.0099b5739.tar.xz | 55.3 MiB |
| aarch64-macos | zig-aarch64-macos-0.17.0-dev.2483+zigpp.0099b5739.tar.xz | 56.2 MiB |
| x86_64-windows | zig-x86_64-windows-0.17.0-dev.2483+zigpp.0099b5739.zip | 102.2 MiB |
SHA256SUMS:
2ff51a34e2013c609b9364378e29ea2cf6bab2e6cd8e3f6388b8559c137e143c zig-aarch64-linux-0.17.0-dev.2483+zigpp.0099b5739.tar.xz
27e0cc33a716d3644275d48687003618b331915ef28769989ad45e9cf643e789 zig-aarch64-macos-0.17.0-dev.2483+zigpp.0099b5739.tar.xz
fe6b4859a1987b6a4db157d9eb9ffe4a4318c0f62d99dc4e772b5cf3b1eef0f5 zig-x86_64-linux-0.17.0-dev.2483+zigpp.0099b5739.tar.xz
daca824c0a6190ad33ad0287a74896a7c1c229ad681192a360d624c612d058dc zig-x86_64-windows-0.17.0-dev.2483+zigpp.0099b5739.zip
0.17.0-dev.2480+zigpp.e95c60250
2026-09-26 15:12 UTC · zigpp-0.17.0-dev.2480
| Target | Archive | Size |
|---|---|---|
| x86_64-linux | zig-x86_64-linux-0.17.0-dev.2480+zigpp.e95c60250.tar.xz | 60.1 MiB |
| aarch64-linux | zig-aarch64-linux-0.17.0-dev.2480+zigpp.e95c60250.tar.xz | 55.3 MiB |
| aarch64-macos | zig-aarch64-macos-0.17.0-dev.2480+zigpp.e95c60250.tar.xz | 56.2 MiB |
| x86_64-windows | zig-x86_64-windows-0.17.0-dev.2480+zigpp.e95c60250.zip | 102.2 MiB |
SHA256SUMS:
2573bbd092f2e18364d55f76f06d82722efbb2869a859c0c74029a8782486e5e zig-aarch64-linux-0.17.0-dev.2480+zigpp.e95c60250.tar.xz
4bb98576693c4aa6e540e28834d3bdb3ada59137574e329565b61ade17d7f9e2 zig-aarch64-macos-0.17.0-dev.2480+zigpp.e95c60250.tar.xz
f1f6704830aa22fc7b6b89b34b7d2e500a2843bcebe39dd4f2718401d730171a zig-x86_64-linux-0.17.0-dev.2480+zigpp.e95c60250.tar.xz
1ac2608316fa95182cebab36ac00420beeca6fc633a6f857ee91894557602084 zig-x86_64-windows-0.17.0-dev.2480+zigpp.e95c60250.zip
0.17.0-dev.2478+zigpp.f682d9860
2026-09-26 14:38 UTC · zigpp-0.17.0-dev.2478
| Target | Archive | Size |
|---|---|---|
| x86_64-linux | zig-x86_64-linux-0.17.0-dev.2478+zigpp.f682d9860.tar.xz | 60.0 MiB |
| aarch64-linux | zig-aarch64-linux-0.17.0-dev.2478+zigpp.f682d9860.tar.xz | 55.3 MiB |
| aarch64-macos | zig-aarch64-macos-0.17.0-dev.2478+zigpp.f682d9860.tar.xz | 56.2 MiB |
| x86_64-windows | zig-x86_64-windows-0.17.0-dev.2478+zigpp.f682d9860.zip | 102.2 MiB |
SHA256SUMS:
3d846e8074190e94e2021c68087c859be37b9fd99fc8e5567d515aa2bc885910 zig-aarch64-linux-0.17.0-dev.2478+zigpp.f682d9860.tar.xz
ab2fca50c3ae3796756eab311cb5622d3457e552ef5cc5fa39e77653f58366c8 zig-aarch64-macos-0.17.0-dev.2478+zigpp.f682d9860.tar.xz
49a27d4aa67e3d57e224cd1668d86115c02fad70ae7d4c3d058cc1cdd7d52d52 zig-x86_64-linux-0.17.0-dev.2478+zigpp.f682d9860.tar.xz
75460135aed989ed11ceeecad0c0a2f7e64765e6656779a8d4e56d4e5b33fccc zig-x86_64-windows-0.17.0-dev.2478+zigpp.f682d9860.zip
0.17.0-dev.2476+zigpp.70dc1eaec
2026-09-26 14:30 UTC · zigpp-0.17.0-dev.2476
| Target | Archive | Size |
|---|---|---|
| x86_64-linux | zig-x86_64-linux-0.17.0-dev.2476+zigpp.70dc1eaec.tar.xz | 60.0 MiB |
| aarch64-linux | zig-aarch64-linux-0.17.0-dev.2476+zigpp.70dc1eaec.tar.xz | 55.3 MiB |
| aarch64-macos | zig-aarch64-macos-0.17.0-dev.2476+zigpp.70dc1eaec.tar.xz | 56.2 MiB |
| x86_64-windows | zig-x86_64-windows-0.17.0-dev.2476+zigpp.70dc1eaec.zip | 102.2 MiB |
SHA256SUMS:
9f834359b8a1f2c875e9e4afead153b76950c2ea8455782cecdb1fa0a13f306c zig-aarch64-linux-0.17.0-dev.2476+zigpp.70dc1eaec.tar.xz
1a5520bf6d7e003f1628903cff0f50cb9438822b92603dde5b397ce644fcf92e zig-aarch64-macos-0.17.0-dev.2476+zigpp.70dc1eaec.tar.xz
c53769d84f336d4c7c1c6fa6f5513b27639a2cae899ac7e68cd8ea7e256d6bff zig-x86_64-linux-0.17.0-dev.2476+zigpp.70dc1eaec.tar.xz
2a76f8f57ec8ec71a0093b37a8de71f62edd68d16669027fb47ed7802436abfa zig-x86_64-windows-0.17.0-dev.2476+zigpp.70dc1eaec.zip
0.17.0-dev.2469+zigpp.04926fc36
2026-09-26 11:42 UTC · zigpp-0.17.0-dev.2469
| Target | Archive | Size |
|---|---|---|
| x86_64-linux | zig-x86_64-linux-0.17.0-dev.2469+zigpp.04926fc36.tar.xz | 60.1 MiB |
| aarch64-linux | zig-aarch64-linux-0.17.0-dev.2469+zigpp.04926fc36.tar.xz | 55.3 MiB |
| aarch64-macos | zig-aarch64-macos-0.17.0-dev.2469+zigpp.04926fc36.tar.xz | 56.2 MiB |
| x86_64-windows | zig-x86_64-windows-0.17.0-dev.2469+zigpp.04926fc36.zip | 102.2 MiB |
SHA256SUMS:
89618cdb462023e104aab3b865060befcad1a3b245e679007376fda41b0d1a90 zig-aarch64-linux-0.17.0-dev.2469+zigpp.04926fc36.tar.xz
c16f854925e182185b525af7697fa4c58332faa267e734422e7dd6e59d1d3e8b zig-aarch64-macos-0.17.0-dev.2469+zigpp.04926fc36.tar.xz
e38e965cadc80e2f6f59ac389da37f954e1a1cb028951c0991911482062d7358 zig-x86_64-linux-0.17.0-dev.2469+zigpp.04926fc36.tar.xz
97000323c96f6d42cea4cab121f21d7d503bb2efb5c4a3d6f4cd1c7f00d64a39 zig-x86_64-windows-0.17.0-dev.2469+zigpp.04926fc36.zip
0.17.0-dev.2453+zigpp.35608841b
2026-09-26 09:55 UTC · zigpp-0.17.0-dev.2453
| Target | Archive | Size |
|---|---|---|
| x86_64-linux | zig-x86_64-linux-0.17.0-dev.2453+zigpp.35608841b.tar.xz | 60.0 MiB |
| aarch64-linux | zig-aarch64-linux-0.17.0-dev.2453+zigpp.35608841b.tar.xz | 55.3 MiB |
| aarch64-macos | zig-aarch64-macos-0.17.0-dev.2453+zigpp.35608841b.tar.xz | 56.2 MiB |
| x86_64-windows | zig-x86_64-windows-0.17.0-dev.2453+zigpp.35608841b.zip | 102.2 MiB |
SHA256SUMS:
1803b48015b5e2a3b0e8ef7198ad1bae17dd65228795a16e9d1c4626a29dfc68 zig-aarch64-linux-0.17.0-dev.2453+zigpp.35608841b.tar.xz
f135169c6c414c7f5cf6c37a16e345bc12db1232797b086d8cd8350c11c28bc3 zig-aarch64-macos-0.17.0-dev.2453+zigpp.35608841b.tar.xz
e978b8678e249781116962fd062d264ffbf59bd5158bd34569c21ca86d781ab4 zig-x86_64-linux-0.17.0-dev.2453+zigpp.35608841b.tar.xz
4f4344a29cf0df4fe6f5b7201e15f2c886f5f00c7551ac0bf40e1a667d08d78f zig-x86_64-windows-0.17.0-dev.2453+zigpp.35608841b.zip
0.17.0-dev.2451+zigpp.628313d7c
2026-09-26 02:09 UTC · zigpp-0.17.0-dev.2451
| Target | Archive | Size |
|---|---|---|
| x86_64-linux | zig-x86_64-linux-0.17.0-dev.2451+zigpp.628313d7c.tar.xz | 60.0 MiB |
| aarch64-linux | zig-aarch64-linux-0.17.0-dev.2451+zigpp.628313d7c.tar.xz | 55.3 MiB |
| aarch64-macos | zig-aarch64-macos-0.17.0-dev.2451+zigpp.628313d7c.tar.xz | 56.2 MiB |
| x86_64-windows | zig-x86_64-windows-0.17.0-dev.2451+zigpp.628313d7c.zip | 102.2 MiB |
SHA256SUMS:
5a2c2862904c64516cf69f835d9219edeeafc3eda0bf98377341f70714098046 zig-aarch64-linux-0.17.0-dev.2451+zigpp.628313d7c.tar.xz
090525146e849e4ff997b791bdf6a60c055d4e6b83a32957626927a38ea8da37 zig-aarch64-macos-0.17.0-dev.2451+zigpp.628313d7c.tar.xz
ac5baed5472c592ba7f2e15ef79be7603c4921eeb9b569c1132716b32fdc4526 zig-x86_64-linux-0.17.0-dev.2451+zigpp.628313d7c.tar.xz
e1c245f1c16d4b57fc6e47c372e6275569e7d66f5dd6d26421b0729c2800badc zig-x86_64-windows-0.17.0-dev.2451+zigpp.628313d7c.zip
0.17.0-dev.2445+zigpp.ee991009e
2026-09-25 23:05 UTC · zigpp-0.17.0-dev.2445
| Target | Archive | Size |
|---|---|---|
| x86_64-linux | zig-x86_64-linux-0.17.0-dev.2445+zigpp.ee991009e.tar.xz | 60.0 MiB |
| aarch64-linux | zig-aarch64-linux-0.17.0-dev.2445+zigpp.ee991009e.tar.xz | 55.2 MiB |
| aarch64-macos | zig-aarch64-macos-0.17.0-dev.2445+zigpp.ee991009e.tar.xz | 56.2 MiB |
| x86_64-windows | zig-x86_64-windows-0.17.0-dev.2445+zigpp.ee991009e.zip | 102.1 MiB |
SHA256SUMS:
2a5a322700be01a4104a83bb90a3e63804ba30eda14aeec4419b771b18041b34 zig-aarch64-linux-0.17.0-dev.2445+zigpp.ee991009e.tar.xz
6ee9d5d07d17e2dd5733723aa565a781b0b37213a5bc011cfb2538d90e59a4ed zig-aarch64-macos-0.17.0-dev.2445+zigpp.ee991009e.tar.xz
ce55a1e171411749d57434a336149075424619c8b97b95c55a137c3223950f72 zig-x86_64-linux-0.17.0-dev.2445+zigpp.ee991009e.tar.xz
c057333fd59aa154b3772ee879c6bb32edc647e02f1780d75cfdc3b3f420f06b zig-x86_64-windows-0.17.0-dev.2445+zigpp.ee991009e.zip
0.17.0-dev.2439+zigpp.5da715b14
2026-09-25 18:52 UTC · zigpp-0.17.0-dev.2439
| Target | Archive | Size |
|---|---|---|
| x86_64-linux | zig-x86_64-linux-0.17.0-dev.2439+zigpp.5da715b14.tar.xz | 60.0 MiB |
| aarch64-linux | zig-aarch64-linux-0.17.0-dev.2439+zigpp.5da715b14.tar.xz | 55.3 MiB |
| aarch64-macos | zig-aarch64-macos-0.17.0-dev.2439+zigpp.5da715b14.tar.xz | 56.2 MiB |
| x86_64-windows | zig-x86_64-windows-0.17.0-dev.2439+zigpp.5da715b14.zip | 102.1 MiB |
SHA256SUMS:
b26a0050741a1ae1122c97945aae4b24f80d4b4e7941504a46b7cd477dd4f22c zig-aarch64-linux-0.17.0-dev.2439+zigpp.5da715b14.tar.xz
88d9274f0041e1a341a619639d572b705ec6bdabf7c8ddcabf65273cbf76031e zig-aarch64-macos-0.17.0-dev.2439+zigpp.5da715b14.tar.xz
25a1d27f7f6b2213a1f626551814eb41245b87c69f7f28553aa2298fed5dcdcb zig-x86_64-linux-0.17.0-dev.2439+zigpp.5da715b14.tar.xz
6984ed83606532fd049504e83332ef8deb728697b40c61e18ba49dd36cc9e9d7 zig-x86_64-windows-0.17.0-dev.2439+zigpp.5da715b14.zip
0.17.0-dev.2435+zigpp.711f09ad4
2026-09-25 16:23 UTC · zigpp-0.17.0-dev.2435
| Target | Archive | Size |
|---|---|---|
| x86_64-linux | zig-x86_64-linux-0.17.0-dev.2435+zigpp.711f09ad4.tar.xz | 60.0 MiB |
| aarch64-linux | zig-aarch64-linux-0.17.0-dev.2435+zigpp.711f09ad4.tar.xz | 55.2 MiB |
| aarch64-macos | zig-aarch64-macos-0.17.0-dev.2435+zigpp.711f09ad4.tar.xz | 56.2 MiB |
| x86_64-windows | zig-x86_64-windows-0.17.0-dev.2435+zigpp.711f09ad4.zip | 102.1 MiB |
SHA256SUMS:
4617ac19114b7166992aa8474e2b3129dfbc259950767bed840a0d28ba0faec3 zig-aarch64-linux-0.17.0-dev.2435+zigpp.711f09ad4.tar.xz
70e8306fda45578d643488b51aa354adcecaf77bf1f6bbe4b4f0171ded1f2348 zig-aarch64-macos-0.17.0-dev.2435+zigpp.711f09ad4.tar.xz
e7833d618ff84edb621a7969a3efc9d17eec91a2b7e3371ffa494c49d5381906 zig-x86_64-linux-0.17.0-dev.2435+zigpp.711f09ad4.tar.xz
0c5547ed494461ed19de46fed6e8e126597bb485d4e82386f86fa0242f760fcb zig-x86_64-windows-0.17.0-dev.2435+zigpp.711f09ad4.zip
0.17.0-dev.2418+zigpp.887649bfd
2026-09-25 07:13 UTC · zigpp-0.17.0-dev.2418
| Target | Archive | Size |
|---|---|---|
| x86_64-linux | zig-x86_64-linux-0.17.0-dev.2418+zigpp.887649bfd.tar.xz | 59.4 MiB |
| aarch64-linux | zig-aarch64-linux-0.17.0-dev.2418+zigpp.887649bfd.tar.xz | 54.5 MiB |
| aarch64-macos | zig-aarch64-macos-0.17.0-dev.2418+zigpp.887649bfd.tar.xz | 55.5 MiB |
| x86_64-windows | zig-x86_64-windows-0.17.0-dev.2418+zigpp.887649bfd.zip | 102.1 MiB |
SHA256SUMS:
1017bef07b89149286470c5590867220ee3aa8d1a5ba323a2e75b611619a36d3 zig-aarch64-linux-0.17.0-dev.2418+zigpp.887649bfd.tar.xz
8a997f00399220bad77f3412478c4251cabda851549ec7eff98f87b329bf59d8 zig-aarch64-macos-0.17.0-dev.2418+zigpp.887649bfd.tar.xz
40da06b12dfff1b8184efcf44d6bfb89eeffdf113a7267f566122090da28d547 zig-x86_64-linux-0.17.0-dev.2418+zigpp.887649bfd.tar.xz
b4a5b55f059865895aa18b390416bae7000135f94b19bed2864f307f884f60e5 zig-x86_64-windows-0.17.0-dev.2418+zigpp.887649bfd.zip
0.17.0-dev.2408+zigpp.ed422e86b
2026-09-25 02:28 UTC · zigpp-0.17.0-dev.2408
| Target | Archive | Size |
|---|---|---|
| x86_64-linux | zig-x86_64-linux-0.17.0-dev.2408+zigpp.ed422e86b.tar.xz | 59.4 MiB |
| aarch64-linux | zig-aarch64-linux-0.17.0-dev.2408+zigpp.ed422e86b.tar.xz | 54.5 MiB |
| aarch64-macos | zig-aarch64-macos-0.17.0-dev.2408+zigpp.ed422e86b.tar.xz | 55.5 MiB |
| x86_64-windows | zig-x86_64-windows-0.17.0-dev.2408+zigpp.ed422e86b.zip | 102.1 MiB |
SHA256SUMS:
31e5f5efafad3b952fe28f190543c815f47edac3f933895c5fd4b8021cc4e1e5 zig-aarch64-linux-0.17.0-dev.2408+zigpp.ed422e86b.tar.xz
a4bc644ed6e02cedc106b48917e5d458fa3b969a2a855764d5342e839baf2a9d zig-aarch64-macos-0.17.0-dev.2408+zigpp.ed422e86b.tar.xz
714f9c798fb61e59cfc8cac3982e3981e40d8b513a6ca9625d997ad1fe7dd9ea zig-x86_64-linux-0.17.0-dev.2408+zigpp.ed422e86b.tar.xz
71470c1ca94de31101aafd07a1f76da9c31dd54f96f9118285e3481966c905f8 zig-x86_64-windows-0.17.0-dev.2408+zigpp.ed422e86b.zip
0.17.0-dev.2391+zigpp.b80ea5342
2026-09-25 00:50 UTC · zigpp-0.17.0-dev.2391
| Target | Archive | Size |
|---|---|---|
| x86_64-linux | zig-x86_64-linux-0.17.0-dev.2391+zigpp.b80ea5342.tar.xz | 59.1 MiB |
| aarch64-linux | zig-aarch64-linux-0.17.0-dev.2391+zigpp.b80ea5342.tar.xz | 54.3 MiB |
| aarch64-macos | zig-aarch64-macos-0.17.0-dev.2391+zigpp.b80ea5342.tar.xz | 55.3 MiB |
| x86_64-windows | zig-x86_64-windows-0.17.0-dev.2391+zigpp.b80ea5342.zip | 101.6 MiB |
SHA256SUMS:
38f00821e59c6f4cb8f7a0ced9c9826513363fff8d3f6dbcddb32a10e437b233 zig-aarch64-linux-0.17.0-dev.2391+zigpp.b80ea5342.tar.xz
a255226202e6d48641ac82390bac4902975d3f008b17c363df72f02f809235cd zig-aarch64-macos-0.17.0-dev.2391+zigpp.b80ea5342.tar.xz
a91b0371aad0a21fe0ecba03d2d6a7984632dccc335d9661bfbed0d6903f7913 zig-x86_64-linux-0.17.0-dev.2391+zigpp.b80ea5342.tar.xz
acf38738088be9f63ad12d0d8180ce1c46245f090b92560f782e95fc83c76b9d zig-x86_64-windows-0.17.0-dev.2391+zigpp.b80ea5342.zip
0.17.0-dev.2383+zigpp.bceb3a816
2026-09-24 22:08 UTC · zigpp-0.17.0-dev.2383
| Target | Archive | Size |
|---|---|---|
| x86_64-linux | zig-x86_64-linux-0.17.0-dev.2383+zigpp.bceb3a816.tar.xz | 59.1 MiB |
| aarch64-linux | zig-aarch64-linux-0.17.0-dev.2383+zigpp.bceb3a816.tar.xz | 54.3 MiB |
| aarch64-macos | zig-aarch64-macos-0.17.0-dev.2383+zigpp.bceb3a816.tar.xz | 55.3 MiB |
| x86_64-windows | zig-x86_64-windows-0.17.0-dev.2383+zigpp.bceb3a816.zip | 101.6 MiB |
SHA256SUMS:
88c6dd087f6040129a19782875dd62ce6d9a3ee1d0c837a41520e9d2b0c270b6 zig-aarch64-linux-0.17.0-dev.2383+zigpp.bceb3a816.tar.xz
3c95e4b6a7796b1f9572256a9843299197e3565c3c59bdab267c9dbf653969a3 zig-aarch64-macos-0.17.0-dev.2383+zigpp.bceb3a816.tar.xz
85b697808c93d9a6a8163b3027b9f3d1ce6af26391822da6e30d0dd64554209a zig-x86_64-linux-0.17.0-dev.2383+zigpp.bceb3a816.tar.xz
e41d124c48248ed78fbbac1cab4250f29524502a23b6d9c20a30f87140b1573d zig-x86_64-windows-0.17.0-dev.2383+zigpp.bceb3a816.zip
0.17.0-dev.2376+zigpp.b926cb810
2026-09-24 21:30 UTC · zigpp-0.17.0-dev.2376
| Target | Archive | Size |
|---|---|---|
| x86_64-linux | zig-x86_64-linux-0.17.0-dev.2376+zigpp.b926cb810.tar.xz | 59.1 MiB |
| aarch64-linux | zig-aarch64-linux-0.17.0-dev.2376+zigpp.b926cb810.tar.xz | 54.3 MiB |
| aarch64-macos | zig-aarch64-macos-0.17.0-dev.2376+zigpp.b926cb810.tar.xz | 55.3 MiB |
| x86_64-windows | zig-x86_64-windows-0.17.0-dev.2376+zigpp.b926cb810.zip | 101.6 MiB |
SHA256SUMS:
4232d06b57059da725db85e44c7269941c58fded3cca93cf5198e0c044402a8c zig-aarch64-linux-0.17.0-dev.2376+zigpp.b926cb810.tar.xz
762a64b811656274d3da9d22b5ecd5c7971b4cedec140e46f26741173d4b28ee zig-aarch64-macos-0.17.0-dev.2376+zigpp.b926cb810.tar.xz
ca7bd086ed96b6cc5ebfee57251fdb736a5a467a557ea913040b7ed9e8fca5f9 zig-x86_64-linux-0.17.0-dev.2376+zigpp.b926cb810.tar.xz
a9e95c3f34f296add9662096ffa09e5b43dd511b58d372bc4b99ec55471584aa zig-x86_64-windows-0.17.0-dev.2376+zigpp.b926cb810.zip
0.17.0-dev.2373+zigpp.add158e97
2026-09-24 20:40 UTC · zigpp-0.17.0-dev.2373
| Target | Archive | Size |
|---|---|---|
| x86_64-linux | zig-x86_64-linux-0.17.0-dev.2373+zigpp.add158e97.tar.xz | 59.1 MiB |
| aarch64-linux | zig-aarch64-linux-0.17.0-dev.2373+zigpp.add158e97.tar.xz | 54.3 MiB |
| aarch64-macos | zig-aarch64-macos-0.17.0-dev.2373+zigpp.add158e97.tar.xz | 55.3 MiB |
| x86_64-windows | zig-x86_64-windows-0.17.0-dev.2373+zigpp.add158e97.zip | 101.5 MiB |
SHA256SUMS:
723baa7b6ac79443b960b600a61f21c9f3881cee83f567e0a5bdbf4d797f7f34 zig-aarch64-linux-0.17.0-dev.2373+zigpp.add158e97.tar.xz
7148a807800498e2a4a4748a1ad802799b589fb79cf9c106d18c87130f8509d2 zig-aarch64-macos-0.17.0-dev.2373+zigpp.add158e97.tar.xz
fc062a67e5ecab871f10d00af08d53bfe6249e837f1fbefda58f0e15ddd0a6cc zig-x86_64-linux-0.17.0-dev.2373+zigpp.add158e97.tar.xz
8f78651bfc9435b77edf3376e9315740b84156543e991746ba0092732741ad1d zig-x86_64-windows-0.17.0-dev.2373+zigpp.add158e97.zip
Verifying a download
sha256sum -c SHA256SUMS
The archives and SHA256SUMS of a release are its assets, so download them into one
directory first. The archives are a Zig++ compiler and its lib/: unpack one anywhere
and run zig from it, with no installation step (see the Installing chapter).
The machine-readable index of every release is at
download/index.json:
master is the newest release, and every release has a key of its own.
Building from Source
Build Zig++ from source when you need a target that has no release, when you are developing the compiler, or when you want to build it against your own LLVM 23 libraries. There are four routes, from the least to the most preparation:
- A Zig++ bootstrap devkit, which brings LLVM, Clang, and LLD with it. This is what Zig++’s own CI and releases use.
- The CMake build, against LLVM, Clang, and LLD 23.x you installed yourself.
- An existing Zig++ compiler, which is the quickest route if you already have one.
bootstrap.c, which needs nothing but a C compiler, and produces a compiler that cannot use the LLVM backend.
Requirements
Whichever route you take, the CMake build needs:
- CMake 3.15 or later;
- a system C/C++ toolchain;
- the LLVM, Clang, and LLD development libraries, version 23.x, built with the same system C/C++ toolchain.
Zig++ accepts LLVM 23.x: the CMake module that finds LLVM rejects anything
older than 23 and anything newer than 24, and it checks that the libraries it
found can target AMDGPU and NVPTX. CMAKE_PREFIX_PATH points the build at a
non-system LLVM:
mkdir build
cd build
cmake .. -DCMAKE_PREFIX_PATH="$HOME/local/llvm23"
make install
This produces stage3/bin/zig, relative to the build directory: the Zig
compiler built by itself.
By default the build prefers the shared LLVM libraries. -DZIG_STATIC=ON
links them statically, which is what releases do; it is not compatible with
glibc. -DZIG_SHARED_LLVM=ON and -DZIG_STATIC_LLVM=ON choose explicitly, and
asking for both at once is an error.
Building against an LLVM you built with Zig
If you have LLVM, Clang, and LLD libraries that were built by Zig, an existing Zig++ compiler can build Zig++ against them:
"$ZIG_PREFIX/zig" build \
-p stage3 \
--search-prefix "$LLVM_PREFIX" \
--zig-lib-dir "lib" \
-Dstatic-llvm
$LLVM_PREFIX is the directory that holds include/llvm/Pass.h and
lib/libLLVMCore.a. The result is stage3/bin/zig.
The compiler that runs the build must be Zig++, not upstream Zig: Zig++
changed std.lang.Type, so an upstream Zig binary cannot compile against it.
Upstream’s zig-bootstrap is still a good way to get the LLVM, Clang, and LLD
libraries, as long as you use a Zig++ compiler to build with them.
Devkits
zigpp-bootstrap is zig-bootstrap for Zig++, and it makes the devkits that Zig++’s CI and releases build with. A devkit is a prebuilt tree of static LLVM, Clang, and LLD libraries, zlib and zstd, and a Zig++ compiler, with the headers to build against.
It builds LLVM, Clang, and LLD 23.1.2 from the llvmorg-23.1.2 release source,
checked against its SHA-256 and patched with the patches that zig-bootstrap
uses for LLVM 23, plus zlib 1.3.1 and zstd 1.5.2.
Host requirements:
- a C++ compiler that can build LLVM, Clang, and LLD: GCC 5.1 or later, or Clang;
- CMake 3.20 or later, and Ninja or another build system that CMake supports;
curl,tarwith xz support,patch, andsha256sumorshasum;- a POSIX system (
sh,mkdir,cd) and Python 3.
git clone --recursive https://github.com/mattneel/zigpp-bootstrap
cd zigpp-bootstrap
./build x86_64-linux-musl baseline
<arch>-<os>-<abi> is a Zig target and <mcpu> is a -mcpu value of Zig:
baseline for a generic CPU of the architecture, or native. The Zig++
distribution for the target appears in out/zig-x86_64-linux-musl-baseline/.
The first run builds LLVM, Clang, and LLD twice: once for the host, to build
Zig++ and the LLVM tools the cross build needs, and once for the target, with
Zig++ as the cross compiler. Later runs for other targets reuse the host build.
CMAKE_GENERATOR=Ninja and CMAKE_BUILD_PARALLEL_LEVEL tune it, ZIG_SRC
builds another Zig++ checkout instead of the submodule, and ZIG_VERSION sets
the version string it is built as.
To get just the libraries and compiler for a target, as one archive:
./devkit x86_64-linux-musl baseline
out/devkit/zig+llvm+lld+clang-x86_64-linux-musl-0.17.0-dev.2361+zigpp.5b96e6d21.tar.xz
That archive holds the target’s libraries and headers, bin/zig, and lib/zig/
for a computer of that target. On Windows, ./devkit x86_64-windows-gnu baseline
packages the same thing as a .zip. Unpack it anywhere and build with it:
zig build -Dtarget=x86_64-linux-musl -Dstatic-llvm --search-prefix /path/to/devkit
./publish uploads every devkit of the version in zig-version to the
devkit-<version> release of zigpp-bootstrap, with a SHA256SUMS file, and
Zig++’s .github/scripts/devkit.sh names the version that CI downloads. So you
do not have to build a devkit to use one: download it, unpack it, and use it as
the --search-prefix above.
An upstream Zig devkit is not a Zig++ compiler, and it cannot build one. Use a Zig++ devkit, whose LLVM is 23.1.2.
How CI builds Zig++
Zig++’s CI does not install LLVM. .github/scripts/bootstrap.sh downloads the
devkit of the machine, x86_64-linux-musl, x86_64-windows-gnu (in Git Bash),
or aarch64-macos-none, into ~/deps, where it stays for the next run, and
unpacks the newest release’s compiler there too, in a directory named for the
release’s version, so a run whose newest release has not moved downloads
nothing. It then builds the compiler with the release’s compiler, against this
checkout’s lib/; on the Linux machine, that is:
ZIG_LIB_DIR="$PWD/lib" "$RELEASE/zig" build \
--prefix build-bootstrap/stage3 \
--search-prefix "$PREFIX" \
-Dtarget=x86_64-linux-musl -Dcpu=baseline \
-Denable-llvm -Dstatic-llvm -Duse-zig-libcxx \
-Doptimize=ReleaseFast -Dstrip -Dno-lib
When there is no release, when stage1/zig1.wasm changed since the release’s
commit, or when the release cannot build the checkout, it bootstraps from
source instead, on Linux and macOS: the CMake build, with the devkit’s Zig++ as the C and C++
compiler and the devkit’s libraries as the search prefix:
cmake .. \
-DCMAKE_INSTALL_PREFIX=stage3 \
-DCMAKE_PREFIX_PATH="$PREFIX" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER="$PREFIX/bin/zig;cc;-target;x86_64-linux-musl;-mcpu=baseline" \
-DCMAKE_CXX_COMPILER="$PREFIX/bin/zig;c++;-target;x86_64-linux-musl;-mcpu=baseline" \
-DZIG_TARGET_TRIPLE=x86_64-linux-musl \
-DZIG_TARGET_MCPU=baseline \
-DZIG_STATIC=ON \
-DZIG_NO_LIB=ON \
-GNinja
ninja install
Either way, lib/ stays in the checkout, so the scripts point ZIG_LIB_DIR at
this repository’s lib/. The compiler that comes out is
build-bootstrap/stage3/bin/zig, and the release workflow cross-compiles every
target’s archive with it, builds the language reference once with it and hands
that to the per-target package jobs, which build with -Dno-langref.
A CI job points ZIG_LOCAL_CACHE_DIR and ZIG_GLOBAL_CACHE_DIR at directories
under the runner’s tool cache, which actions/checkout leaves alone, so a run
starts with what the run before it built. The compiler is one build step, its
C++ glue inside it, so a run that changes the Zig source rebuilds that step: on
the WSL2 machine, 326 s against 415 s with no cache, and 0.04 s for a commit
that has already been built once. A run of the suites leaves about 6 GB in the
local cache and the compiler’s share of the global one under 1 GB, so each
directory is wiped whole when it passes 32 GiB, several runs of history: a Zig
cache entry may not be deleted on its own, because a manifest may outlive the
output it names, and one rebuild is cheaper than a machine whose disk has
quietly filled.
Building without LLVM
The only system dependency of this route is a C compiler:
cc -o bootstrap bootstrap.c
./bootstrap
This produces zig2 in the current working directory: a stage2 build of the
compiler, reported as 0.17.0-dev.bootstrap, without LLVM extensions, and
therefore lacking:
- release-mode optimizations;
- some ELF linking features, some COFF/PE linking features, and some WebAssembly linking features;
- the ability to create static archives from object files;
- the ability to compile assembly files;
- the ability to compile C, C++, Objective-C, and Objective-C++ files.
It still provides an LLVM backend that produces bitcode files, which a separately installed Clang can optimize and compile, and a C backend that produces C source. From there, the build system installs a compiler as usual:
./zig2 build
Zig++ is not going upstream’s way on LLVM: LLVM is a permanent, first-class
dependency of Zig++, and the LLVM-less zig2 above exists only to bootstrap
it. Upstream’s project to
completely eliminate the dependency on LLVM library API
calls will not be resolved here,
and LLVM is forever explains why.
AI Policy and Governance
AI policy
Upstream Zig bans LLMs from issues, patches, and bug tracker comments. Zig++ welcomes them. The first Zig++ language feature, private fields, was implemented, tested, and documented by Claude, and the commit is signed that way.
Zig++ has AI on both sides of compilation: as a runtime facility in the
programs it builds, and as a build-system facility in the build itself.
Neither is in std yet: both are packages today, and both are headed into the
standard library under the names std.ai and std.Build.ai.
std.ai will be ai.zig, the AI toolkit
for Zig: an implementation of the Vercel AI SDK v7 core built on std.Io,
with text generation and streaming, multi-step tool loops, structured output,
embeddings, reranking, reusable agents, and an MCP client.
std.Build.ai will be Zigger, AI code
generation as a Zig build step. Today it is a package that adds a
zig build gen step: it reads SPEC.md, runs the Claude CLI to implement it,
runs zig build test, feeds any failures back, and repeats until the tests
pass (up to 10 times by default). With -Dtdd=true it writes failing tests
first. The plan is to move this pipeline into std.Build.ai.
The goal: by September 2027, all new Zig++ code is written through the Zigger pipeline.
Governance
Upstream Zig is BDFN (Benevolent Dictator For Now). Zig++ is BDFL: Matthew Neel is the Benevolent Dictator For Life and has final say on the design and implementation of everything.
Zig++ has no Code of Conduct. It has one rule: talk about code. Issues, pull requests, reviews, and comments are for the compiler, the language, the standard library, and the tools. Everything else, politics included, is off topic and will be closed.
Language proposals are welcome. Zig++ is made of them.
Contributing
The source, the issues, and the pull requests are at
github.com/mattneel/zigpp. CI runs on the
project’s own machines: x86_64 Linux under WSL2 and x86_64 Windows, on a laptop
with an RTX 5090 Laptop GPU, and an M4 Mac. On each it builds Zig++ against the
caches the run before it left, and runs all four suites at the same time, a
quarter of the machine’s cores each through -j, printing their logs when they
have all finished:
# The standard library and behavior tests, with both back ends.
zig build test-fmt
zig test lib/std/std.zig -lc
zig test test/behavior.zig
zig test lib/std/std.zig -lc -fllvm
zig test test/behavior.zig -fllvm
# Zig++ against C that the Clang in Zig++ compiled.
zig build test-c-abi -Dskip-non-native -Dskip-release
zig build test-cases -Dskip-non-native -Denable-llvm
zig build test-standalone -Dskip-non-native -Dskip-release
The standalone tests include the GPU suite: its kernels compile for NVIDIA, AMD, and Apple GPUs, and it runs them wherever a GPU driver is present. The Linux machine runs the CUDA kernels on the RTX 5090 Laptop GPU, and the Mac runs the Metal kernels on the M4; the suite skips what a machine has no driver for.
A pull request that changes only documentation, ppup, or the other workflows does not run CI, and neither does one from a fork: the machines run only branches of the repository itself. Building from Source has the same bootstrap the CI job uses, so a change can be tested against the compiler CI builds.