|
|
Subscribe / Log in / New account

Zig's 0.14 release inches the project toward stability

By Daroc Alden
March 12, 2025

The Zig project has announced the release of the 0.14 version of the language, including changes from more than 250 contributors. Zig is a low-level, memory-unsafe programming language that aims to compete with C instead of depending on it. Even though the language has not yet had a stable release, there are a number of projects using it as an alternative to C with better metaprogramming. While the project's release schedule has been a bit inconsistent, with the release of version 0.14 being delayed several times, the release contains a number of new convenience features, broader architecture support, and the next steps toward removing Zig's dependency on LLVM.

More targets

Zig tracks the different architectures it supports using a tier system to describe different levels of support. This release looks as though it demotes all tier-1 targets except for x86_64 on Linux to tier-2, but that's actually an artifact of changing how the project defines its tiers. tier-1 targets used to be the ones with working code generation, a fully implemented standard library, regular continuous-integration testing, all language features known to work, and a few other conditions. The 0.14 release adds a requirement that the compiler can generate machine code for the target without relying on LLVM for it to be considered tier-1.

Zig has been trying to drop its dependency on LLVM for some time. The first step of that process was switching to the self-hosting compiler; once that was complete, the project started focusing on adding native code generation. This release includes a mostly-working x86_64 code generator, although LLVM remains the default. If it proves stable, the Zig code generator may become the default for debug builds (where performance impacts from a less-mature optimizer are less important) in the next release. The release also includes an (experimental) incremental-compilation mode. When combined with the Zig code generator, the new mode makes debug builds quite fast.

Besides the code generation changes, Zig's list of supported targets has significantly expanded in this release, largely due to additional support in the standard library for different platforms. The release notes make this bold claim:

The full list of target-specific fixes is too long to list here, but in short, if you've ever tried to target arm/thumb, mips/mips64, powerpc/powerpc64, riscv32/riscv64, or s390x and run into toolchain problems, missing standard library support, or seemingly nonsensical crashes, then there's a good chance you'll find that things will Just Work with Zig 0.14.0.

Pages and allocation

The 0.14 release also saw a major change to the way the language handles different page sizes. It used to be that each architecture was expected to have a single, static page size. This doesn't match how some architectures (notably Apple Silicon) work — the page size can vary at run time. The new Zig release switches to having two compile-time constants: a minimum and maximum size. At run time, the program can detect what the actual page size is.

This change required rewriting Zig's general-purpose memory allocator, because it assumed that the page size was fixed. Andrew Kelley, Zig's founder, wrote about the improvements that he was able to make in rewriting the allocator. In short, the old general-purpose allocator is now Zig's debug allocator, and a new multi-threaded allocator should be Zig users' first choice.

Kelley has some benchmarks showing that the multi-threaded allocator actually outperforms the GNU C library's (glibc's) memory allocator — although, as is always the case with benchmarks, simple tests can be misleading. The multi-threaded allocator is about 10% faster when using the compiler to compile itself, but Zig's compiler has a somewhat unusual approach to memory use, so whether the gains translate to other programs remains to be seen. One potential complication is memory fragmentation; allocators can trade decreased time for increased fragmentation or vice versa. Zig programmers can experiment with the multi-threaded allocator, the debug allocator, glibc's allocator, or others in order to find what works best for their programs.

Labeled switches

The biggest change to the language itself is the addition of labeled switch statements. In Zig, continue statements require specifying the label of the loop that is being continued to. This helps avoid confusion when there are nested loops in a program. Now, switch statements may be given optional labels, so that continue statements can target them. This turns switch statements into implicit loops, since control flow can return to the start of the statement. The intended use case is writing more efficient byte-code interpreters (a topic that LWN discussed in a recent article), although the feature also makes writing some finite-state automatons more straightforward.

    state: switch (State.start) {
        .start => switch (self.buffer[self.index]) {
            '0'...'9' => {
                result.tag = .number;
                continue :state .number;
            },
            else => continue :state .invalid,
        },
        .number => { ... },
        .invalid => { ... },
    }

When the values given to the continue statements are known at compile time, the compiler produces direct jumps. When they are only known at run time, the compiler produces a separate indirect jump for each case, which helps the CPU's branch predictor learn which cases are likely to follow which other cases. The first implementation of the feature ran into problems with an LLVM optimization pass that combined the jumps into one, but the version present in the release doesn't have that problem.

Another change that makes writing Zig programs more pleasant has to do with how the language interprets enumeration literals. Zig supports a limited form of type inference in the form of "Result Location Semantics"; when the compiler knows what type the result of an expression should have, that information can be used to automatically perform certain casts, or provide a shorter syntax. For example, when the compiler knows that the result of an expression is a value of an enumeration type, the programmer can leave the name of the type out — writing .number in place of State.number, as in the example above.

The 0.14 release extends this shorthand to things that aren't enumerations. Now, when the compiler sees the expression ".name", it will look for an associated constant with that name on the inferred type of the expression; if one exists, the value of that constant will be used. Besides being a useful shorthand, the new syntax lets developers rename an enum variant in-place without needing to update all the callers by defining the old name as an alias for the new one. In combination with improvements to packed structures — allowing them to be compared for equality and operated on atomically — Zig programmers have a good deal of freedom to create types that seem like simple enumerations while containing other information as well.

A practical example of this use in the standard library comes from Zig's support for different calling conventions. Zig functions can optionally be given a callconv() annotation in order to set their calling convention:

    pub fn callable_from_c() callconv(.C) void { ... }

Previously, this annotation took an enumeration type specifying one of a handful of alternatives. Now, the standard library has changed it to a tagged union, so that different calling conventions can add additional configuration options. Most of them permit setting the required stack alignment, for example. The previous versions have become constants defined for the type, so the new literal syntax ensures existing code doesn't need to change.

The future

Zig's development community remains active, with 350 commits by 58 authors over the past month — a speed that's fairly typical for the project. Despite this, the 1.0 release of the language remains several years away. Kelley has already made a strong commitment to backward compatibility once Zig reaches version 1.0, and consequently has a long list of experimental features and deficiencies in the current implementation that should be solved before then.

The number of actual changes to the language itself in each release seems to be slowing down, with this release boasting many small changes, as opposed to the wide-reaching differences of previous releases. That stability seems to have afforded the project the chance to spend time improving the tooling instead, as demonstrated by the expanded number of supported architectures and improvements to the compiler's backend. Despite that improvement, there are still a large number of known problems with Zig's tooling, not least of all because of how expansive that tooling is. Zig includes a lot of tools that other languages do not — a translator for C code, a cross-platform C compiler, a build system, and (new in this release) a built-in fuzzer — and it seems likely that the development cycle for the next release, expected in about six months, will be spent improving them.



to post comments

I'd love to see Zig work on libgccjit based code generation

