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.
std.zip
and Zip Archive Support in build.zig.zon
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.
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 | ✅ |
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 |
zig targets
is guaranteed to include this target.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 | ✅ |
zig targets
will display the target if it is available.-femit-asm
and cannot emit
object files, in which case -fno-emit-bin
is enabled by
default and cannot be overridden.Tier 4 targets:
std.process.Child: Mitigate arbitrary command execution vulnerability on Windows (BatBadBut) (#19698)
See the pull request description for the details
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 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:
kvs_list
param from type param to an initComptime()
paramkeys()
, values()
helpersinit(allocator)
, deinit(allocator)
for runtime datagetLongestPrefix(str)
, getLongestPrefixIndex(str)
- i'm not sure
these belong but have left in for now incase they are deemed usefulThis 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
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.
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:
std.Progress.Node
instead of *std.Progress.Node
(no longer a pointer).node.activate()
. Those are not needed anymore.Node.start
since the data will be copied.std.Progress
more than once. Do that in main() and nowhere else.std.debug.lockStdErr
and std.debug.unlockStdErr
before writing to stderr to integrate properly with std.Progress
(std.debug.print
already does this for you).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
Upgrade guide:
.{ .iov_base = message.ptr, .iov_len = message.len },
↓
.{ .base = message.ptr, .len = message.len },
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.
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.
<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
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.
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:
CLICOLOR_FORCE
, was created in 2015. The corresponding https://force-color.org/ for FORCE_COLOR
was created in 2023.CLICOLOR_FORCE
appears to have been introduced by ls
in FreeBSD 4.1.1 in 2000. FORCE_COLOR
appears to have been introduced by the chalk JavaScript library in 2015.CLICOLOR_FORCE
is supported by CMake and Ninja./(?-i)\bCLICOLOR_FORCE\b/
returns 28.9k files and /(?-i)\bFORCE_COLOR\b/
39k files, the former seems more common with software written in C/C++ while the latter seems more tied to Node.js and Python ecosystems.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
.
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.
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.
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.
This release of Zig upgrades to LLVM 18.1.7.
This was the primary motivation for tagging the 0.13.0 release.
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 version 2.39 are now available when cross-compiling.
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.
Here are all the people who landed at least one contribution into this release:
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: