Zig ELF Linker Improvements Devlog
Recorded: May 30, 2026, 6:01 p.m.
| Original | Summarized |
Devlog Devlog Download Download Devlog Also available as an This page contains entries for the year 2026. Other years are available in May 30, 2026 Author: Matthew LuggI’ve spent the past few weeks working on our new ELF linker which debuted in 0.16.0 release. At the time of the 0.16.0 release, this linker implementation was in its fairly early stages, and only really supported linking Zig-only code without any external libraries (even libc)—hence why it was (and still is) disabled by default (it can be enabled with -fnew-linker). However, quite a lot of progress has been made since that initial release!Here’s a nice milestone—as of my latest PR, the new ELF linker is capable of building the self-hosted Zig compiler with LLVM and LLD libraries enabled, a task which requires quite a few features under the hood.[mlugg@nebula master]$ # Build the Zig compiler using the new linker: Of course, an ELF linker isn’t necessarily the most exciting thing in the world, which is why the headline feature of this new linker is its support for fast incremental compilation. After the recent enhancements, it is now possible (on x86_64 Linux) to perform incremental rebuilds while linking external libraries, C sources, etc—without any additional performance overhead! Here’s a clip of me trying it out on Andrew’s Tetris clone: A few silly changes to Andrew’s Tetris clone being built in around 30ms each.Oh, and fast incremental rebuilds also work nicely on the Zig compiler itself:[mlugg@nebula master]$ zig build -Dno-lib -Denable-llvm -fincremental --watch Build Summary: 4/4 steps succeeded Build Summary: 4/4 steps succeeded Build Summary: 4/4 steps succeeded Build Summary: 4/4 steps succeeded May 26, 2026 Author: Andrew KelleyBig branch just landed: separate the maker process from the configurer processThis devlog entry is essentially a preview of the upcoming release notes, but serves as an advanced notice to those who want to help test out the new features and provide feedback that will guide the Zig project moving forward.Before, build.zig files plus the build system implementation were all compiled into one bloated process, in Debug mode. After build.zig logic finished constructing a build graph in memory, the “build runner” code executed it.Now, build.zig files are compiled into a small process (the “configurer”) in debug mode. After this logic finishes constructing a build graph in memory, it is serialized to a binary configuration file. The parent zig build process is aware of this file and caches it for next time. While waiting for all that, it asynchronously compiles the build graph execution process (the “maker”) in release mode. Once the configuration file is available and the maker process is finished compiling, the maker process is executed, passing it the configuration file. The maker process only needs to be compiled once per zig version thanks to the global cache. The maker process then executes the build graph, which is contained within the serialized configuration file.The primary motivation of this change was to make zig build faster, in three ways:Only the user’s build.zig logic will be compiled with each change, rather than the entire build system along with it. This is starting to become more valuable now that we have introduced --watch, --fuzz and --webui. The build system can grow more features without making zig build take longer.Now the build system can skip rerunning the build.zig logic entirely when it knows nothing will change, for example if you add -freference-trace to your zig build command line, it now avoids re-running your build.zig logic redundantly, using the same configuration as last time.Now the process that actually executes the build graph is compiled with optimizations enabled.To demonstrate points 2 and 3, here is the difference between running zig build --help before and after:Benchmark 1 (34 runs): master/zig build -h ⬇️run_cmd.addPassthruArgs(); This removes a capability from build scripts since they can no longer observe those arguments. In exchange, it means that when changing those arguments, build scripts no longer must be rebuilt from source.If you’re someone who wants to influence the direction of Zig, this is a good time to upgrade your projects to the development version and try out these changes. We’ll be releasing 0.17.0 within a couple weeks from now. However, if you don’t have time, and you find out that 0.17.0 broke your build, don’t worry, there will be plenty of opportunity to get fixes in for the 0.17.1 tag as well. April 08, 2026 Author: Matthew LuggI’ve been spending a bit of time working on personal projects after merging my type resolution changes last month, but I did find the time recently to make some improvements to the LLVM codegen backend. This involved a few different enhancements with various goals, but one nice user-facing change was that I managed to get incremental compilation working with the LLVM backend.Sadly this can’t do anything to speed up the dreaded LLVM Emit Object: that time is entirely down to LLVM. However, what incremental compilation does help with is minimizing the time spent in the actual Zig compiler code, which means that if your code has compile errors (so “LLVM Emit Object” will be skipped), you’ll usually get those errors very quickly. (Of course, it does still give you a slight speed-up in successful builds too.)This support is available in master branch builds right now, and will be in the 0.16.0 release (which we’ll be tagging very soon).For anyone who still hasn’t tried it, especially if you’re using Zig’s master branch, please do try out incremental compilation by passing -fincremental --watch to zig build! The Zig core team have benefited from incremental compilation in our workflows for a good year now, and we’re also hearing good things from users. The feature is relatively stable at this point, and people are often surprised how much time they can save just by getting up-to-date compile errors in milliseconds rather than seconds.I haven’t really personally used incremental compilation with the LLVM backend, but all of the incremental test coverage in CI is now enabled for the LLVM backend, and I’ve had positive feedback from users, so it’s definitely worth giving a shot. As always, if you encounter bugs in incremental compilation, please report them if you can!Thank you, and I hope you find this useful :) March 10, 2026 Author: Matthew LuggToday, I merged a 30,000 line PR after two (arguably three) months of work. The goal of this branch was to rework the Zig compiler’s internal type resolution logic to a more logical and straightforward design. It’s a quite exciting change for me personally, because it allowed me to clean up a bunch of the compiler guts, but it also has some nice user-facing changes which you might be interested in!For one thing, the Zig compiler is now lazier about analyzing the fields of types: if the type is never initialized, then there’s no need for Zig to care what that type “looks like”. This is important when you have a type which doubles as a namespace, a common pattern in modern Zig. For instance, when using std.Io.Writer, you don’t want the compiler to also pull in a bunch of code in std.Io! Here’s a straightforward example:const Foo = struct { Previously, this code emitted a compile error. Now, it compiles just fine, because Zig never actually looks at the @compileError call.Another improvement we’ve made is in the “dependency loop” experience. Anyone who has encountered a dependency loop compile error in Zig before knows that the error messages for them are entirely unhelpful—but that’s now changed! If you encounter one (which is also a bit less likely now than it used to be), you’ll get a detailed error message telling you exactly where the dependency loop comes from. Check it out:const Foo = struct { inner: Bar }; $ zig build-obj repro.zig February 13, 2026 Author: Andrew KelleyAs we approach the end of the 0.16.0 release cycle, Jacob has been hard at work, bringing std.Io.Evented up to speed with all the latest API changes:io_uring implementationGrand Central Dispatch implementationBoth of these are based on userspace stack switching, sometimes called “fibers”, “stackful coroutines”, or “green threads”.They are now available to tinker with, by constructing one’s application using std.Io.Evented. They should be considered experimental because there is important followup work to be done before they can be used reliably and robustly:better error handlingremove the loggingdiagnose the unexpected performance degradation when using IoMode.evented for the compilera couple functions still unimplementedmore test coverage is neededbuiltin function to tell you the maximum stack size of a given function to make these implementations practical to use when overcommit is off.With those caveats in mind, it seems we are indeed reaching the Promised Land, where Zig code can have Io implementations effortlessly swapped out:const std = @import("std"); pub fn main(init: std.process.Init.Minimal) !void { var threaded: std.Io.Threaded = .init(gpa, .{ return app(io); fn app(io: std.Io) !void { $ strace ./hello_threaded pub fn main(init: std.process.Init.Minimal) !void { var evented: std.Io.Evented = undefined; return app(io); fn app(io: std.Io) !void { execve("./hello_evented", ["./hello_evented"], 0x7fff368894f0 /* 98 vars */) = 0 February 06, 2026 Author: Andrew KelleyIf you have a Zig project with dependencies, two big changes just landed which I think you will be interested to learn about.Fetched packages are now stored locally in the zig-pkg directory of the project root (next to your build.zig file).For example here are a few results from awebo after running zig build:$ du -sh zig-pkg/* February 03, 2026 Author: Andrew KelleyThe Windows operating system 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.This is why the Zig standard library policy is to Prefer the Native API over Win32. 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: EntropyAccording 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 NtWriteFileReadFile looks like this:pub extern "kernel32" fn ReadFile( NtReadFile looks like this:pub extern "ntdll" fn NtReadFile( 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 KelleyOver 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 { 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. |
Recent development in the Zig programming language has focused heavily on enhancing compiler internals, optimizing the build system, advancing standard library implementations, and improving the package management workflow. Matthew Lugg introduced significant updates concerning the ELF linker and incremental compilation, while Andrew Kelley focused on reshaping the build system architecture, integrating advanced I/O mechanisms, and restructuring the standard library. Matthew Lugg's work on the ELF linker introduced substantial progress, advancing support beyond linking Zig-only code to enabling the building of the entire Zig compiler with LLVM and LLD libraries integrated. A key achievement of this linker is support for fast incremental compilation, allowing incremental rebuilds while linking external libraries without incurring significant performance overhead on x86_64 Linux. This incremental compilation, supported by the LLVM backend, minimizes time spent in the actual Zig compiler code, particularly when errors occur, as it surfaces compilation errors in milliseconds rather than seconds. While this feature is powerful for debugging, Lugg noted that support for generating DWARF debug information remains a future priority. Furthermore, refinements to the type resolution logic improved the compiler's behavior; for instance, the compiler is now lazier about analyzing uninitialized type fields, which is beneficial when types are used as namespaces, and the error reporting for dependency loops has been significantly improved, providing more detailed and actionable error messages. Andrew Kelley's contributions addressed workflow and system-level features. Regarding the build process, the build system was reworked to separate the maker process, which executes the build graph, from the configurer process, which handles configuration serialization. This change aims to improve build speed by caching configuration files and compiling the execution process asynchronously in release mode, allowing the build system to skip unnecessary recompilations when inputs are unchanged. This architectural shift results in dramatic performance improvements observed in benchmarking, particularly for build execution time and memory usage. When interacting with external systems, Kelley’s work on std.Io successfully implemented I/O using user space stack switching, integrating io_uring and Grand Central Dispatch. This implementation leverages concepts like fibers or green threads to manage I/O, though some performance degradation is still noted, indicating areas for further optimization. The package management workflow was also enhanced by introducing local storage for fetched packages in a project-local directory, which facilitates offline building and archival, alongside a globally cached copy. Additionally, the introduction of the --fork flag enables developers to temporarily override dependency trees during development, which supports iteration over ecosystem breakage. This workflow enhancement aims to allow developers to work on their source control independently of upstream changes. In the area of the standard library, Andrew Kelley worked on the zig libc subproject to reduce redundancy. This involved transitioning libc functions to Zig standard library wrappers rather than vendoring C source files, which leads to reduced installation sizes and improved compilation speed by enabling shared compilation units. This transition provides a foundation for future integrations, such as potentially forcing all libc I/O calls to participate in an io_uring event loop. Moreover, Kelley explored moving beyond kernel32.dll wrappers, advocating for the use of Native APIs like NtReadFile and NtWriteFile to avoid unnecessary heap allocations and failure modes associated with older Windows APIs. This pursuit emphasizes using lower-level primitives to achieve better control, error reporting, and integration capabilities for asynchronous file operations. |