Zig is one of the most interesting languages I've seen in a very long time, and possibly the first radical breakthrough in low-level programming design in decades. Maybe it will become a big success and maybe it will tank, but after having two visions for low-level programming -- that of C's "syntax-sugared Assembly", or that of C++'s "zero-cost abstractions" whose low-level, low-abstraction code appears high level on the page, once you get all the pieces to fit (Rust, while introducing the ingenious borrow checking, still firmly follows C++'s design philosophy and can be seen as a C++ that checks your work) -- Zig shows a third way by rethinking, from the ground up, how low-level programming could and should be done.
It manages to address all of C++'s biggest shortcomings, which, in my view, are 1. language complexity, 2. compilation time, 3. safety -- in this order -- and it does so in a language that is arguably simpler than C, can be fully learned in a day or two (although the full implications of the design might take longer to sink in), and also inspires a new approach to partial evaluation, replacing generics and value templates, concepts/traits/typeclasses, constexprs/procedural macros, macros (or, at least, the "good parts" of macros) and conditional compilation with a single, simple feature.
At this point in time, i agree with this analysis.
But what for a C programmer (systems/embedded) remains to be seen, is, if any of the new languages (say for example Rust, Zig, Odin) can (or want to) offer one of C's strengths most language designers do not think of (at least it looks to me like that). And this would be for me "leave the language alone (for the most part)".
If i may quote from Zig's website (https://ziglang.org/#Small-simple-language):
"Focus on debugging your application rather than debugging your programming language knowledge."
I have yet so see a community driven language that fulfills this promise in a comparable way to C's.
There's always "this little feature" necessary, or "this cruft to clean up". You name it i've seen it.
If you want people like me (and trust me HN community, there's a GIANT silent majority out there) to use Zig e.a. for production in the embedded and systems space where product lifecycles are counted in decades, you will have to provide this feature. Sure, you can call me a dinosaur (still under 40 :-)), or a BLUB programmer or what not. But language designers wanting to (slowly) put C to rest, should, imho, not forget about this "feature".
In this space you really want to focus on the real problems, not on problems of keeping up with the latest and greatest.
Side note for all the "if all you know is a hammer" responses:
Please don't compare programming languages with tools. This is a bad analogy. Programming languages are more like materials. Once chosen and the product (the source code) built, you cannot simply change it without starting from scratch and a new product (new source code).
My 0.02... and have an upvote for stating your opinion.
C is not going any where.. we all know that.
But it is not the best tool for accomplishing many systems programming tasks. IMO, this has probably been true since Ada95. Ada2012/SPARK, Rust, and probably Zig make this even more clear.
For some domains, C was the ONLY tool available... e.g. C is/was the only language with board-level support and an available compiler.
At my place of work, we presently use C for embedded code because it is best supported for the microprocessor we are using... but we'd rather use Rust, Ada, or Zig. we may explore using Nim to generate C with static-only allocation.
Yes indeed, Ada/SPARK would be better suited, but suffer (unfortunately) from a hefty price tag, not every shop is willing to pay. Especially if there are no lifes at stake should your software crash.
Rust has the (imo) above stated problem. Also, if you avoid dynamic allocation, Rust's advantages are not as compelling in the real world as advertised (personal opinion). They are there, but IMO minor.
The problem right now is that we have a chicken and egg problem.
You said you work in embedded too, so you know MCU bugs exist. Use anything but the "recommended" tool chain, and the compilers will be blamed, no matter how much proof you have that it is the MCU.
Don't get me wrong, i'm definitely keeping an eye on Zig. There is lots to be liked. I cited that its goal is keeping the language simple. But only time will tell if the developers can keep up to this promise. I, personally, don't trust languages developed by communities in this regard. Reality has (sadly) proven me right.
When you say Ada has a hefty price tag, what do you mean, exactly?
(Are you talking about ide licences, the commercially supported version of the compiler, ramp-up time to productiveness, or maybe something else?)
Somehow I've become interested in the economics of Ada, and am considering it in comparison to several other strongly typed systems programming languages for a learning project. So I'm a bit curious.
There are still 5 surviving Ada vendors, only Ada Core supports a FOSS compiler, for most commercial deployments the compilers lie in typical enterprise seat prices with "talk to our sales team".
This new announcement for Zig makes it seems like in the future it might be independent of LLVM... Maybe it could support C compilation back-end one day. But for now it's pretty much LLVM based languages, or C.
Alas... not to mention debugging tools, etc. That's why I've gone down the Nim path at work as well, and it's actually pretty fun once you get the basics. Having JSON support in a few kB's of flash is nice. I've ported enough of the Nim standard library to FreeRTOS/LwIP to be able to use it with the ESP32! If you want to take a look and see how it integrates with FreeRTOS [1].
I'd recommend trying out Nim's new ARC garbage collector if you have more than a few dozen kB's of RAM. The ARC GC is pretty friendly to working with C based-boards as it can use the native malloc and any native debugger mechanisms. The references are non-locking to it's pretty low overhead, but if you do try it run `-d:nimArcDebug -d:traceArc` first to see the overhead. You can't use `async` without cycle collection (ORC) however.
Of course no-GC works too and you can annotate functions to guarantee no GC (I think). Just less fun if you have the RAM available.
The low cost of porting a C toolchain to a new platform is a feature of C. Arguably, it's one of C's two remaining "killer apps" that keep it in use. The other is that C is the "universal recipient" language for FFI - it's easy to call C functions from almost any other language (but not vice-versa).
It's currently much more expensive to port an Ada, Rust, or even C++ toolchain to a new platform, so I'd expect C to continue to be dominant on lower-spec chips. Here's a previous comment adding a bit more technical detail to that argument: https://news.ycombinator.com/item?id=22822931
I am an old c-head (actually pascal was my first compiled language) and I remember in the early aughts writing vtables manually in C because I didn't trust what I couldn't see in c++ (I'm also pushing 40)
I have to say, zig really captures this feel. The only places where it gets a tiny bit strange in the way you complain about is comptime, but imo it is a very good trade-off for ditching the preprocessor and ditching make. And anyways comptime is far more easy to reason about than some of the mangled ways in which people who use C (in prod) use preprocessor: It took me weeks to understand the ffi header file for the erlang VM, for example. Three languages for the price of 1.25. not bad.
Rust has editions to help with this. All code is backwards compatible (except soundness bugs) and if need be editions are added to allow adding breaking changes. The compiler still supports 2015 edition and will forever. You can even use different editions in the same project by importing crates that use a different edition, still overall creating a single binary.
As an example of this one of the features they added was NLL (non-lexical lifetimes) which was designed to help iron out some of the annoying places the compiler couldn't see what you were doing was safe. One of the constraints they gave in implementing it was no changes to existing code that compiled, the way they did it as a non-breaking change was to ensure that the new system only allowed programs to compile that didn't compile before.
It sounds like you aren't interested in Rust for other reasons but wanted to point out that language hears your concerns about breaking changes being the worst.
You are still forced to upgrade your edition if you want to use certain libraries. I wanted to the newest versions of some async libraries and the update forced me to update my edition, otherwise I wouldn't have been able to use the libraries as they required me to use the async language feature.
Also, editions are no response to the issue of new features being added to the language, changing what is considered idiomatic, okay, etc. If Rust turns out to evolve constantly like C++ has in the past, then it'll be hard to keep up with it, in addition to the great one time cost of learning it in the first place.
Note that it wasn't a simple cargo update, but an update between two semver incompatible releases of the futures crate, from 0.1 which supported edition 2015 and provided combinators, to 0.3 which was built on the async/await feature, and required its use, because the combinators were removed. I guess it would have been hard to provide them compatibly to async/await but I'm not sure about it. Anyways, they weren't available, and as async/await is not available on the 2015 edition, I had to update the edition of the crate. The code got a lot simpler thanks to async/await so I'm very thankful for its existence.
Ah yes, that makes perfect sense. I was thinking purely from the "you can still use the keywords because of the raw syntax" perspective, but in this case, that wouldn't quite work. Thank you.
Yeah rather than supporting backporting things like that they instead decided that every feature should have a lint that shows you how to update your edition.
Basically the idea was rather than making it as easy as possible to stay on the oldest version they focused that effort on making it as easy as possible to update once you decided that was what you wanted to do.
That's interesting but I don't know if it helps with the parents concern. If I arrive into a project, the same reasons that made them choose rust mean that they have likely chosen the latest version of rust, with all its complexity and power.
If I am inspecting code for safety that is doing low level things then I want to have a clear and simple (imperative) model in my head. I don't want to spend the review admiring how clever the author is. I want to know as directly as possible what is going on in the machine.
In another domain the ability for the author to do clever things could make me very productive but it doesn't matter because the safety of low level code is not a concern.
"Newfangled features" and "don't be too smart for your own good" don't overlap as much as you imply. They aren't entirely orthogonal but very much so.
C++'s `unique_ptr` is a "newfangled feature" but is not the latter (ignoring custom delete functionality).
I tried to account for the discrediting of zero cost abstractions (which I buy) but I think you are adding the latter onto that which I didn't get from the person I was responding to.
> I want to know as directly as possible what is going on in the machine
Certainly not in multi-threaded code on an x64 machine (and IIRC ARM is trending in that way as well). There are too many abstractions to actually do that, the best you can do is understand the abstract machine of the language and the rough edges translating that into real machine code entails.
> safety of low level code is not a concern
I will remind you that in the case of Rust safety is where all of this comes from not productivity.
Writing a mutex by hand is hard and you will make mistakes, here is a library construct to do it for you. It will be more complex than what you would write but it will be safer because others have worked with it. etc. etc.
When working with minimal computation resources using "works for everyone" constructs stops being zero cost and starts being expensive both conceptually (what were the poisoning rules again?) and performance wise when the cost of providing your abstraction is hard (why can't I arena allocate an RC?)
Those are the situations where Rust and even C++ have difficulty shining and C wins out. It provides so little you never have to pay for anything but what you absolutely need. But remember the things you lose out on are from every category you can think of, not just one. Performance, reliability and safety are all often sacrificed in some amount when writing custom code.
When you write code instead of a random Joe in a garage you know who to blame when it blows up. When you write code instead of using the larger community code you give up the often great characteristics in every metric for your more fine tuned characteristics in some metric generally.
Sorry that got wordy... TL;DR nothing about most low level languages up to things like Java and C# IMO stops you from having a simple imperative model in your head so using that to chose C over something else is missing the point.
I think people like you are the last one to adopt. Only when a new language is battle tested and has been around for at least two decades, with an language update rate equal or smaller than Go, only then you might want to consider it for your next embedded system. New languages invocations are by definition of new not for you, fair enough.
On the other hand if you change the language, say every two years, when is the right point to dive in? Is it at some point (say i missed the last four years of language development) hopeless to catch up?
> "Focus on debugging your application rather than debugging your programming language knowledge."
>
> I have yet so see a community driven language that fulfills this promise in a comparable way to C's.
I mean, this is only true if you ignore the myriad ways in which C allows you to manifest memory unsafety and undefined behaviour
Yes, but in Debug/ReleaseSafe it's much stronger than C with sanitisers. Zig makes those problems so much easier to catch in development than C. Zig doesn't have pointer arithmetic and pervasive casts, and all traversals require knowing the data's size either at compile-time (Zig arrays) or runtime (Zig slices). Unless you explicitly do unsafe casts, Zig won't allow you to index anything outside a buffers, and by that I don't just mean outside of allocated buffers, but even "sub-buffers" derived from them.
"Yes, but in Debug/ReleaseSafe it's much stronger than C with sanitisers."
Is it also stronger than C with Frama-C?
Are you sure you hit all the execution paths during development?
Pervasive casts?
Not under MISRA C.
Look, I'm not criticising Zig on a technical level (I mentioned above I'm keeping an eye on it because it looks interesting), but I don't have any trust in community driven languages keeping it simple (although Zig promises to do so, see link above).
But, 1. as much as I like formal verification and am actively involved in the space, we're talking less than 1% of C code that uses those tools (unfortunately), and 2. because Zig is so simple -- simpler even than C from a formal analysis perspective because of slices and casts -- if it ever becomes successful, I have no doubt we'll see such tools emerge.
Of course, the other 99% can't use Zig either because it's not even "out" yet. Obviously, a product that doesn't even exist yet can't replace any product that does, and no one is suggesting that Zig -- in it's current virtually nonexistent form -- is a replacement for anything, let alone for C. We're talking about what Zig could do when it exists in some production-ready form.
I really like the Zig approach of improving the tooling for manual memory management rather than replacing manual memory management.
I think Rust is a great, worthy language, and for a lot of use-cases it makes sense to optimize for trying to just put everything on the stack, but there are a lot of other cases where what you really want to do is own allocation by yourself instead of trusting it to the compiler. I haven't tried Zig, but I really want to see if it can deliver on this.
> I really like the Zig approach of improving the tooling for manual memory management rather than replacing manual memory management.
The new test allocator checks your code for memory leaks, use after free, and double free in a highly ergonomic fashion. I think is fantastic that zig encourages writing tests, and I think if you get full test coverage in your zig code with the test allocator, you will probably solve 99% of your memory errors.
I know this is possible in C++ (see alexandrescu's allocators talk from CppCon 2017?) But I would be surprised if anyone does it.
I guess my feeling is that the tool chain for C/C++ is now C, C++, plus the preprocessor stuff (its own language), linker, make, (plus probably automake, cmake), gdb, valgrind, asan, etc etc etc, just to get "best practices/safe coding" right.
With zig, not 100% comparable (like not sure about gdb, but zig stacktraces and error traces are fantastic) it's all done in one language. Such a lowering of mental overhead and entry barrier!
The only thing you have to learn is that at comptime you get access to some magic (like the ++ operator)
Just to be clear, Zig isn't the same as C/C++ with sanitizers. Zig has slices and it doesn't have pointer arithmetic and pervasive casts. For example, if you preallocate a buffer and then reuse it, or chunk it into multiple pieces, sanitizers won't find an issue, but Zig will (thanks to slices). C simply has no way to express, "I want to pass a pointer into an array to this subroutine but it is only allowed to use n elements," when n is dynamic.
Right, but in this case it's not a matter of what you have, but what you don't. Zig has slices as the only form of moving pointers around, and it doesn't have any pointer arithmetic. I.e. every time some data is traversed, the language ensures the size is known either at compile time or runtime.
This isn't exactly pointer arithmetic as in C (or in unsafe Zig, as with @intToPtr, or using the "unknown size" pointer type, [*]). This is talking about preserving size information, and only when the arithmetic is compile-time-known; so it's more "array arithmetic".
Sure but a language where "the only hard part" isn't a factor surely has a leg up on one where it is.
A lot of the advantage which Zig has over C is exactly in not having to support a towering edifice of tools and hacks which is older than I am. This is true to a significant degree of Rust and C++ as well.
Rust has manual allocation with Box<T>, it's just not as automatic as Zig. Rust's manual allocation pain is constant-factor overhead: typing out the type signatures takes longer every time you use it, but the complexity doesn't grow beyond that. I don't enjoy typing Box everywhere, but it's not that bad.
Rust allows virtually any type of memory management you want, my point is the syntax is not optimized for things like arena-based memory management or custom allocators. Rust assumes most of the time you'll be passing around references to values or small structures allocated on the stack.
In Zig, as far as I understand, you really just pass an allocator around. I don't see any special syntax to support this?
This could be done in Rust. There is, for example, the simple bump allocator bumpalo [1].
It would be nice if the the std collections supported this (in planning, but hasn't seen much progress), and most dependencies would not be built around a manually passed allocator.
> Rust assumes most of the time you'll be passing around references to values or small structures allocated on the stack.
Can you clarify what you mean here? All of the std collections (Vec, HashMap, etc) use allocation. There are also `Box`, `Rc` and `Arc`, which allocate and are used everywhere.
Again, it's not about what you are able to do in Rust vs. Zig, it's essentially about the API of the language. Zig's semantics around memory management are imperative: i.e. you are telling the compiler when to allocate and deallocate memory. Rust's are declarative: you tell the compiler how the memory for a given value should be managed, and the compiler interprets these requirements to decide when memory should be allocated or deallocated.
Again, I do not think this a weakness of Rust, and I do not think it should change. In very many cases, the Rust approach is very helpful.
The point is there are also cases where imperative, explicit memory management is desirable.
> The point is there are also cases where imperative, explicit memory management is desirable.
I agree that is the case, and Zigs convention of passing an allocator around and making those calls very explicit has merit.
I think you are over-estimating the amount of "magic" in Rust though.
The allocation-related logic is not part of the language or the compiler, but comes from the std (or alloc) crate. You just don't usually see a "malloc" call because it is hidden behind types in the standard library, which default to a global allocator. You still "manually" allocate by calling eg `Box::new()`.
The only magic is that Rust calls a destructor ( Drop::drop) when a value goes out of scope. Types can implement drop and use it to deallocate or decrement a reference counter. This is made possible by using an affine type system with lifetimes and move semantics.
This is conceptually not that different from doing something like `defer allocator.free(x)` in Zig, though less explicit.
ps: I realize that conventions make a difference. But nothing in Zig would stop me from having a global variable with an allocator, and using that without passing it around.
> But nothing in Zig would stop me from having a global variable with an allocator, and using that without passing it around.
That's correct and you probably should do that in your own applications, but it will be frowned upon in libraries, and people will not choose to use them if you make that choice.
There's also another pattern, where you stuff a pointer to your allocator in your struct and have your "object" carry around its allocation system, you don't have to pass around allocators that way.
> and possibly the first radical breakthrough in low-level programming design in decades
Can you expand what you mean here?
I really like Zig, and there is definitely a big design space to explore in creating a modern low-level language that doesn't come with the complexity of something like Ada/Spark or Rust.
This binary patching + daemonized compiler approach is particularly exciting.
But as a language, I'm not aware of any features that would amount to a "radical breakthrough". As far as I am aware, it is a modernized, low level language, with nice compile time evaluation, async, and some runtime-provided safety guarantees. But nothing that is so novel from a type system / language design point of view.
I'm not sure I like the "better C" label. After all, Zig allows you to easily do stuff that's virtually impossible in C, and only possible in C++ with templates and constexprs and concepts. It also doesn't have pointer arithmetic and pervasive wild casts, so it's not a "syntax-sugared Assembly". Its only similarity to C is that it is a low-level language and that, unlike other low-level languages that aren't C -- like C++, Rust, and Ada -- not only is it not among the most complex languages in the history of software (all three of those have probably secured their places in the top-five), but it is probably among the simplest.
> I'm not aware of any features that would amount to a "radical breakthrough". As far as I am aware, it is a modernized C, with nice compile time evaluation, async, and some runtime-provided safety guarantees. But nothing that is novel from a type system / language design point of view.
AFAIK, general partial evaluation with introspection as a single mechanism to do the work of generics, typeclasses/traits/concepts, value templates, macros, and conditional compilation -- combined with a general error reporting mechanism that is shared between runtime and compile-time -- has never been attempted before. It is revolutionary.
I agree that Zig is more than a "better C", and I edited my comment.
Regarding `comptime`: I haven't fully grokked it, but it looks very similar to the capabilities of D (and in part Nim). Do you by chance know those languages and can give a comparison?
(Languages like Idris also allow to express a lot at compile time, but that's quite a different domain)
Nim and D have a similar feature, but Zig's radical design is not in including this feature but in not including others it can replace (generics and macros). Both D and Nim have generics as a separate features, and Nim has macros and D has conditional compilation as a separate features. Zig is not special in having comptime; it's special in having only comptime as the single partial-evaluation mechanism.
Are you sure that minimalism is a design principle of Zig's, and not just the current state of the project? @andrewrk's comments at https://github.com/ziglang/zig/issues/130 lead me to believe that it's still undecided.
Yes, I am sure it's a design principle. Which is not to say that some aspects will change. E.g. I'm in favour of some of the (more minimal) suggestions here: https://github.com/ziglang/zig/issues/1669
Someone pointed it out to me a while ago. I don't think the approach is comparable to Zig, but is in a somewhat similar spirit. Like with macros, Terra contains two languages (one of them being the meta language), while Zig has general partial evaluation in just one language (also https://news.ycombinator.com/item?id=24293611). For example, in Terra, conditional compilation is a branch in the metalanguage; in Zig, it's just a branch in Zig that is partially evaluated.
Fully agree. There were many other attempts at building a better C, but Zig's big achievement is that it offers enough new and compelling features that sufficiently distinguishes it from C, while still managing to keep the overall complexity very low while still being familiar. That mix of "fresh, yet familiar and simple" sets it apart.
Tend to agree, but I hope that the zig folks aren't biting off too much by trying to replace LLVM. Sure, I can understand some of their reasons, but LLVM provides all kinds of functionality (optimization, code lowering, targeting lots of cpus,etc.) that will be difficult and time consuming to replace. I hope this doesn't cause them to lose focus on the development of the language itself and libraries.
> also inspires a new approach to partial evaluation, replacing generics and value templates, concepts/traits/typeclasses, constexprs/procedural macros, macros (or, at least, the "good parts" of macros) and conditional compilation with a single, simple feature.
So, zig is new to me but looking at an overview of the feature in question here this feels pretty hyperbolic? I certainly agree that the approach it takes is syntactically less noisy than angle-bracket generics/templates (which I dislike quite a lot) but it doesn't seem particularly revolutionary vs. the way evaluation of types in haskell works, for eg. A function that produces a type based on type arguments is roughly a typeclass.
And the intermediate products of a function with comptime arguments are still pretty much semantically equivalent to a C++ template anyways, just with the arguments in a different place.
It is cool, and I like the angle it takes, but it seems more synthesis than revolution to me. Am I missing something?
I need it to be a first-class feature because I’m a baby. My #1 complaint with C++ is that they make everything a lower-class feature to avoid breaking backwards compatibility and it makes the language more verbose, less enjoyable, and less intuitive.
All that said, I’m personally okay with the decision to omit typeclasses since the language is meant to compete with C, not C++.
I think Zig competes with C++, too. But to match the expressivity of C++ you don't necessarily need to have the exact same features. C++ gives you control over generics with concepts, and Zig does it differently -- with introspection, which is very much a first-class feature. If Zig wanted to achieve what other languages do in the same way they do it, it wouldn't be radical. Instead, it offers a different way. When comparing two languages, especially those designed with drastically different aesthetics, you don't ask how easily can an expression in one language be translated to the other, but how easy it is to achieve similar general goals.
I think Zig competes with a school of C++ that tries to stick to a simpler, limited subset of C++. Games is one area where C++ is much more common than C, but also where a lot of C++ practices (exceptions, excessive template use, many parts of the stdlib) are discouraged and things tend to be kept "simpler".
I'm sure there are a good handful of other fields that currently write "minimal C-like C++" and I think Zig competes with C++ in those cases.
But Zig has counterparts to exceptions, templates and anything you may want to do with them; it compares quite nicely with even "rich" C++. The one thing it intentionally doesn't have is operator overloading. In general, I'd say that the difference between Zig and C++ is that when you do some clever stuff in Zig, you need to be explicit about it so that the reader knows this call-site uses dynamic dispatch or that there is a destructor being called or that this operation isn't really the standard +.
> Zig shows a third way by rethinking, from the ground up, how low-level programming could and should be done.
I don't know about that. "that of C's "syntax-sugared Assembly" is pretty much how most people want to do system programming. Don't systems programmers want to be as close to assembly without delving into the pain that is assembly? Also, I don't know how "ground up" it is. Seems like zig is just more syntactic sugar on top of C to me.
Wasn't there a big push for D to supplant C/C++ a while back. Now Rust has joined the fray. Why would Zig succeed when D failed and Rust is failing? What is it that Zig offers that D or Rust doesn't? And does that justify the migration of legions of C programmers and mountains of C code to Zig?
It manages to address all of C++'s biggest shortcomings, which, in my view, are 1. language complexity, 2. compilation time, 3. safety -- in this order -- and it does so in a language that is arguably simpler than C, can be fully learned in a day or two (although the full implications of the design might take longer to sink in), and also inspires a new approach to partial evaluation, replacing generics and value templates, concepts/traits/typeclasses, constexprs/procedural macros, macros (or, at least, the "good parts" of macros) and conditional compilation with a single, simple feature.