1. 68
  1. 10

    This is so great - Zig covers a crucial part of the design space that other languages haven’t executed as well or as seriously.

    I have my hopes that some language will attempt to compete and add memory safety in the next five years, but I still think that Zig is adding tons of value to the landscape right now.

    1. 7

      I have, unfortunately, not had time to play with Zig yet, and it’s been on my list forever. I put Rust ahead of it because it appears to have some, erm, green pa$ture$ and you know how the$e thing$ are.

      But what I see from the outside is extremely encouraging. I see non-trivial programs (interpreters, allocators, compilers) with decent failsafe mechanisms, efficiently written in a language much smaller than many of its alternatives in the systems space (C++, Rust, even D, I think, although I haven’t written D in practically forever…), without it having spawned content mills that explain the fifteenth right way to return error codes shortly before it, too, goes out of fashion, or how to get the compiler to not choke on a three-line program.

      This tells me it’s a language (along with a standard library) that hits close to that sweet spot where it has enough features that you don’t have to reinvent too many wheels, but also has few enough features that keeping up with the language isn’t a full-time job that you have to take on top of your existing full-time job lest your code becomes irrelevant by next year. Also that it’s likely to age well, as programs that use the standard library will not be littered with failed experiments in architecture astronautics.

      I dunno, I hope I’m right :-).

      1. 4

        I guess, but the part of the design space that they’ve chosen to cover is use after free vulnerabilities. Even C++ has better memory management through smart pointers.

        This is critical, as while yes out-of-bound errors do happen, the majority of security bugs of the last decade have involved use-after-free vulnerabilities as their primary vector. Explicitly, and deliberately - it’s a design goal - maintaining the malloc/free model that has accumulated decades of evidence that it results in bugs that at least cause data loss, and are frequently exploitable - even indirectly: the notorious “goto fail” bug was because manual memory management frequently requires handling said memory in error cases, so requiring things like fail: labels.

        In terms of OoB errors modern C++ provides std::span, custom data types (see WebKit’s WTF::Vector, etc), etc; all can/do perform bounds checking and don’t require re-writing everything in a new language (that gets rid of smart pointers making things even worse).

      2. 5

        I’m keeping both eyes on Zig and also on Odin, a language with very similar goals to Zig, but a slightly different approach. (Ginger Bill, the creator of Odin, once said that “Zig tries to maximize explicitness while Odin tries to minimize implicitness”, which is a pretty good way to describe their difference in design.) I find both languages extremely compelling for exactly the reason cited in the article: they are much smaller (and simpler) languages than Rust.

        Rust is currently my main programming language and although I really like what it offers, I feel that it’s slowly slipping away from me. The language is accumulating new features with some regularity, but I don’t make it a very high priority to keep up with every one of those changes. As a result, I don’t know or know poorly a growing fraction of the language. The community on the other hand seems extremely eager to adopt and use new features as soon as they’re available, compounding my drift. In the past, I was a very eager Smalltalk/Lisp/Haskell user and relished in learning complex and expressive features. Now that I’m a full-time step-dad, for the past couple of years my style has become noticeably more basic. I call it FASE: functions, arrays, structs, enums. I don’t want to spend a lot of time wrangling complex expressions anymore, I want straight-forward imperative code.

        The creators of Zig and Odin value having a small and simple language and I think either one could become my escape hatch from Rust. Yes, I will lose on some of the good parts of Rust, but maybe that’s the price one has to pay to have a simple tool.

        1. 4

          I love Rust (at least the parts of it that I know and use) and I’ve had a blast over the years building small CLI tools with it.

          However, I haven’t yet found any joy in working on larger projects with it. There’s a deep satisfaction in knowing you’re safe from memory issues, but there’s also a deep anxiety that one day I’ll come across a feature or qwerk that I just can’t wrap my head around and it blocks my progress indefinitely.

          I have a feeling that I’d immediately find the joy with a memory safe Zig.

          1. 3

            but there’s also a deep anxiety that one day I’ll come across a feature or qwerk that I just can’t wrap my head around and it blocks my progress indefinitely.

            I share the sentiment of feeling blocked indefinitely sometimes. Rust’s type checking (and not only Rust’s) lacks incrementalism in the sense that with when you get a runtime error, you have all sorts of tools to help you narrow down the issue: print(), debuggers, deleting parts of the program and seeing if the issue persists, etc. With type checking OTOH, you just get the nice, terse No ;) with some not always helpful error message. One way to address this issue would be of course to make the compiler explain its reasoning step-by-step so that you can see where your reasoning diverges from the compiler’s but that’s a tall order. If it comes to lifetimes, I think there should be a way to dump the code with all the lifetimes inferred by the compiler so that we can compare them with our expectations (that would have been really helpful for me at the learning stage, back when I hadn’t memorized all the lifetime elision rules). Another way to make debugging type checking errors easier would be to get more incrementalism by way of making it possible to break down any code into a series of easy-to-comprehend partial expressions of the form let var_introduced_purely_for_comprehension_purposes: ComprehensibleType = short_expression that later combine into the final, desired expression that’s trivially of the right type. The reason we can’t always do that today is that some types are so big that are better left untold, like a combination of iterators (impl Trait types do not always work or they don’t constraint the type enough).

            Another major complicating factor is the return type polymorphism (along with the turbofish). Ideally, method dispatch would only work one way but it’s obviously too late for that (that’s something Zig has as a design goal BTW). The next best thing I think is to add support for debugging it in the IDE: something like a “get the type of the selected expression” functionality in rust_analyzer. Type ascriptions could help too as they’re more flexible than turbofish.

            1. 2

              I understand your worry, but I don’t think it’s a big deal. There’s a helpful Rust forum that can get you unstuck if you’re missing some obscure case syntax or a trait trick. If you run into something that the compiler just can’t understand, you always have the unsafe hammer to push though.

              Also note that people usually get stuck in Rust when they are pushing the limits of zero-cost abstractions and static safety guarantees. You can back off a bit and write Rust in a “dumber” way the way you’d write C.

              1. 3

                A particular scare I had was a codebase that relied quite heavily on a third party library implemented as a macro on all their fundamental types. I had to add something that I thought would be trivial and would interact in a very minor way with what that library was doing and quickly hit that “nuh uh” fighting the compiler stage.

                I remember seeing a full screens worth of cryptic type information and trying to dig into the underlying library to get an understanding of what was happening. It was extremely clever stuff, but I think that’s the first and only time in my career that a multiple day effort resulted in no progress at all.

                I reached the conclusion that I had to choose between:

                1. Ripping out that library, which was potentially a multiple week effort
                2. Digging in further and hoping that I’d just missed an easy solution
                3. Abandoning that feature, and having a little cry

                Unsafe would’ve meant pretty much the entirety of the codebase was unsafe, I don’t think that would’ve even helped if memory serves (it was a long time ago). I think the major disappointment was realising that after building so much work on top of a third party library we were completely oblivious as to how that was going to restrict our future work, is it realistic to expect a full audit of macro-based code before adopting it? It probably should be if you’re trusting months or even years of effort building on top of it.

                I ended up being instructed to go with improvised option 4 which was a half-baked and ugly work around version of the feature, and had a little cry.

            2. 3

              Thanks for sharing this! I’ve been interested in learning Zig for a while, but I haven’t found an excuse to do it, since most of my work is web development.

              I found Andrew Kelley’s interview on the CoRecursive podcast interesting because it’s fun to hear about what motivates someone to take on a project as ambitious as replacing all of the world’s C software:

              https://corecursive.com/067-zig-with-andrew-kelley/

              1. 2

                As someone who isn’t very familiar with Zig, can you better define “smaller” vis a vis Rust and Zig? Feature set? Stdlib? Syntax?

                1. 27

                  Rust is three-five languages in a trench coat:

                  • imperative plain rust code: for x in [1, 2, 3] (with a subset for const)
                  • substitution-based macros: macro_rules! { /*this looks nothing like Rust*/} + proc-macros
                  • logical traits (impl X for Y where Z:T)

                  Zig is very uniformly just one imperative language.

                  This I think is the major reason why Zig feels smaller.

                  1. 8

                    Very true! I recently rewrote a packet struct generator I made a few years ago in Rust to Zig. The original implementation used heavy Rust macros to manipulate types, while the Zig implementation doesn’t require anything special because I can manipulate types as values. The comptime reflection is crazy cool, and felt more intuitive to me than building this pseudo-DSL for packet definitions.

                  2. 1

                    Along the lines of matklad’s comment, comparing both languages to C++ is also useful. C++ has

                    • the imperative language, with the addition of classes
                      • the compile-time constexpr subset
                    • the compile-time template language (which is famously turing complete, and people do weird things like write regex engines and parser generators at compile time, in a tortured syntax, with limited tools)

                    And if you look at what happened in C++ 14, 17, 20, and I think 23 – a lot of the “weight” is in the compile-time languages (constexpr and templates).

                    I think the language probably doubled in size during that period.

                    https://github.com/AnthonyCalandra/modern-cpp-features

                    Features like coroutines rely on a lot on templates from what I remember, and lambdas interact with them, etc.


                    Now Rust has all of those things (constexpr / const contexts and templates / parameterized types), and it also has 2 different types of macros! And it’s getting bigger along the same lines.

                    I think the comptime feature of Zig can go a long way towards simplifying this situation. Admittedly I haven’t used it, but I have done lots of metaprogramming C++ with Python, so I know all the things I would use it for. (Anything that happens before main() and doesn’t depend on runtime values.)

                    For systems languages, the compile-time / runtime split is a bit like a “shadow language”

                    https://gbracha.blogspot.com/2014/09/a-domain-of-shadows.html

                    It could be justified, but it definitely makes the whole language bigger and introduces problems of composition.

                  3. 2

                    I wonder if the problem with memory safety is less a problem of the “language”, narrowly defined, but much more of the totality of the programming environment. Like, it may not be possible to have a “small” language that provides memory safety if you need to support Linux or Windows or &c. Right?

                    1. 4

                      This is definitely a part of it, but it’s not the whole. An unsafe environment ups the importance of those issues—it means that a failure can result in unbounded problems. In a safe environment like WebAssembly, the cost is more constrained: at least so far, it means you’ll just crash rather than ending up scribbling all over someone else’s memory and therefore causing vulnerabilities. Being in a context where memory-unsafety doesn’t directly result in CVEs helps, in other words—but “it’ll just crash” still isn’t great!

                      1. 1

                        I just think that the idea that we can address memory safety at the C level and be fine is really misguided? Maybe it’s time to reconsider many of the architectural spandrels that we maintain and even lionize. I don’t know. I wish the Zig and Rust folks the best, because even impossible goals can yield impressive results.

                        1. 5

                          I think I would say the goal is different (at least for Rust): it’s not “ah, then we’re fine” but rather we desperately need defense in depth. Not least because even OS- or runtime-level safety can’t save you from side-channel attacks which rely on speculative execution at the hardware level! Yeah, we can introduce mitigations on the OS side for those, but fundamentally they illustrate that what we actually need is for our tools to help us produce software safely at every level of the stack.

                          1. 1

                            It makes perfect sense; I was thinking more about the language size issue.

                    2. 2

                      One of the funny things about starting with C and later Go is that every other language seems so vast in comparison. That gives Zig some appeal to me.