This page contains a curated list of recent changes to the main branch Zig.
This page contains entries from the year <span>2026</span>. Other years are available on the Devlog archive page.
</p><div>
<div id="2026-02-13">
<span>13 February 2026</span>
<p>Author: Andrew Kelly
As we approach the end of the 0.16.0 release cycle, Jacob is hard at work std.Io.Evented Get up to speed with all the latest API changes:
Both of these are based on userspace stack switching, sometimes called “fibers”, “stackful coroutines”, or “green threads”.
they are now available to be tampered withBy building one’s application using std.Io.Evented. they should be considered experimental Because there is important follow-up work to be done before these can be used reliably and robustly:
With those caveats in mind, it looks like we are indeed reaching the promised land, where IO implementations in Zig code can be easily changed:
const std = @import("std");
pub fn main(init: std.process.Init.Minimal) !void {
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
const gpa = debug_allocator.allocator();
var threaded: std.Io.Threaded = .init(gpa, .{
.argv0 = .init(init.args),
.environ = init.environ,
});
defer threaded.deinit();
const io = threaded.io();
return app(io);
}
fn app(io: std.Io) !void {
try std.Io.File.stdout().writeStreamingAll(io, "Hello, World!\n");
}
$ strace ./hello_threaded
execve("./hello_threaded", ["./hello_threaded"], 0x7ffc1da88b20 /* 98 vars */) = 0
mmap(NULL, 262207, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f583f338000
arch_prctl(ARCH_SET_FS, 0x7f583f378018) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
prlimit64(0, RLIMIT_STACK, {rlim_cur=16384*1024, rlim_max=RLIM64_INFINITY}, NULL) = 0
sigaltstack({ss_sp=0x7f583f338000, ss_flags=0, ss_size=262144}, NULL) = 0
sched_getaffinity(0, 128, [0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31]) = 8
rt_sigaction(SIGIO, {sa_handler=0x1019d90, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x10328c0}, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGPIPE, {sa_handler=0x1019d90, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x10328c0}, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
writev(1, [{iov_base="Hello, World!\n", iov_len=14}], 1Hello, World!
) = 14
rt_sigaction(SIGIO, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x10328c0}, NULL, 8) = 0
rt_sigaction(SIGPIPE, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x10328c0}, NULL, 8) = 0
exit_group(0) = ?
+++ exited with 0 +++
Swapping only the I/O implementation:
const std = @import("std");
pub fn main(init: std.process.Init.Minimal) !void {
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
const gpa = debug_allocator.allocator();
var evented: std.Io.Evented = undefined;
try evented.init(gpa, .{
.argv0 = .init(init.args),
.environ = init.environ,
.backing_allocator_needs_mutex = false,
});
defer evented.deinit();
const io = evented.io();
return app(io);
}
fn app(io: std.Io) !void {
try std.Io.File.stdout().writeStreamingAll(io, "Hello, World!\n");
}
execve("./hello_evented", ["./hello_evented"], 0x7fff368894f0 /* 98 vars */) = 0
mmap(NULL, 262215, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f70a4c28000
arch_prctl(ARCH_SET_FS, 0x7f70a4c68020) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
prlimit64(0, RLIMIT_STACK, {rlim_cur=16384*1024, rlim_max=RLIM64_INFINITY}, NULL) = 0
sigaltstack({ss_sp=0x7f70a4c28008, ss_flags=0, ss_size=262144}, NULL) = 0
sched_getaffinity(0, 128, [0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31]) = 8
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f70a4c27000
mmap(0x7f70a4c28000, 548864, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f70a4ba1000
io_uring_setup(64, {flags=IORING_SETUP_COOP_TASKRUN|IORING_SETUP_SINGLE_ISSUER, sq_thread_cpu=0, sq_thread_idle=1000, sq_entries=64, cq_entries=128, features=IORING_FEAT_SINGLE_MMAP|IORING_FEAT_NODROP|IORING_FEAT_SUBMIT_STABLE|IORING_FEAT_RW_CUR_POS|IORING_FEAT_CUR_PERSONALITY|IORING_FEAT_FAST_POLL|IORING_FEAT_POLL_32BITS|IORING_FEAT_SQPOLL_NONFIXED|IORING_FEAT_EXT_ARG|IORING_FEAT_NATIVE_WORKERS|IORING_FEAT_RSRC_TAGS|IORING_FEAT_CQE_SKIP|IORING_FEAT_LINKED_FILE|IORING_FEAT_REG_REG_RING|IORING_FEAT_RECVSEND_BUNDLE|IORING_FEAT_MIN_TIMEOUT|IORING_FEAT_RW_ATTR|IORING_FEAT_NO_IOWAIT, sq_off={head=0, tail=4, ring_mask=16, ring_entries=24, flags=36, dropped=32, array=2112, user_addr=0}, cq_off={head=8, tail=12, ring_mask=20, ring_entries=28, overflow=44, cqes=64, flags=40, user_addr=0}}) = 3
mmap(NULL, 2368, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_POPULATE, 3, 0) = 0x7f70a4ba0000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_POPULATE, 3, 0x10000000) = 0x7f70a4b9f000
io_uring_enter(3, 1, 1, IORING_ENTER_GETEVENTS, NULL, 8Hello, World!
) = 1
io_uring_enter(3, 1, 1, IORING_ENTER_GETEVENTS, NULL, 8) = 1
munmap(0x7f70a4b9f000, 4096) = 0
munmap(0x7f70a4ba0000, 2368) = 0
close(3) = 0
munmap(0x7f70a4ba1000, 548864) = 0
exit_group(0) = ?
+++ exited with 0 +++
The main thing here is that app The function is the same between those two snippets.
Moving on from Hello World, the Zig compiler works fine using itself std.Io.EventedWith both io_ureing and GCD, but as mentioned above, the performance degradation when doing this has not yet been diagnosed.
Happy hacking,
andrew
06 February 2026
Author: Andrew Kelly
If you have a Zig project with dependencies, there are two big changes that just came out that I think you’ll be interested in knowing about.
Fetched packages are now archived locally In zig-pkg The directory of the project root (next to yours). build.zig file).
For example here are some results from Awebo after running zig build: :
$ du -sh zig-pkg/*
13M freetype-2.14.1-alzUkTyBqgBwke4Jsot997WYSpl207Ij9oO-2QOvGrOi
20K opus-0.0.2-vuF-cMAkAADVsm707MYCtPmqmRs0gzg84Sz0qGbb5E3w
4.3M pulseaudio-16.1.1-9-mk_62MZkNwBaFwiZ7ZVrYRIf_3dTqqJR5PbMRCJzSuLw
5.2M uucode-0.1.0-ZZjBPvtWUACf5dqD_f9I37VGFsN24436CuceC5pTJ25n
728K vaxis-0.5.1-BWNV_AxECQCj3p4Hcv4U3Yo1WMUJ7Z2FUj0UkpuJGxQQ
It is highly recommended to add this directory to the project-local source control ignore file (for example .gitignore). However, staying outside .zig-cacheIt offers the possibility to distribute a self-contained source tarball, which includes all dependencies and can therefore be used offline, or for archival purposes.
Meanwhile, a Excessive A copy of the dependency is cached globally. After filtering all unused files based on paths Filter, content is recompressed:
$ du -sh ~/.cache/zig/p/*
2.4M freetype-2.14.1-alzUkTyBqgBwke4Jsot997WYSpl207Ij9oO-2QOvGrOi.tar.gz
4.0K opus-0.0.2-vuF-cMAkAADVsm707MYCtPmqmRs0gzg84Sz0qGbb5E3w.tar.gz
636K pulseaudio-16.1.1-9-mk_62MZkNwBaFwiZ7ZVrYRIf_3dTqqJR5PbMRCJzSuLw.tar.gz
880K uucode-0.1.0-ZZjBPvtWUACf5dqD_f9I37VGFsN24436CuceC5pTJ25n.tar.gz
120K vaxis-0.5.1-BWNV_BFECQBbXeTeFd48uTJRjD5a-KD6kPuKanzzVB01.tar.gz
The motivation for this change is to make it easier to tinker. Go ahead and edit those files, see what happens. Replace your package directory with git clone. Grab all your dependencies together. Configure your IDE to autocomplete based on zig-pkg Directory. Run Baobab on your dependency tree. Additionally, having compressed files in the global cache makes it easier to share that cached data between computers. In the future, it is planned to support peer-to-peer torrenting of dependency trees. By recompressing the packages into canonical form, this will allow peers to share Zig packages with minimal bandwidth. I like this idea because it simultaneously provides resilience to network outages, as well as a popularity contest. Find out which open source packages are popular based on the number of seeders!
Here is the addition of the second change --fork flagged down zig build.
Looking back it seems so obvious that I don’t know why I didn’t think of it from the beginning. it looks like this:
zig build --fork=[path]
this is one project override Option. Given a path to a project’s source checkout, all packages matching that project in the entire dependency tree will be overridden.
Thanks to the fact that the package content hash includes the name and fingerprint, This package is potentially resolved before being received.
This is an easy way to temporarily use one or more forks that are in completely different directories. Using the development environment and source control of dependency projects comfortably, you can iterate over your entire dependency tree until everything is working.
The fact that it’s a CLI flag makes it reasonably short-lived. The moment you drop the flags, you are back to using your pristine, derived dependency tree.
If the project does not match, an error is generated, preventing confusion:
$ zig build --fork=/home/andy/dev/mime
error: fork /home/andy/dev/mime matched no mime packages
$
If the project matches, you get a reminder that you’re using a fork, to avoid confusion:
$ zig build --fork=/home/andy/dev/dvui
info: fork /home/andy/dev/dvui matched 1 (dvui) packages
...
This functionality aims to enhance the workflow of dealing with ecosystem breakdown. I’ve already tried it out a bit and found it to be quite pleasant to work with. The new workflow is as follows:
- Failure to manufacture from source due to ecosystem breakdown.
- Tinker with
--fork Until your project works again. During this time you can use the real upstream source control, test suite. zig build test --watch -fincrementaletc.
- Now you have a new choice: be selfish and just keep working on your work, or you can move on to submitting your patch upstream.
…and you can probably skip the step where you switch your build.zig.zon On your fork unless you expect it to take a long time for upstream to merge your fixes.
03 February 2026
Author: Andrew Kelly
The Windows operating system provides a large ABI surface area to work with in the kernel. However, not all ABIs are created equally. As Casey Muratori points out in his lecture, The Only Unbreakable Law, the organizational structure of software development teams has a direct impact on the structure of the software they produce.
DLLs on Windows are organized into a hierarchy, with some APIs being high-level wrappers around low-level ones. For example, whenever you call functions kernel32.dllUltimately, the real work is done by ntdll.dll. You can check this directly by using ProcMon.exe and examining the stack trace.
What we’ve learned empirically is that NTDLL APIs are generally well-engineered, reasonable, and powerful, but kernel32 wrappers introduce unnecessary heap allocations, additional failure modes, unintended CPU usage, and bloat.
This is why the Zig standard library policy is to prefer native API over Win32. We’re not there yet – we have a lot of calls left in kernel32 – but we’ve made a lot of progress recently. I will give you two examples.
Example 1: Entropy
According to the official documentation, Windows has no direct way to obtain random bytes.
Many projects including Chromium, BoringSSL, Firefox, and Rust Call SystemFunction036 From advapi32.dll Because it worked on versions older than Windows 8.
Unfortunately, starting with Windows 8, the first time you call this function, it is loaded dynamically. bcryptprimitives.dll And calls processPRNG. If loading the DLL fails (for example due to an overloaded system, which we have seen many times on Zig CI), it returns error 38 (from a function). void return type and is documented to never fail).
first thing ProcessPrng Is the heap allocates a small, constant number of bytes. If it fails it returns NO_MEMORY one in BOOL (The documented behavior is to never fail, and always return TRUE).
bcryptprimitives.dll Apparently it also runs a test suite every time you load it.
all that ProcessPrng Is In fact doing NtOpenFile But "\\Device\\CNG" and reading 48 bytes NtDeviceIoControlFile To obtain a seed, and then initialize a per-CPU AES-based CSPRNG.
so depend on bcryptprimitives.dll And advapi32.dll Both can be avoided, and non-deterministic failure and latency can also be avoided on the first RNG read.
Example 2: NtReadFile and NtWriteFile
ReadFile looks like this:
pub extern "kernel32" fn ReadFile(
hFile: HANDLE,
lpBuffer: LPVOID,
nNumberOfBytesToRead: DWORD,
lpNumberOfBytesRead: ?*DWORD,
lpOverlapped: ?*OVERLAPPED,
) callconv(.winapi) BOOL;
NtReadFile looks like this:
pub extern "ntdll" fn NtReadFile(
FileHandle: HANDLE,
Event: ?HANDLE,
ApcRoutine: ?*const IO_APC_ROUTINE,
ApcContext: ?*anyopaque,
IoStatusBlock: *IO_STATUS_BLOCK,
Buffer: *anyopaque,
Length: ULONG,
ByteOffset: ?*const LARGE_INTEGER,
Key: ?*const ULONG,
) callconv(.winapi) NTSTATUS;
as a reminder, The above function is implemented by calling the below function.
Already we can see some nice things about using lower level APIs. For example, Real The API gives us only the error code as a return value, while the kernel32 wrapper hides the status code somewhere, returning a BOOL and then you need to call GetLastError To find out what went wrong. suppose! Returning a value from a function 🌈
Ahead, OVERLAPPED There is a fake type. The Windows kernel doesn’t actually know or care about it at all! Here are the real primitive events, APC and IO_STATUS_BLOCK.
If you have a synchronous file handle, Event And ApcRoutine it must happen null. You will find the answer in this IO_STATUS_BLOCK Immediately. If you pass an APC routine here some old bitrotated 32-bit code runs and you get garbage results.
On the other hand, if you have an asynchronous file handle, you have to use either Event or one ApcRoutine. kernel32.dll Uses events, which means it’s allocating and managing extra, unnecessary resources just to read from the file. Instead, Zig now passes the APC routine and then calls NtDelayExecution. It integrates seamlessly with cancellation, making it possible to cancel tasks while performing file I/O, regardless of whether the file is opened in synchronous mode or asynchronous mode.
For an in-depth look at this topic, please check out this issue:
Windows: Prefer Native API over Win32
31 January 2026
Author: Andrew Kelly
Over the past month or so, a number of entrepreneurial contributors have taken interest in the ZigLibsy subproject. The idea here is to incrementally remove unnecessary code by providing libc functions as Zig standard library wrappers instead of vendor C source files. In many cases, these functions are one-to-one mappings, e.g. memcpy Or atan2Or trivially wrap a generic function, like strnlen: :
fn strnlen(str: [*:0]const c_char, max: usize) callconv(.c) usize {
return std.mem.findScalar(u8, @ptrCast(str[0..max]), 0) orelse max;
}
So far, about 250 C source files have been removed from the Zig repository, with 2032 remaining.
With each function change, Zig gains independence from third-party projects and the C programming language, compilation speed improves, Zig’s installation size becomes simpler and smaller, and user applications that statically link libc enjoy reduced binary sizes.
Additionally, a recent enhancement now makes Zig libsy share the Zig compilation unit with other Zig code rather than as a separate static collection that is later linked together. This is one of the advantages of Zig having an integrated compiler and linker. When exported libc functions share a ZCU, redundant code is eliminated because the functions can be optimized together. It’s like enabling LTO (link-time optimization) across the libc limit, except it’s done properly in the frontend instead of very late in the linker.
Additionally, when this work is combined with recent std.Io changes, it becomes possible for users to control how libc performs I/O – for example forcing all calls to read And write io_ure to participate in the event loop, even though that code was not written with such a use case in mind. Or, resource leak detection can be enabled for third-party C code. For now it’s just a vaporware idea that hasn’t been experimented with, but the idea appeals to me.
Many thanks to Szabolcs Nagy for libsy-test. This project has been very helpful in ensuring that we do not leave any math functions behind.
As a reminder to our users, now that Zig is transitioning to becoming a stable libc provider, if you encounter issues with the musl, mingw-w64, or wasi-libc libc functionality provided by Zig, Please file a bug report in Zig first So we don’t bother maintainers for bugs that exist in Zig, and are no longer vendored by independent libc implementation projects.
The same day I sat at home, cowardly writing this blog, less than five miles away in my city, armed forces unprovoked tear gas against peaceful protesters against the wishes of our elected officials. Next time I hope I have the courage to connect with my neighbors, and I hope I don’t have to get shot like Alex Pretty and Renee Good.