0.13.0 Release Notes

Zero the Ziguana

Download & Documentation

Zig is a general-purpose programming language and toolchain for maintaining robust, optimal, and reusable software.

Zig development is funded via Zig Software Foundation, a 501(c)(3) non-profit organization. Please consider a recurring donation so that we can offer more billable hours to our core team members. This is the most straightforward way to accelerate the project along the Roadmap to 1.0.

This release features 2 months of work: changes from 73 different contributors, spread among 415 commits.

This is a relatively short release cycle, primarily motivated by Toolchain upgrades, such as upgrading to LLVM 18.

Table of Contents §

Support Table §

Tier System §

A green check mark (✅) indicates the target meets all the requirements for the support tier. The other icons indicate what is preventing the target from reaching the support tier. In other words, the icons are to-do items.

Tier 1 Support §

freestanding Linux 3.16+ macOS 11+ Windows 10+ WASI
x86_64 N/A
x86 #1929 🐛 💀 #537 🐛 N/A
aarch64 #2443 🐛 #16665 🐛 N/A
arm #3174 🐛 💀 🐛📦🧪 N/A
mips #3345 🐛📦 N/A N/A N/A
riscv64 #4456 🐛 N/A N/A N/A
sparc64 #4931 🐛📦🧪 N/A N/A N/A
powerpc64 🐛 N/A N/A N/A
powerpc 🐛 N/A N/A N/A
wasm32 N/A N/A N/A

Tier 2 Support §

free standing Linux 3.16+ macOS 11+ Windows 10+ FreeBSD 12.0+ NetBSD 8.0+ Dragon FlyBSD 5.8+ OpenBSD 7.3+ UEFI
x86_64 Tier 1 Tier 1 Tier 1 Tier 1
x86 Tier 1 💀 🔍 🔍 N/A 🔍
aarch64 Tier 1 Tier 1 🔍 🔍 N/A 🔍 🔍
arm Tier 1 💀 🔍 🔍 🔍 N/A 🔍 🔍
mips64 N/A N/A 🔍 🔍 N/A 🔍 N/A
mips Tier 1 N/A N/A 🔍 🔍 N/A 🔍 N/A
powerpc64 Tier 1 💀 N/A 🔍 🔍 N/A 🔍 N/A
powerpc Tier 1 💀 N/A 🔍 🔍 N/A 🔍 N/A
riscv64 Tier 1 N/A N/A 🔍 🔍 N/A 🔍 🔍
sparc64 Tier 1 N/A N/A 🔍 🔍 N/A 🔍 N/A

Tier 3 Support §

freestanding Linux 3.16+ Windows 10+ FreeBSD 12.0+ NetBSD 8.0+ UEFI
x86_64 Tier 1 Tier 1 Tier 1 Tier 2 Tier 2 Tier 2
x86 Tier 1 Tier 2 Tier 2 Tier 2
aarch64 Tier 1 Tier 2 Tier 2
arm Tier 1 Tier 2
mips64 Tier 2 Tier 2 N/A N/A
mips Tier 1 Tier 2 N/A N/A
riscv64 Tier 1 Tier 2 N/A
powerpc32 Tier 2 Tier 2 N/A N/A
powerpc64 Tier 2 Tier 2 N/A N/A
bpf N/A N/A
hexagon N/A N/A
amdgcn N/A N/A
sparc N/A N/A
s390x N/A N/A
lanai N/A N/A
csky N/A N/A
freestanding emscripten
wasm32 Tier 1

Tier 4 Support §

Tier 4 targets:

Standard Library §

BatBadBut Mitigation §

std.process.Child: Mitigate arbitrary command execution vulnerability on Windows (BatBadBut) (#19698)

See the pull request description for the details

Simplify hash.crc §

This removes the two original implementations in favour of the single generic one based on the Algorithm type. Previously we had three, very similar implementations which was somewhat confusing when knowing what one should actually be used.

The previous polynomials all have equivalent variants available when using the Algorithm type.

The Koopman polynomial did not have an exact equivalent so one has been added to the catalog.txt file. This does mean we have patched this file but this will be clear if updated in future and tests will catch this.

This is a breaking change for direct users of the old polynomial api. Specifically when using a custom or non-standard polynomial (the Crc32 alias will continue to work). There are clear compile errors indicating what is required in order to retain existing functionality, this may require a small code-change from the user.

const hash = Crc32WithPoly(.Castagnoli); // old
const hash = Crc(.Crc32Iscsi); // new

Custom polynomials require a bit more interaction and will require a user to define their own Algorithm type. If used note that the previous implementation expected a pre-reflected polynomial and used the following parameters:

.{
    .polynomial = 0x741b8cd7, // equivalent to the reflected polynomial: 0xeb31d82e
    .initial = 0xffffffff,
    .reflect_input = true,
    .reflect_output = true,
    .xor_output = 0xffffffff,
});

Loose performance measurements below. Table sizes are indicated to the right. Nothing new, helps rationalise the overlap that was present.

crc32-slicing-by-8 # 8K of tables
   iterative:  3074 MiB/s [2d191d9400000000]
  small keys:  32B  4650 MiB/s 152387950 Hashes/s [20024c446a99a300]
crc32-half-byte-lookup # 64b of tables
   iterative:   281 MiB/s [2d191d9400000000]
  small keys:  32B   389 MiB/s 12751954 Hashes/s [20024c446a99a300]
crc32 # 1K of tables
   iterative:  3077 MiB/s [2d191d9400000000]
  small keys:  32B  4660 MiB/s 152709182 Hashes/s [20024c446a99a300]

ComptimeStringMap renamed to StaticStringMap and enhanced §

ComptimeStringMap is renamed to StaticStringMap, accepts only a single type parameter, and returns a known struct type instead of an anonymous struct. Initial motivation for these changes was to reduce the 'very long type names' issue described in #19682.

This breaks the previous API. Users will now need to write:

const map = std.StaticStringMap(T).initComptime(kvs_list);

More details:

PriorityQueue stores capacity instead of length §

This is a breaking change that aligns the PriorityQueue API to the ArrayList API.

Before, PriorityQueue stored the full allocated slice in the items field, and length in a separate len field. This was inconsistent with ArrayList and led to the mistake of accessing undefined memory.

Now, the items field points to only the valid items in the queue, and the extra unused capacity is stored in a separate cap field.

#19960

Rename std.ChildProcess to std.process.Child §

The old name has been deprecated for several releases now.

Upgrade guide:

std.ChildProcess

std.process.Child

Some other functions are also moved from std.ChildProcess to std.process namespace.

In the future there will be even more breaking changes. For example, instead of creating a Child and then setting fields on it and then calling spawn, there will be std.process.spawn which takes an "options" parameter and then returns the Child, which is an object that lasts only from spawn until termination. This is a practice that we have been moving more towards in Zig, which is to have types designed to have minimal lifetimes and minimal states with undefined fields.

Reworked CLI Progress §

Primarily, this is a breaking change to the Standard Library. However, the Compiler and Build System both lean heavily on this API and are thereby affected.

simple asciinema demo [demo source]

demo: building a music player with zig build [source code]

Performance impact: insignificant [source]

Upgrade Guide:

var progress = std.Progress{
    ...
};
const root_node = progress.start("Test", test_fn_list.len);

const root_node = std.Progress.start(.{
    .root_name = "Test",
    .estimated_total_items = test_fn_list.len,
});

All the options to start are optional.

Finally, when spawning a child process, populate the progress_node field first:

child.progress_node = node;
try child.spawn();

The previous implementation of std.Progress had the design limitation that it could not assume ownership of the terminal. This meant that it had to play nicely with sub-processes purely via what was printed to the terminal, and it had to play nicely with progress-unaware stderr writes to the terminal. It also was forbidden from installing a SIGWINCH handler, or running ioctl to find out the rows and cols of the terminal.

The new implementation is designed around the idea that a single process will be the sole owner of the terminal, and all other progress reports will be communicated back to that process. With this change in the requirements, it becomes possible to make a much more useful progress bar.

This creates a standard "Zig Progress Protocol" and uses it so that the same std.Progress API works both when an application is the main owner of a terminal, and when an application is a child process. In the latter case, progress information is communicated semantically over a pipe to the parent process.

The file descriptor is given in the ZIG_PROGRESS environment variable. std.process.Child integrates with this, so attaching a child's progress subtree in a parent process is as easy as setting the child.progress_node field before calling spawn.

In order to avoid performance penalty for using this API, the Node.start and Node.end APIs are thread-safe, lock-free, infallible, and do minimal amount of memory loads and stores. In order to accomplish this, a statically allocated buffer of Node storage is used - one array for parents, and one array for the rest of the data. Children are not stored. The statically allocated buffer is used for a bespoke Node allocator implementation. A static buffer is sufficient because we can set an upper bound on supported terminal width and height. If the terminal size exceeds this, the progress bar output will be truncated regardless.

A separate thread periodically refreshes the terminal on a timer. This progress update thread iterates over the entire preallocated parents array, looking for used nodes. This is efficient because the parents array is only 200 8-bit integers, or about 4 cache lines. When iterating, this thread "serializes" the data into a separate preallocated array by atomically loading from the shared data into data that is only touched by a single thread - the progress update thread. It then looks for nodes that are marked with a file descriptor that is a pipe to a child process. Such nodes are replaced during the serialization process with the data from reading from the pipe. The data can be memcpy'd into place except for the parents array which needs to be relocated. Once this serialization process is complete, there are two paths, one for a child process, and one for the root process that owns the terminal.

The root process that owns the terminal scans the serialized data, computing children and sibling pointers. The canonical data only stores parents, so this is where the tree structure is computed. Then the tree is walked, appending to a static buffer that will be sent to the terminal with a single write() syscall. During this process, the detected rows and cols of the terminal are respected. If the user resizes the terminal, it will cause a SIGWINCH which signals the update thread to wake up and redraw with the new rows and cols.

A child process, instead of drawing to the terminal, takes the same serialized data and sends it across a pipe. The pipe is in non-blocking mode, so if it fills up, the child drops the message; a future update will contain the new progress information. Likewise when the parent reads from the pipe, it discards all messages in the buffer except for the last one. If there are no messages in the pipe, the parent uses the data from the last update.

Andrew Kelley's blog post on the topic

posix.iovec: use .base and .len instead of .iov_base and .iov_len §

Upgrade guide:

.{ .iov_base = message.ptr, .iov_len = message.len },

.{ .base = message.ptr, .len = message.len },

Added std.zip and Zip Archive Support in build.zig.zon §

build.zig.zon files now support .zip archive dependencies. std.zip has also been added which can extract zip files.

Build System §

Step.Run: global lock when child inherits stderr §

The docs for setting stdio to "inherit" say:

Causes the Run step to be considered to have side-effects, and therefore always execute when it appears in the build graph. It also means that this step will obtain a global lock to prevent other steps from running in the meantime. The step will fail if the subprocess crashes or returns a non-zero exit code.

The implementation of this lock was missing but is now implemented, ensuring that only one process which owns stdout/stderr is running at a time.

Install Windows DLLs to <prefix>/bin/ by default §

Windows does not support RPATH and only searches for DLLs in a small number of predetermined paths by default, with one of them being the directory from which the application loaded.

Currently, if you build an executable and a DLL, link them and install them with the default settings, the exe will go in bin/ and the DLL in lib/, which makes the exe unable to find the DLL at runtime without manually adding lib/ to the PATH environment variable.

Installing both executables and DLLs to bin/ by default helps ensure that the executable can find any DLL artifacts it has linked to. DLL import libraries are still installed to lib/.

These defaults match CMake's behavior.

Illustrative example:

exe.root_module.linkLibrary(sdl3_lib);

b.installArtifact(exe);
b.installArtifact(sdl3_lib);
zig-out/
├───bin/
│   ├───example.exe
│   ├───example.pdb
│   ├───SDL3.dll
│   └───SDL3.pdb
├───include/
│   └───SDL3/
│       ├───SDL.h
│       └───<omitted>
└───lib/
    └───SDL3.lib

Step.ObjCopy: don't accept multiple sections §

The actual zig objcopy does not accept keeping multiple sections. If you pass multiple -j .section arguments to zig objcopy, it will only respect the last one passed.

The build system API now uses a type that reflects this.

Compiler §

Replace the YES_COLOR env variable with CLICOLOR_FORCE §

Detecting the NO_COLOR environment variable and disabling color output if set is a standard practice respected by most CLI tools.

When it comes to the inverse task of forcing color output even when not writing to a terminal, there exist two standards, CLICOLOR_FORCE and FORCE_COLOR. Neither of these two standards come even close to being as ubiquitous as NO_COLOR, but they both have some precedence and are respected by a handful of CLI tools.

Prior to e45d24c0de29eb6668e56ea927e15505674833a6, Zig used the ZIG_DEBUG_COLOR env variable to force color output, but that commit changed it to YES_COLOR.

YES_COLOR seemingly has next to no precedent in existing software (a search for /(?-i)\bYES_COLOR\b/ on GitHub returned 142 files).

Instead of throwing a third standard into the mix and making it even harder for users to manage colored output in CLI tooling, it makes more sense to instead tag along with one of CLICOLOR_FORCE or FORCE_COLOR.

CLICOLOR_FORCE is chosen over FORCE_COLOR for the following reasons:

In a better universe, someone would have established a convention like CLICOLOR=ON or CLICOLOR=OFF way back in time so that we wouldn't have to juggle multiple environemnt variables, but that ship has sailed.

CLICOLOR_FORCE is also added to the output of zig env.

LLVM Backend §

loongarch64 support added. It still can't build hello world because LLVM didn't implement fp_to_fp16 for that target yet.

zig-cache renamed to .zig-cache §

This plays nicely with more tooling. For example, text editors will typically exclude this directory from "find in files" features. It communicates to new users of Zig that these files are ephemeral. I apologize for not getting this right the first time.

zig-out is unchanged.

Bug Fixes §

Full list of the 43 bug reports closed during this release cycle.

Many bugs were both introduced and resolved within this release cycle. Most bug fixes are omitted from these release notes for the sake of brevity.

This Release Contains Bugs §

Zero the Ziguana

Zig has known bugs and even some miscompilations.

Zig is immature. Even with Zig 0.13.0, working on a non-trivial project using Zig will likely require participating in the development process.

When Zig reaches 1.0.0, Tier 1 Support will gain a bug policy as an additional requirement.

Toolchain §

LLVM 18 §

This release of Zig upgrades to LLVM 18.1.7.

This was the primary motivation for tagging the 0.13.0 release.

musl 1.2.5 §

Zig ships with the source code to musl. When the musl C ABI is selected, Zig builds static musl from source for the selected target. Zig also supports targeting dynamically linked musl which is useful for Linux distributions that use it as their system libc, such as Alpine Linux.

This release upgrades from v1.2.4 to v1.2.5.

glibc 2.39 §

glibc version 2.39 are now available when cross-compiling.

Roadmap §

Ziggy the Ziguana

The major theme of the 0.14.0 release cycle will be compilation speed.

Some upcoming milestones we will be working towards in the 0.14.0 release cycle:

The idea here is that prioritizing faster compilation will increase development velocity on the Compiler itself, leading to more bugs fixed and features completed in the following release cycles.

It also could potentially lead to language changes that unblock fast compilation.

Thank You Contributors! §

Ziggy the Ziguana

Here are all the people who landed at least one contribution into this release:

Thank You Sponsors! §

Ziggy the Ziguana

Special thanks to those who sponsor Zig. Because of recurring donations, Zig is driven by the open source community, rather than the goal of making profit. In particular, these fine folks sponsor Zig for $50/month or more: