Zig vs. Rust in 2026

ibobev1 pts0 comments

Zig vs Rust in 2026

5/11/2026<br>Zig vs Rust in 2026

{pubDate.toString()}} -->

Nearly 3 years ago, before coding agents, I wrote a bytecode VM and garbage<br>collector in Zig and unsafe Rust, and felt that the human ergonomics of writing<br>unsafe code was in Zig’s favor.

Unfortunately, I’m writing less and less code by hand nowadays, meaning I have<br>less and less reasons to use Zig now. Every 1.5-5x human DX productivity boost<br>from Zig features is eclipsed by the 100x boost from coding agents in Rust.

Many of Zig’s greatest features were designed for human ergonomics, but this<br>doesn’t really matter that much to agents.

I’ll talk about my favorite Zig features and why they don’t matter that much<br>anymore.

Allocator interfaceλ

This is my favorite Zig feature, you feel so galaxy brain using a specialized<br>allocator to optimize a code path (e.g. arena, stack fallback etc).

// A real example:<br>// Reading a line of user input requires a heap allocator because<br>// in theory the input length is unbounded. In practice though,<br>// the input is almost always a short search query or path that<br>// fits in well under 1kb.<br>//<br>// stackFallback puts a fixed-size buffer on the stack and only<br>// hits the heap when the input overflows it. The common case<br>// costs zero heap allocations. The rare long input still works<br>// because the allocator silently upgrades to the heap.<br>var stack_fallback = std.heap.stackFallback(256, heap_allocator);<br>const alloc = stack_fallback.get();

const line = try reader.readUntilDelimiterAlloc(alloc, '\n', 4096);<br>defer alloc.free(line);

The problem in Rust used to be that there was no Allocator interface equivalent<br>and if you wanted a Vec that used a custom allocator you literally had to<br>copy+paste the std version and modify it to use it (this is what Bumpalo did,<br>look at the source, the collections are forks of the std versions wired up<br>to the bump allocator).

For a long while now there has been an Allocator trait in nightly, and it<br>seems to be good now. Because it is a trait it is static dispatch, vs Zig’s<br>which is based on a vtable.

Unlike Zig there isn’t a community-wide convention of designing data structures<br>to be parametric based on the allocator, but AI changes the game and makes it<br>trivial to copy paste code and change that. I find it works well enough for my<br>use-case.

Arbitrary bit width integers + packed structsλ

Another beloved Zig feature of mine. It makes it so easy to do DOD-style CPU<br>cache optimizations and stuff like tagged pointers, NaN boxing, etc. and even<br>made bitflags really easy to make.

Here’s a real example. When using Obj-C APIs like Metal through the<br>Obj-C runtime C API, an id can be a tagged pointer instead of an aligned<br>heap object pointer. I hit UB by passing a tagged NSNumber through code that<br>assumed alignment.

So you need a cheap “heap pointer vs tagged immediate” check. Here’s a<br>simplified Objective-C tagged pointer layout: one low bit says “not a heap<br>pointer”, the next 3 bits identify the class slot, and the remaining 60 bits are<br>payload:

pub const TaggedClass = enum(u3) {<br>ns_atom = 0,<br>ns_string = 1,<br>ns_number = 2,<br>ns_date = 3,<br>};

pub const ObjcTaggedPointer = packed struct {<br>is_tagged: bool = true,<br>class: TaggedClass,<br>payload: u60,

pub fn ns_number(n: u60) ObjcTaggedPointer {<br>return .{ .class = .ns_number, .payload = n };

pub fn from_raw(raw: u64) ObjcTaggedPointer {<br>return @bitCast(raw);

pub fn raw(self: ObjcTaggedPointer) u64 {<br>return @bitCast(self);

pub fn is_ns_number(self: ObjcTaggedPointer) bool {<br>return self.is_tagged and self.class == .ns_number;<br>};

The Rust equivalent OR-s the Objective-C class slot in on construction and masks<br>it out on every access. The slot is just a u64 constant, not a real type:

pub struct ObjcTaggedPointer(u64);

impl ObjcTaggedPointer {<br>const TAG_MASK: u64 = 0b1;<br>const CLASS_MASK: u64 = 0b1110;<br>const CLASS_SHIFT: u64 = 1;<br>const PAYLOAD_SHIFT: u64 = 4;

const CLASS_NS_ATOM: u64 = 0;<br>const CLASS_NS_STRING: u64 = 1;<br>const CLASS_NS_NUMBER: u64 = 2;<br>const CLASS_NS_DATE: u64 = 3;

pub fn ns_number(n: u64) -> Self {<br>Self(<br>(n Self::PAYLOAD_SHIFT)<br>| (Self::CLASS_NS_NUMBER Self::CLASS_SHIFT)<br>| Self::TAG_MASK,

pub fn is_ns_number(self) -> bool {<br>self.0 & Self::TAG_MASK != 0<br>&& (self.0 & Self::CLASS_MASK) >> Self::CLASS_SHIFT == Self::CLASS_NS_NUMBER

You can see the native Rust way is unergonomic. You’re actually better off using<br>some crate like bitfield/bitflags which both rely on proc macro magic to work,<br>and I don’t find as nice as Zig’s packed structs.

However, with coding agents I literally do not care how annoying it is to write the<br>code by hand.

Comptimeλ

This is Zig’s flashiest feature, no other programming language except maybe for<br>obscure dependent-types langs have compile time evaluation as nice as Zig’s.

I thought I would miss it a lot, but I actually don’t. For me, 95% of comptime<br>usage is to create Zig’s version of generic data structures with parametric<br>types, e.g.:

fn ArrayList(comptime T: type) type {<br>return...

self const allocator rust heap objctaggedpointer

Related Articles