I was early onto the Rust train. I started my career as a software engineer mere weeks before rust was first released, and within 3 years I was pushing it onto companies that I worked at. As an engineer that has always been intrigued by lower level engineering, and constantly in the pursuit of “efficient code” (as it pertains to the subject), rust was insane. I could get to the same control over the computer (nearly) that I had with C, but I didn’t have to worry that each early return from a function properly free()
d the memory that had been allocated. I already had refused to seriously learn C++ (something I am proud to maintain to today), so it was nice to have something a bit extra while working at a lower level (comparatively). It felt like if you took the batteries included of Java (basic language extensions like Hashmap are quickly missed in basic C) but without any JVM. Sure, Go had been around, and I really pushed for Go, but being able to get rid of GC and have straight forward memory allocation and deallocation felt magical.
I don’t think anyone would argue that rust is the first true modern high-performance language. It has a number of modern amenities you don’t find in older languages like C, but includes the memory safety engineers have come to rely on with GC’d programs. No controversial take here, rust is C for the modern day (circa 2016). I’ve written my fair share of rust both personally and professionally. I’ve yet to have the chance to work in a large rust codebase, but I’ve played with many different aspects of the language to a fairly deep level, though I won’t claim to be a macro master or that good at handling lifetimes. And I’ll still say it, a lot of the built-ins are still super nice. Having Arc::new()
anytime you want, throw it into a RWLock
and you have high performance reads with memory safety across threads. It is insane the performance you can get out of very little effort in rust.
But as anyone who has written a decent amount of rust will tell you, when you write rust code, you are writing rust code. This is fully an artifact of the design of the language on a deep level, with the ever mysterious borrow checker being the largest contributor. Mostly it centers around the core decision of the language to ALWAYS know where memory is and what it is doing, even at compile time. For the most part, I view it as a quirk of the language, but nothing too serious. If you’ve written rust for a few days you start to get into the swing of it and it starts to feel natural again. But there’s always that friction, that slight rewiring that has to happen. I guess you could ignore it and just copy()
everything you come across, but you’ll run into the bottlenecks of doing that, and you’ll have to confront the beast. An Rc
or Arc
can help, but then you gotta make sure you’re maintaining your references, so you’re just passing things around everywhere. It can quickly become a bit annoying to have to bend to the borrow checkers will.
Then comes along Zig. A quick google lets me know it showed up in 2016, but I don’t know if I was too aware of it before 2018. Zig is something interesting. On the surface, what it does and provides is really basic and obvious, but it seems to be the only language doing it. To start, it is so close to C its almost scary, the zig compiler will compile C just as happily as it’ll compile zig. You have all the control you could want to throw memory around as you please. The zig language then took this solid base of… C, and went “how do we make it modern?”. Their answer was some basic features. Just throw in type inference and some basic niceties for memory management and bobs your uncle, you got a modern C.
The big difference here is exemplary of a larger debate in computer science around how programs should be written. Rust, with its use of the borrow checker, is further into the camp of “code correctness”. This is the idea that you could reduce all of your programs to something akin to a math problem, and then write a proof for it. If you are able to strongly prove your code, then its “correct”. I tend to like this idea and find it very attractive, but I think it has a very core issue. Not everything in the world can be known all the times. This is most obvious when you have to cross a network boundary in your code. You could “prove” that your handling of an API response is “correct”, but what happens when the service goes down and now its giving you an Nginx 504? Do you handle the 504 correctly? Maybe you handle error codes, but what about timeouts, or not having a network connection, or a server not responding in the correct format. At some point, you’re having to assume you’re given some constraints on your inputs when that is not always the case. A network boundary introduces limitless possibilities, and while you could try to prove that you just handle all invalid input the right way, it just starts to get weird. This is where the weirdness I find in rust comes in. To be able to prove you’re handling memory in the way the borrow checker wants you to, you are forced to write your code in a specific way.
Zig on the other hand seems to take the more practical approach. The zig compiler does a lot of work around types, and the type inference system is great, but it doesn’t force you to handle your data in any specific way, just to let it know how to size things. When it comes to memory management, instead of going full boar like rust and introducing a borrow checker or some other kind of compile-time proofing code, they just provided some basic utilities to make sure the memory you allocate you can make sure it gets deallocated. This is mostly through the defer
keyword, pretty much a quick way to add a finally
block to your function where you can free
all of your allocated code. And the extra nice thing is that the defer
keyword allows you to put it anywhere in your function, and when it goes out of scope, the defer
statement will be run. This lets you do something akin to
var a = std.Arraylist(u8).init()
defer a.deinit()
I know all the go programmers out there are screaming that Go has had that for a while. Its true, but its just not as useful without including the manual memory management. Its nice for things like making sure you always close your file pointer, but its just not as powerful when you’re still relying on some GC tracking of your memory to clean things up. This opens up so much that isn’t possible in (some) GC’d languages, circular references anyone? It also removes any and all overhead of doing GC, removing the days of profiling on a sufficiently large GC’d project trying to get the GC to run when you want it to and not when you don’t.
While utilizing the defer
keyword makes it so that we can’t prove the memory is handled correctly, it makes it easy enough to handle it correctly that even if something is missed it is easily fixed. Its so much less overhead to make sure you defer
any malloced memory as compared to ensuring that all branching paths in your code include a free
. The cherry on top is that none of these new parts of Zig force you to write your code any differently. While there are patterns that are impossible in rust due to the borrow checker, zig is as flexible as any other language, and really is most comparable to C.
I know theres probably some rust or maybe a haskell developer or two that is going to get on me about correctness of programs, but I’d assert that, especially when fully utilizing the computing power of modern machines, its harder than ever to truly prove any program. Multithreading, networking, speculative execution (spectre but still), these and so many more introduce enough instability into the system that any proof could at best be analytical. So maybe instead of trying to prove the programs we write mathematically, we can use tools we know work for code. Zig makes testing just as easy as it is in rust, letting you inline tests with your source. Type checking catches a ton, and makes development quick. Strong typing allows the same memory tricks you can do with C, something that is much harder to do in Rust (data before a pointer?). These checks can catch a vast majority of issues in code when utilized, and have much less restrictions implied by themselves like a proofer such as the borrow checker. A personal favorite for the argument of “correct” computing is the Timsort, a sort made combining a slow insertion with a quick merge sort. It turns out that time and time again, in real world situations, Timsort performs better than a more “correct” choice such as a quicksort. It even falls apart at sufficiently large inputs, but the real-world uses of it are so wide that it is the built in sort for several languages, and quickly gained dominance when it appeared on scene (python has now gone to powersort which seems to be a refinement of timsort).
For me, when I first used zig (which has mostly just been once for a personal project), I was blown away. It felt like using rust for the first time again, except when I went to compile, instead of being told I wasn’t moving my memory around in a way the borrow checker approved of, it just worked. Sure, I ran into my fair share of segfaults and leaks, but the language made it so easy to patch up the leaks, and the little bells and whistles that are included in the standard library make segfaults much less common than writing C (sometimes you just really want a vector that just works, and not having to realloc
your own buffer all the time). Something else also happened to me when I first wrote Zig. Rust all of a sudden seemed… clunky? Like it just needed you to do so much and bend over so far to please the borrow checker, whereas with zig I could just… write my code. Before it felt like the borrow checker was a copilot, making sure I was following the general course of memory safety while I charted out the path of what the code should actually do. After trying zig it felt like a nanny, constantly telling me that I was doing it wrong, even when I knew what I was doing. Zig felt much closer to the right balance of memory safety and modern features with control and low level performance.
So what of it? Professionally for the last few years I’ve been pretty much stuck on web. I was able to ride a new management hire into pushing some rust into the otherwise js/python/c++ landscape, but I haven’t hardly made a peep about Zig. Personally, this comes down to two things. One is just the overall general awareness. Most engineers nowadays know of rust, and will generally have some idea of what it is. I just don’t find the same to be true of Zig. Its hardly that much newer, but rust just really took off with the mozilla backing and quickly took over the mind share for high performance memory safe languages. And this leads to the second, which is just the relative immaturity of the wider community support for zig. With smaller general awareness has come a smaller overall community. This is generally not the worst thing, and zig by no means has a small community, but when choosing languages for a whole company to rely on it can be a big make or break point. Its much harder to sell something where we may have to write some complicated code for some small tangential need that is in no way related to the work we’re trying to do, just because the community isn’t large enough to have a package available that helps. While this cargo cult programming has its own downsides, when working on small teams with limited resources it can be life or death.
So while Zig isn’t a big part of my day to day life, my experience with it has really changed my outlook. When I’m reviewing egui code it all just starts to look like old Java EE applications. I know its harsh to compare rust to something as horrid as Spring, but the boilerplate sticks out to me much more now. Looking at rust feels like looking at explaining code to the borrow checker, not writing code to do something. I’m still yet to go back to Zig, and have actually recently picked back up C to play with raylib and its been a nice break from the React TS hell I live in every day, but it does have me thinking about Zig. I have a feeling once I get tired of my memory leaking into swap I’ll reach for zig, but I know for a fact that Zig is my de-facto modern high performance language, and I think it has a great case to make for itself to really reign in that title one day.