Posted Mar 12, 2025 15:22 UTC (Wed) by darcagn (guest, #168667) [Link]

Rust is working on using GCC's libgccjit as a layer to leverage GCC for AOT code generation (this is rustc_codegen_gcc), I'd love to see Zig try to do the same, and maybe even help antoyo with improvements to libgccjit to help facilitate this.

Zig for C compilation

Posted Mar 12, 2025 18:07 UTC (Wed) by Cyberax (✭ supporter ✭, #52523) [Link] (3 responses)

One really nice use for Zig is to use it to cross-compile C! It just works.

I'd been using `CC="zig cc -target aarch64-macos-gnu"` to cross-compile tools that need Cgo in Go for macOS without having to tear out my hair in frustration.

Zig for C compilation

Posted Mar 13, 2025 23:42 UTC (Thu) by ringerc (subscriber, #3071) [Link] (2 responses)

I assume you still need to build target arch specific dependencies though? So how does that work for platform libraries that may not be available locally?

Do you need an unpacked xcode tree etc for the headers? Copies of OSX .dylib s as link targets?

Zig for C compilation

Posted Mar 14, 2025 1:44 UTC (Fri) by Cyberax (✭ supporter ✭, #52523) [Link]

You need a cross-compile toolchain, which is amazingly OpenSource. But you'll need XCode if you want something that's not in the C stdlib.

Zig for C compilation

Posted Mar 14, 2025 7:19 UTC (Fri) by mathstuf (subscriber, #69389) [Link]

> Copies of OSX .dylib s as link targets?

Note that Xcode hasn't linked to `.dylib` files for system libraries for years. There are now `.tbd` files which are kind of like Windows' `.lib` files in that they are linkable but direct runtime loading of other files except they are JSON and describe what symbols are available in which architectures along with some other metadata.

Thinking on it, it'd be really nice if ELF linkers supported something like that so that one could link to `glibc-2.18.symtab` or something to limit oneself to a glibc-2.18 runtime without actually having to execute in such a location. Probably doable with linker scripts, but I feel like a declarative file would be easier for tooling across the ecosystem rather than writing (subtly broken) linker script interpreters all over the place.

Finally, labelled continue

Posted Mar 12, 2025 18:37 UTC (Wed) by epa (subscriber, #39769) [Link] (20 responses)

In Zig, continue statements require specifying the label of the loop that is being continued to. This helps avoid confusion when there are nested loops in a program.
Aaaahhh. And can we now have that in other languages? At least the ability to give the label, even if it's not made compulsory as in Zig. And ideally with the ability to continue or break an enclosing loop if you specify its name.

Perl very nearly gets it right, allowing any loop to have a label, but the labels are not checked at compile time.

Finally, labelled continue

Posted Mar 12, 2025 18:39 UTC (Wed) by mb (subscriber, #50428) [Link] (2 responses)

> And can we now have that in other languages?

Rust also has that feature.

Finally, labelled continue

Posted Mar 12, 2025 21:54 UTC (Wed) by tialaramex (subscriber, #21167) [Link]

In fact, since Rust is expression oriented (rather than statement oriented) we can break from any expression to a label containing that expression _with_ a value of the same type as the expression if the expression has a type other than the unit / empty tuple (). We can't do this with continue, only with break.

In the middle of your sixteen line expression labelled 'find: which is checking all the Hay in the Haystack with the intent that the expression will have the result None when it can't find what it wanted the code might instead find a Needle and just

break 'find Some(needle);

This is very similar to what you'd write at the function level, where you might ordinarily return None after exhausting the loop, but if instead you find it you can return Some(needle) -- sometimes you can't really justify a whole function and so it's annoying if you must write a function to enable this nice behaviour.

There was considerable push back to break 'label value for Rust years ago, but it was eventually added to the language. I have used it maybe once every couple of years, so, not often but enough that I think it was the right call.

N3355 will add this to some future C and P3568 looked for direction from WG21 and got none, so C++ will probably inherit whatever C standardises.

Finally, labelled continue

Posted Mar 25, 2025 15:48 UTC (Tue) by jezuch (subscriber, #52988) [Link]

And Java too. In fact, in Java you can label any block and break out of it, not just loops.

I dare you to use that feature, though ;)

And it's not *requiring* labels on break and continue, which is the most useful part, probably.

Finally, labelled continue

Posted Mar 12, 2025 22:33 UTC (Wed) by fenncruz (subscriber, #81417) [Link] (1 responses)

Fortran has named do loops (for loops) as well as exiting (or continuing) outer loops based on name.

Finally, labelled continue

Posted Mar 12, 2025 22:47 UTC (Wed) by Wol (subscriber, #4433) [Link]

I thought I remembered that, so that must be Fortran 77. I can't understand why people would think it a bad idea ...

Cheers,
Wol

Finally, labelled continue

Posted Mar 13, 2025 11:59 UTC (Thu) by Karellen (subscriber, #67644) [Link] (14 responses)

Isn't this just a goto? Why not use goto instead?

Finally, labelled continue

Posted Mar 13, 2025 12:54 UTC (Thu) by excors (subscriber, #95769) [Link] (13 responses)

> Isn't this just a goto?

No, because it's much more constrained than goto - you can only jump to the top/bottom of an enclosing lexical scope.

> Why not use goto instead?

As Dijkstra pointed out, unconstrained goto leads to programs with control flow that is too hard to reason about. You'll often have code that enforces an invariant, syntactically followed by code that relies on that invariant, and it'll break if another part of the program can jump into the middle of those two sections. (E.g. you have code that initialises a local variable, followed by code that uses that variable.)

continue/break largely avoids that problem because the program can only jump outwards to a boundary between scopes, and the boundary is visually obvious to the programmer (through indentation etc), so you'll know to be careful about invariants across that boundary. It's still harder to understand than a language with very simple control flow and no continue/break/goto/etc, but that's a tradeoff with expressivity.

It looks like Zig used to have goto, but they removed it at the same time as they added labeled continue/break, because they couldn't find compelling use cases that couldn't be rewritten with defer/continue/break (https://github.com/ziglang/zig/issues/630).

Java also has labeled continue/break and no goto, so it's a well established design and probably a good tradeoff for most languages.

Finally, labelled continue

Posted Mar 13, 2025 17:44 UTC (Thu) by tialaramex (subscriber, #21167) [Link] (12 responses)

Dijkstra is concerned about the actual unconstrained go-to feature which is long dead, nothing you would use today has this language feature outside of assembler - for exactly the reason you describe, it's not practical to do engineering with 1MLOC, 10MLOC and bigger software stacks that are written today while having go-to.

C has only a neutered goto statement, you can only goto a label in the same execution of the same function, so you can jump out of a loop (like break) or back to another iteration of that loop (like continue) but not much more.

Break and continue are nicer semantically than goto, but I can see certainly in C you might value having this very terse single feature (the neutered goto) rather than adding labelled-continue and labelled-break. There was some real push-back to labelled-continue for C and C++ but it wasn't very passionate. Some people want it, some people would rather not, nobody was dying on any hills.

Finally, labelled continue

Posted Mar 13, 2025 18:12 UTC (Thu) by Wol (subscriber, #4433) [Link] (4 responses)

> Break and continue are nicer semantically than goto, but I can see certainly in C you might value having this very terse single feature (the neutered goto) rather than adding labelled-continue and labelled-break. There was some real push-back to labelled-continue for C and C++ but it wasn't very passionate. Some people want it, some people would rather not, nobody was dying on any hills.

I've used "goto" in C. ONCE.

My colleague said I wasn't allowed to use it (despite the user's spec "requiring" it). I said "you forbid goto, you write the code". He replied "all yours" so I used it.

Cheers,
Wol

Finally, labelled continue

Posted Mar 13, 2025 21:10 UTC (Thu) by dskoll (subscriber, #1630) [Link] (3 responses)

I haven't looked at Linux kernel code in quite a while, but I recall it used this idiom quite a bit.

thing1 = acquire_thing1();
if (!thing1) goto bailout1;

thing2 = acquire_thing2();
if (!thing2) goto bailout2;

thing3 = acquire_thing3();
if (!thing3) goto bailout3;

/* do work */
free_thing3(thing3);
bailout3: free_thing2(thing2);
bailout2: free_thing1(thing1);
bailout1: return whatever;

Finally, labelled continue

Posted Mar 14, 2025 0:34 UTC (Fri) by NYKevin (subscriber, #129325) [Link] (2 responses)

That's just another way of spelling "my language has no defer/RAII/try-with-resources/etc." It's perfectly fine in C, but only because there is no reasonable alternative.

Finally, labelled continue

Posted Mar 14, 2025 1:18 UTC (Fri) by dskoll (subscriber, #1630) [Link]

Yes, definitely.

Finally, labelled continue

Posted Mar 14, 2025 10:32 UTC (Fri) by bluca (subscriber, #118303) [Link]

You can do RAII in C actually, with the cleanup attribute. Works really nicely too

Finally, labelled continue

Posted Mar 14, 2025 4:38 UTC (Fri) by gutschke (subscriber, #27910) [Link] (4 responses)

C has only a neutered goto statement, you can only goto a label in the same execution of the same function, so you can jump out of a loop (like break) or back to another iteration of that loop (like continue) but not much more.

C is sufficiently flexible that if you absolutely insisted on it, you could put your entire program into the main() function, make all your variables global, and exclusively rely on goto for control flow. You'd effectively write the infamous BASIC "spaghetti" code using C syntax; and you could jump into the middle of control flow to your heart's content.

From a pragmatic point of view, you are of course entirely correct and nobody would do that. In most real-world applications, the restrictions that C puts on goto do apply. And nobody feels the need to go back to Dijkstra's nightmare scenario. I don't often use goto in my own programs, but it does have its place in C. In other languages, there often are more idiomatically correct solutions.

I see the benefits and the potential risks of labelled control flow, and understand why language designers tread extra carefully. I still would like labelled break and continue statements in C though. If only, because it makes the intent more clear.

I also understand why BASIC was prone to all the bad aspects of goto. In a language that has barely usable subroutines, function definitions that are all but pointless, no scoping for variables and lacks user-defined data types, it is very hard to see how you'd implement a sane restricted subset of goto. But that's what you have to put up with, if you want to squeeze the runtime into 1kB of RAM and 8kB of ROM that some of the early devices of that era came with.

Finally, labelled continue

Posted Mar 14, 2025 9:28 UTC (Fri) by tialaramex (subscriber, #21167) [Link] (1 responses)

The computer my family had when I was young (a Commodore Vic 20) had only 4096 bytes of RAM which these days wouldn't even be enough for the L1 data cache integrated into a CPU core, it's possible the "real" CPU registers (not the names visible to a machine code programmer) are 4kB these days.

As I mentioned above C2b (maybe named C29, another ISO C standard in the next few years) is likely to have the labelled break and continue.

Finally, labelled continue

Posted Mar 15, 2025 14:01 UTC (Sat) by willy (subscriber, #9762) [Link]

Certainly if you're willing to include the AVX registers, this is true. Golden Cove has over 300 64-byte physical registers, so that's 20kB. Heck, there's 4k entries in the uOp cache, and those must be on the order of 5 bytes each, so there's another 20kB.

Finally, labelled continue

Posted Mar 14, 2025 23:35 UTC (Fri) by khim (subscriber, #9252) [Link] (1 responses)

> You'd effectively write the infamous BASIC "spaghetti" code using C syntax; and you could jump into the middle of control flow to your heart's content.

And how would you emulate GOSUB? With [computed goto](https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html)? That's GNU C extension, not standard C.

> And nobody feels the need to go back to Dijkstra's nightmare scenario.

“Dijkstra's nightmare scenario” is not possible in C, or even BASIC. For it to make any sense one have to live in the world of Wheeler Jump and FORTRAN 66 or ALGOL 68.

Then you can nicely combine GOTO with the use of procedures and functions.

That world doesn't exist today. Except for assembler and even there it's not common.

Finally, labelled continue

Posted Mar 14, 2025 23:55 UTC (Fri) by gutschke (subscriber, #27910) [Link]

GOSUB was extremely limited in many BASIC dialects, so the Wheeler GOTO isn't as far fetched as you'd imagine. That would certainly have felt quite natural to owners of these early home computers.

And if you're ok with GNU extensions such as computed GOTOs and macro trickery, then I'm confident you could implement the general idea.

Finally, labelled continue

Posted Mar 14, 2025 15:28 UTC (Fri) by ballombe (subscriber, #9523) [Link] (1 responses)

goto are needed because break and continue do not allow to specify how many level of loops to get out of, and the
alternatives are worse.

Finally, labelled continue

Posted Mar 14, 2025 16:20 UTC (Fri) by farnz (subscriber, #17727) [Link]

Labelled break and continue do allow you specify how many levels of loops to get out of - in C-like syntax, it'd look something like:


outer:
while (conditional()) {
    for(i = 0; i < limit; i++) {
        if (is_cont_condition(data[i])) {
            continue outer;
        }
     } else if (is_break_condition(data[i]) {
            break outer;
     }
}

In this case, because "continue outer" and "break outer" specify a label, they leave both the inner loop (for) and the outer loop (while). A plain break; or continue; would only have acted on the inner for loop.

allocator performance

Posted Mar 25, 2025 1:49 UTC (Tue) by kmeyer (subscriber, #50720) [Link]

> Kelley has some benchmarks showing that the multi-threaded allocator actually outperforms the GNU C library's (glibc's) memory allocator

This is a low-ish bar. Glibc's allocator is notoriously bad. The state of the art here is jemalloc or mimalloc.


Copyright © 2025, Eklektix, Inc.
This article may be redistributed under the terms of the Creative Commons CC BY-SA 4.0 license
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds