Devlog ⚡ Zig Programming Language


February 03, 2026

Author: Andrew Kelley

Microslop Windows provides a large ABI surface area for doing things 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.

The DLLs on Windows are organized into a heirarchy, with some of the APIs being high-level wrappers around lower-level ones. For example, whenever you call functions of kernel32.dll, ultimately, the actual work is done by ntdll.dll. You can observe this directly by using ProcMon.exe and examining stack traces.

What we’ve learned empirically is that the ntdll APIs are generally well-engineered, reasonable, and powerful, but the kernel32 wrappers introduce unnecessary heap allocations, additional failure modes, unintentional CPU usage, and bloat. Using ntdll functions feels like using software made by senior engineers, while using kernel32 functions feels like using software made by Microsoft employees.

This is why the Zig standard library policy is to avoid all DLLs except for ntdll. We’re not quite there yet – we have plenty of calls into kernel32 remaining – but we’ve taken great strides recently. I’ll give you two examples.

Example 1: Entropy

According to the official documentation, Windows does not have a straightforward way to get 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 dynamically loads bcryptprimitives.dll and calls ProcessPrng. If loading the DLL fails (for example due to an overloaded system, which we have observed on Zig CI several times), it returns error 38 (from a function that has void return type and is documented to never fail).

The first thing ProcessPrng does is heap allocate a small, constant number of bytes. If this fails it returns NO_MEMORY in a BOOL (documented behavior is to never fail, and always return TRUE).

bcryptprimitives.dll apparently also runs a test suite every time you load it.

All that ProcessPrng is really doing is NtOpenFile on "\\Device\\CNG" and reading 48 bytes with NtDeviceIoControlFile to get a seed, and then initializing a per-CPU AES-based CSPRNG.

So the dependency on bcryptprimitives.dll and advapi32.dll can both be avoided, and the nondeterministic failure and latencies on first RNG read can also be avoided.

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 the lower level API. For instance, the real API simply gives us the error code as the return value, while the kernel32 wrapper hides the status code somewhere, returns a BOOL and then requires you to call GetLastError to find out what went wrong. Imagine! Returning a value from a function 🌈

Furthermore, OVERLAPPED is a fake type. The Windows kernel doesn’t actually know or care about it at all! The actual primitives here are events, APCs, and IO_STATUS_BLOCK.

If you have a synchronous file handle, then Event and ApcRoutine must be null. You get the answer in the IO_STATUS_BLOCK immediately. If you pass an APC routine here then some old bitrotted 32-bit code runs and you get garbage results.

On the other hand if you have an asynchronous file handle, then you need to either use an Event or an ApcRoutine. kernel32.dll uses events, which means that it’s doing extra, unnecessary resource allocation and management just to read from a file. Instead, Zig now passes an APC routine and then calls NtDelayExecution. This integrates seamlessly with cancelation, making it possible to cancel tasks while they perform file I/O, regardless of whether the file was opened in synchronous mode or asynchronous mode.

For a deeper dive into this topic, please refer to this issue:

Windows: Prefer the Native API over Win32

January 31, 2026

Author: Andrew Kelley

Over the past month or so, several enterprising contributors have taken an interest in the zig libc subproject. The idea here is to incrementally delete redundant code, by providing libc functions as Zig standard library wrappers rather than as vendored C source files. In many cases, these functions are one-to-one mappings, such as memcpy or atan2, or 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, roughly 250 C source files have been deleted from the Zig repository, with 2032 remaining.

With each function that makes the transition, Zig gains independence from third party projects and from the C programming language, compilation speed improves, Zig’s installation size is simplified and reduced, and user applications which statically link libc enjoy reduced binary size.

Additionally, a recent enhancement now makes zig libc share the Zig Compilation Unit with other Zig code rather than being a separate static archive, linked together later. This is one of the advantages of Zig having an integrated compiler and linker. When the exported libc functions share the ZCU, redundant code is eliminated because functions can be optimized together. It’s kind of like enabling LTO (Link-Time Optimization) across the libc boundary, except it’s done properly in the frontend instead of too late, in the linker.

Furthermore, when this work is combined with the recent std.Io changes, there is potential for users to seamlessly control how libc performs I/O – for example forcing all calls to read and write to participate in an io_uring event loop, even though that code was not written with such use case in mind. Or, resource leak detection could be enabled for third-party C code. For now this is only a vaporware idea which has not been experimented with, but the idea intrigues me.

Big thanks to Szabolcs Nagy for libc-test. This project has been a huge help in making sure that we don’t regress any math functions.

As a reminder to our users, now that Zig is transitioning to being the static libc provider, if you encounter issues with the musl, mingw-w64, or wasi-libc libc functionality provided by Zig, please file bug reports in Zig first so we don’t annoy maintainers for bugs that are in Zig, and no longer vendored by independent libc implementation projects.

The very same day I sat at home writing this devlog like a coward, less than five miles away, armed forces who are in my city against the will of our elected officials shot tear gas, unprovoked, at peaceful protestors. Next time I hope to have the courage to join my neighbors, and I hope to not get shot like Alex Pretti and Renée Good.



Source link