Why Zig when there is already C++, D, and Rust?
ziglang.org
ziglang.org
- The language is really simple and "enjoyable". Writing C++ often feels like puzzle solving to me even after 25 years. From the little Rust I've written so far, I got the same impression. Writing Zig is like writing C or Go but even more straight-forward. The language completely "disappears" while coding.
- The simple integration with C, Objective-C and C++ doesn't get enough credit (disclaimer: I haven't tried C++ yet). Even if Zig aims to replace C, I think the reality will be that most cross-platform / "cross-language" libraries will continue to be written in C, and most "serious" Zig projects will actually be mixed-language projects. Most other languages are too much trapped in their own ecosystem and aim to "rewrite the world" (which won't happen of course).
- Integrating the build system into the compiler and standard library is the right way to do it (not a feature unique to Zig, but if the C/C++ world is looking for the right way to solve the "build system problem", this is it).
...lots of other cool stuff in Zig to write about such as the incredibly simple module system, the comptime-, reflection- and generics-features, but IMHO the only feature that really counts in the end is: ergonomics.
This is the one thing that matters and where Zig beats the more "powerful" alternatives.
So if you have a C program, and you want to dip your toes in Zig-land by writing the next library in Zig, you just can. If you chose Rust instead, you're going to have a bad time.
Obviously, you can’t expect people that didn’t managed to learn these languages to know that.
But if that’s your killer Zig feature, lots of languages have it.
Naw.
/* main.c */
#include <stdio.h>
#include <stdint.h>
uint8_t answer();
int main(int argc, char** argv) {
printf("%d\n", answer());
return 0;
}
// example.rs
#[no_mangle] extern "C" fn answer() -> u8 {
println!("The answer is:");
42
}
# test.sh
rustc --crate-type staticlib -o example.a example.rs
gcc main.c example.a -lpthread -ldl
./a.out
$ ./test.sh
The answer is:
42
Workflows using `cargo` instead of `rustc` are also sane and easy.However that compatibility is also what makes Objective-C and C++ impossible to ever be fully safe, without breaking compatibility with the C subset.
Static analysis help, but only when all code is available.
Nim, if you let it use C as a backend, will let you use C macros, despite being generally safe and not remotely a C superset.
Yes, the use of C macros (and any kind of ffi) is unsafe, that cannot be avoided. But it doesn’t need to be a c superset (and indeed, when you use JS as a backend, or the native LLVM backend, you get no access to C macros)
#[no_mangle] extern "C"
All due respect to Rust, which is a fantastic language, what this line says is "here is a C function written in Rust".You can of course do the same thing with C++ and with D, allowing all three to check the box for interoperability. And with Rust you get everything Rust offers: so you can write in the `extern "C"` dialect while getting memory-safety guarantees within the library, that's not nothing.
What Zig offers is the ability to write ordinary Zig and call that from C. That's part of what it's designed to do.
This is interoperability by design, rather than by configuration. It's simply a different thing, and considered along this singular dimension, I would assert that it's better.
I didn't, and wouldn't, say that Zig has the potential to replace Rust. I said that about C, and explained why.
You say that, but Zig appears to suffer from the exact same problem you point out in my Rust code:
export
https://ziglang.org/documentation/master/#Exporting-a-C-Libr...Perhaps I'm mistaken. How do I directly call Zig's `std.debug.print` from C? Or Timestamp.unixEpoch as defined here? https://ziglang.org/documentation/0.7.1/#Doc-comments
There's a bunch of rust crates containing macros, binaries, and build scripts that cut down on Rust's boilerplate as well. While there are some rough edges and room for further improvement, I am far happier with Rust's C interop than I am with the C interop of most other languages. Zig might have a touch more polish, but the bit I originally quoted made Rust sound like it was a good order of magnitude worse than Zig at C interop, which I just can't agree with.
One of the very first things I did when learning Rust was write a test library and drop it into an existing C++ codebase. I've also experimented heavily with cross compiling it to pretty much anything I can get my hands on. It was a good time, not a bad one.
It’s a great language though and a ton of fun. It’s so easy to dive into a codebase quickly which I value as a very distracted dad. The standard lib is a joy to read too.
¹ https://github.com/ziglang/zig/search?q=design+flaw&type=iss...
while the debug allocator can tell you at runtime about use-after-free or memory leaks, that's a less strong guarantee than rust or a GCed language
there are no closures; in general the code you write is lower-level than code in python, c#, rust
the language is very new
--opinions of a zig dilettante
bonus observations:
the std library code is very readable
the language overall has an excellent philosophy & design approach, and i predict it will become extremely popular
(as a single point of comparison: even after puzzling over it for a while, i still don't understand how modules work in rust; the equivalent feature in zig is immediately obvious, and in fact is the same concept as structs. how cool is that? (very cool, imo.))
I think that's very important feature to have for some people like me. I love Java standard library, because it's really easy to read. I regularly read its source code when I need to understand something and it's amazing how much time that could save. The only nitpick when it comes to Java is that some low-level stuff implemented in C and it's not as straightforward to jump into C implementation.
And that what turns me off from Scala and from C++. Scala collections library is horrible to read. It's immensely complicated. I get that they're solving hard problem, but I don't like it. And I hate C++ STL code (at least those that I saw), they're using obscure __identifiers__ and so on (may be there are better implementations, I saw GCC and MSVC ones).
Standard library is how language is supposed to be used. Those who learn language can just read its sources to better understand language idioms and replicate those in their code. And there's a huge difference between languages with readable standard library and languages with unreadable standard library (or even without sources at all, Apple sucks).
If only we could skip forward a decade and get mature implementations of it right now...
this is why the world is such a sad place
Also, currently it's possible to return a pointer/slice to a stack-allocated array from a function without a warning or error resulting in a dangling pointer. But this too will hopefully be fixed (because even C compilers warn about this these days).
But I think all those points can be considered bugs / todos.
They have runtime checks (unlike Rust compile checks) which are at least slower, but likely still don't guarantee memory safety (did not dig deep).
You can use defer keyword to free right after allocating and it won't be called until the function/program completes. You can use debug or testing allocators to ensure no memory is left unfree'd or double free'd. The std lib has a large selection of custom allocators that you can use depending on your needs. One great one is an "Arena Allocator" which is just a linked-list of allocations that can be free'd at one time, versus tracking all these individual allocations. And if none of these allocators fit your needs, the interface to define one is pretty easy to implement. https://ziglang.org/documentation/0.7.1/#Choosing-an-Allocat...
"I'm not mutating or holding onto anything I'm not meant to"
Not really sure Zig can help you with this one. With great power comes great responsibility, or something like that.
"that I've accounted for every exception that I can result from a given call"
Zig does not have exceptions, but it does have error types which functions can return. From the function definition you usually can determine all the errors that can possibly be returned, unless the function definer use'd the catch all ! error type. Regardless, it is still much better than regular exceptions because
- you know by the function def that an error could be thrown, the compiler forces the caller to account for a possible error by "unwrapping" the return value
- an error happening just passes back a different type to the caller and in combination with the above, you can never "miss" an error https://ziglang.org/documentation/0.7.1/#Errors
Also Zig switch statements without an else ensure you handle every possible value so if you forget to handle one possible error (based on the type), it will throw a compile error. https://ziglang.org/documentation/0.7.1/#toc-Exhaustive-Swit...
There's also ZLS, a language server implementation for Zig that I believe can help with that (or if not today, it will in the future).
this can do that quite easily afaik.
Learning Rust kind of feels like brute-forcing my brain until it sticks. But I can't say it isn't fun and exciting!
I was planning on using Nim but might give Zig a try also.
It looks really cool. I feel like Nim and Zig both fill a similar niche, maybe Zig aiming more towards the lower level / even embedded.
That's just my impression, not having used either in anger.
I agree on the C++ puzzle solving thing, though in a way it's almost part of the appeal!
even if the language is great, if all the ecosystem is written by people that didn’t manage to learn C++ in 25 years, I can’t really trust any library there
Thanks for all of your work, it's great.
Meaning, having binary crates compiled with different epochs, and then passing data and closures across crate public functions, specially using as input data from modern ones into the older ones.
With the language evolution it won't be possible to keep semantic changes to work as expected, or if the runtime requirements have changed, that linking everything together will still work as expected.
To me epochs seem to only work when using a single compiler, compiling all crates from scratch, with is nice, but not what enterprise C, C++, Java, .NET code looks like, and any new language being adopted is expected to play the same game.
Hence why Apple took such a big effort to make Swift ABI being able to deal with language updates.
We mix editions in >1000 crate projects every day just fine, and we are sure it will work with all future editions.
How are the editions going to help when semantic changes do eventually come up in future Rust versions?
If we’re talking ABI-level stuff, that doesn’t require an edition because you build all your crates with the same compiler, so it can use a consistent ABI.
Swift’s ABI stability has nothing to do with semantic changes and everything to do with they wanted binary compatibility, so they could start using Swift in the system frameworks that your app links against.
Rust will never be an option in certain scenarios if the only option is to be like a scripting language and require its users to compile everything from source code.
Swift's ABI also takes into account ways to secure the ABI while evolving the language.
That’s a feature.
If you want languages that introduce semantic changes and break your code, you have many options to pick from, C++ being one such language in the same space as Rust.
Zig's reasons for being better include:
- No operator overloading
- Rust panics when allocator fails
- Rust uses a std allocator (which is configureable)
- No metaprogramming (IE, macros)
- Simpler to grok
Essentially, Zig being the "Go" of C/C++/Rust/D. But, I think that dances around the problem where Systems Programming is just complex. Just because some functions and control flow is simpler doesn't mean the program itself is.
If trying to pull Rust programmers away, I'm not sure the merits in this article are strong enough. However, for newcomers to systems programming, Zig might be easier to learn and solve problems with.
Ehh??? It's the first point there.
And I don't like all this hate for operator overloading. I prefer to write "vec1 * (vec2 + vec3)" instead of "vec1.mul(vec2.plus(vec3))".
I mean, most of us aren't going use linear algebra, but still...
I thought his post was pretty clear tbh. For example he mentions complex types so that should be an indicator that he's not talking about the kind of high school maths that the average developer can coast along with.
2) Slightly more real-world: Measuring things when you only have approximate rulers, have difficult things to measure (odd surfaces, etc.), or have to calculate from 2nd hand measurements (pictures with rulers, etc.)
Not sure if that qualifies as Real World, but...?
Weird to think that game programming is (weirdly) kind of a niche thing at this point. Compared to programming boring CRUD apps, that is. :)
Lovely.
I don't override operators often, but when I do it's useful because the operation I'm describing is a really close parallel to other uses and properties of that operation. As an example, it's why I dislike + for list append, + is normally commutative (which list appending definitely isn't) and subtraction is pretty bonkers as a reflected operator on lists.
0: In particular, exponentiation by natural numbers (or positive integers for things like nonempty lists that deliberately exclude the multiplicative identity) is almost always well defined[1], even if the thing you're exponentiating bears no resemblance to a natural number.
1: Although note than with vectors and dimensional quantities (eg meters), x^2/x^3/etc are all different types: area/volume, bivector, etc.
C#: foo[index] = bar[index];
Java: foo.put(index, bar.get(index));
Sometimes there are good reasons to mask complexity.
Why did the division symbol get chosen for a concatenation behavior, of all things? Well, I suppose because it looks kind of like how we write paths with slashes, e.g. “dir/subdir/etc”. While I understand how this might be fun aesthetically once you already know the types involved, I find this completely opposite-than-numeric behavior here to be quite ambiguous and unintuitive.
To someone just skimming the code though, it might not at all be obvious that “+=“ and “/=“ both exist as concatenation operators, and their behaviors are different.
But yes it’s an aesthetic preference or opinion; I tend to prefer operators only when their only possible behavior is nearly so obvious that no documentation should be necessary. When that’s not the case, I prefer named functions/methods because they permit being explicit about subtle differences.
OTOH / is an instant mnemonic cue for "append with separator", and + matches the + on std::string.
In fact, to generalize, I think we can fairly say that operator overloads are akin to extremely short function names; in some cases they may work out great when there is not much ambiguity implicit in the underlying problem, but in other cases a longer and more explicitly descriptive name is required to disambiguate. In this case, “append” and “concat” seem quite poor names for different functions, given that they are virtually synonymous and therefore do nothing to describe or distinguish their differences of behavior.
So my claim is that we can do significantly better at resolving ambiguity with carefully named functions (or other approaches) than operators, or tersely/ambiguously named functions (like “append” and “concat”). Of course, this does come at the cost of code verbosity. Just where we should draw the line between too ambiguous vs too verbose, is of course a difficult subjective matter.
I have been thinking recently that everything should (or rather could, in a special language) be overloadable. And every operator should be treated as a non-first class citizen of a language, '=', '+', and all. The inelegance is granting those operators any privileged status. So the parsing can be in every case dependent on the arguments being parsed: if you're adding numbers, number addition, if you're adding vectors; even more complex behavior could be added for special parsing cases (which I don't even know what could be), say for creating certain algebraic operators with special conditions, maybe something like knuth's up-arrow.
Of course, thought must be employed on the scoping of these parsing changes but since this behavior would be conditional on the argument properties, it is very difficult to see any problems. For example, while adding vectors overloads '+', it is not going to cause problems in other cases (when the arguments aren't vectors), and when dealing with vectors 'vec1+vec2' the programmer will essentially always be thinking about the overloaded operation anyway, it seems absurd a confusion would occur. I should be able to write 'x=5!+3' to mean 'x=factorial(5)+3'.
Allowing contextual meaning (evaluation) of operators, functions, even syntax, allows more compact, expressive language, because we can reuse words, associate their slightly different applications and adapt syntactical behavior to the problem at hand.
Take the usages of the word 'slow' in natural language: it can be a description of current velocity (variable) of an object (context-dependent) "the car is slow", it can be a description of a property of an object (low typical/maximum velocity) "slugs are slow", it can be a verb "please slow down", and so on. Creating new words for each use case is inefficient and disregards the natural close association of their (contextual and algorithmic) meaning.
Especially in math-heavy domains I don't think anyone is arguing that "vec1.mul(vec2.plus(vec3))" is cleaner code in the abstract (and I know I've personally written pre-processors _entirely_ to avoid having to write that kind of garbage when doing math-heavy code in an environment unfriendly to such syntax), but the function calls make it crystal clear that something non-trivial is happening under the hood.
Do I want to to give up operator overloading in Python? Absolutely not, and to be frank I wish that portion of the language were even more dynamic. Do I care about Zig not having operator overloading? Not in the slightest. It sits at a different point in the language design space, and I'm super excited about it.
Well, it makes it crystal clear that something non-trivial may be happening under the hood. If I have a vector type which is implemented using SIMD intrinsics, I'd still call its addition operation "trivial", even if it hasn't been blessed by the language as such.
While we're nit-picking, a wide vector type implemented with SIMD intrinsics would still have a non-trivial addition as far as Zig is concerned; your specific example really only holds for sufficiently primitive vectors.
Alright well I'd also rather write "vec1 [*] (vec2 [+] vec3)" too. Or equivalent.
Google has gotten much better at handling symbols.
Basically, custom infix operators are super convenient as long as they are auditable and aren't abused.
If you want your code to run fast, you need to ensure that you know what your program is doing and you're in control, at all times. Manual memory management enables that.
Actually unlike C++ and Rust you can have a lean D and most probably it can perform as good as Zig if not better. D has excellent CTFE support similar to Zig and D has no macro as well. If you want extra memory safety with ownership support D has that feature covered.
I doubt this is really meaningful. The performance-ceiling on all four languages is presumably very high, as they all allow you to write hand-optimised low-level code if you want to.
Since C++ is more relevant to what I do, naturally I keep validating how good they fair and so far not good.
Lifetimes checker for example, is only able to deal with basic workflows and when it complains the error messages aren't that good, leaving you wonder why the analyser considered it an incorrect lifetime.
Main reason being, if the call sites aren't annotated, it can only guess the lifetime use, and mostly it gets it wrong.
No doubt if they keep throwing money at it, they might eventually solve it, however this is already a three year effort and this is the best so far.
[1]https://dlang.org/blog/2017/08/23/d-as-a-better-c/
[2]https://dlang.org/blog/2017/08/01/a-dub-case-study-compiling...
I run Zig SHOWTIME [1] and we have quite a few talks that introduce basic concepts to people that aren't used to systems programming. Again, not yet enough to consider the problem solved, but it's a step in the right direction and we'll build a bigger library as more people join the community.
[1] https://zig.show
That said, you can get quite far without needing to understand pointers or memory management in any of the example languages you mentioned - "Clojure, Scala, F#, Rust". In the first 3, the garbage collector still does most of the work. In the last, there is are no calls to malloc() or free(). Rust's compile-time lifetime concept allows to compiler to do the work for you.
Many of us were doing systems programming with other languages before C went mainstream.
What you need to learn is computer architecture.
Getting back to JVM or .NET, you can get hold of JIT Watch, VS debug mode or play online in SharpLab.
Get to understand how some code gets translated into MSIL/JVM, and how those bytecodes end up being converted into machine code.
https://github.com/AdoptOpenJDK/jitwatch/wiki/Screenshots
Languages like F# and C# allow you to leave the high level comfort and also do most of the stuff you would be doing in C.
Or just pick D, which provides the same comfort and goes even further in low level capabilities.
Use them to write a toy compiler, userspace driver, talking to GPIO pins in a PI, manipulating B-Tree data stuctures directly from inodes, a TCP/IP userspace driver.
Not advocating not to learn Zig, do it still, the more languages one learns the better.
Only advocating what might be an easier transition path into learning about systems programming concepts.
With Python, Perl,R, Bash... whatever, I never had to know any C to be very productive.
With F#, good luck not knowing C#. With Clojure, not knowing Java is a pain. I've tried both several times and was frustrated by constantly being referred to something Java related that I didn't understand. Because of that, I feel like those languages will never pick up anyone outside of regular users of C# or Java that want to try something different. Of course there are exceptions, but they're just that.
I agree with you that perhaps one should go through all of what you're referring to Ex: learn assembly, write a Forth...etc.
I'm just saying a lot of these supposedly amazing languages stagnant with regards to their userbase for a reason. APL uses an entirely different way of doing things than Clojure, but I'd bet I could get up to speed a lot faster with APL than Clojure where I constantly have to fight with learning the daunting ecosystem.
Those two factors make me think it's close to the ideal language for newcomers to lower-level programming. Even the syntax ain't bad, as far as C-like syntax goes; it'd be interesting to see some alternative languages that put alternate syntaxes over Zig's semantics, though (and one of my New Year's Resolutions is to put together something to that effect, whether by transpiling to Zig or by (ab)using Zig's compile-time programming).
Zig is insanely easy to read coming from high level languages like these. It's only when you start to write Zig that you need to take more time to understand, for example, the difference between the lifetime of a pointer to stack allocated memory vs a pointer to heap allocated memory.
But by then, I think Zig will have already hooked you.
Zig is also a great way to learn systems programming, because the design of the language and the std lib code itself is such a great teacher.
I'm not sure that's a fair comparison, as Zig features compile-time evaluation ("comptime") that supports first-class types. Generics falls out of that by letting you create types imperatively at compile time. It's hard to imagine Go ever going in this direction.
As a long-time C programmer, I am really excited to try out Zig once it stabilizes a bit more. Zig's take on generics seems like a compelling alternative to C++ templates.
What makes you argue that Rust is the "Go" of systems programming?
Go in may ways has the same approach: stop developers from doing potentially damaging things (according to Go's definition of what's "potentially damaging"), which I guess is one of the reasons why the creators decided to remove pointer arithmetic and add a garbage collector.
Zig wants to keep the language small, but the programmer is expected to have access to all kinds of advanced tools, in a way that I don't see neither Go nor Rust consider acceptable.
In Zig you can use `undefined` to avoid initializing memory, do all kinds of pointer arithmetic, and while you can't overload operators, you can use comptime to concoct all kinds of static/dynamic dispatch schemes, the only limit is just that they can't masquerade as basic operations.
Of course you can do all these things in Rust unsafe, and same in Go with cgo, but the point is not what you can or can not do, it's what the language guides you towards, and the only potentially damaging thing that Zig tries to prevent you from doing, is making code hard to understand for readers.
If I want unsafe languages, C and C++ already fulfil the purpose with 50 years of ecosystem.
Being safer than UNIX for certain kinds of customers, is one of the selling points of Unysis ClearPath MCP, thanks to NEWP.
At least at the moment, Rust's many features are mostly orthogonal and work well in combination. But in some sense, that makes the learning curve even harder, because you can't focus on an "accepted subset" as you would in an existing C++ codebase.
We will keep the productivity of the existing eco-systems and tooling, with an additional tool for the 1% of use cases that it actually matters.
Languages like Rust and Zig are just on a different level, for cases where performance or hardware restrictions are the number one concern.
Furthermore, isn't go a statically compiled language? Why isn't go comparable to c + others?
Aside from just wanting to be creative, the desire to do it usually indicates either a missing core language feature, or a programmer's missing knowledge of how to use core features effectively and idiomatically. Otherwise you'd use the simple core language feature (the one every developer is guaranteed to know how to use already) to achieve your goal.
The other reason it's sometimes used is prioritizing terseness over simplicity. That is, you introduce some meta-programming abstraction that saves you some LOC, but at the cost of abstraction overhead. Almost always, you're better off with the reverse priorities.
I would also say that for "very large long lived projects", memory safety is not actually the most important issue, but rather correctness, followed by (in no particular order) safety, performance, explicitness, readability and exhaustive fine-grained error handling, the latter also not found in too many languages besides Zig.
Assuming we both mean "memory safety" as in a guarantee given by the language (e.g. Rust), then no, logically speaking, it can't be a requisite for correctness, and it's not even a subset of correctness.
Here's why:
If you can write a correct program in a language which does not guarantee memory safety (which we certainly could, for example, simply by not allocating memory at all, or not using pointers etc, or by using runtime checks e.g. to ensure there are no double frees or out of bounds reads/writes), then memory safety is neither a subset of, nor a requisite for correctness.
Memory safety is a double-edged sword. It can make correctness easier to achieve. But that also depends on how the language implements memory safety. If this is done by at the expense of a steeper learning curve, then that could in itself be an argument that the language is less likely to lead towards correctness, as opposed to say an almost memory safe language that implements 80% of this guarantee while optimizing for readability, and with a weekend learning curve.
Historically, the lack of memory safety has obviously been the cause of too many CVEs. But even CVEs in themselves are more a measure of security than correctness. I would say that exhaustive fine-grained error handling checked by the compiler is probably right up there for writing correct programs.
For a systems programming language though, I think Zig hits the sweet spot, and not only with regards to memory safety.
Correctness, in this realm, is as much memory safety as:
* error safety (making sure that your program correctly handles all system call errors that could possibly occur, without forgetting any, and there are many! The Zig compiler can actually inspect and check this for you, something not many languages do), see https://www.eecg.utoronto.ca/~yuan/papers/failure_analysis_o... for how critical error handling is in distributed systems,
* OOM safety (making sure your program can actually handle resource allocation failures without crashing),
* explicitness (clear control flow with a minimum of abstractions to make it easy to reason about the code with no hidden surprises, no weird undefined behavior),
* and especially as much runtime safety as you can possibly get from the language when you need to write unsafe code (which you will still need to do when writing systems code, even if your language offers memory safety guarantees, see https://andrewkelley.me/post/unsafe-zig-safer-than-unsafe-ru...). Here, Zig helps you not only at compile time, but also at runtime (and with varying degrees of granularity as you see fit), something not all systems languages will do.
On all these axes, Zig is at least an order of magnitude more likely to lead to a correct program than C, while optimizing for more readable code (even for someone coming from TypeScript) and thus code review, also essential for improving the odds that your code is correct with respect to its requirements.
>This means you can be sure that the following code calls only foo() and then bar().
No you can't be sure of this. Zig has different rules about error handling depending on what mode you build your software in, and it's not clear or rigorously specified how different modules built using different modes interact with one another when it comes to error handling.
So that initial line:
var a = b + c.d;
That may end up causing a panic depending on the type of b and c.d, or it might result in undefined behavior, in which nothing is guaranteed. You also need to know something about the type of c, since if c is a pointer type then it could be a dangling reference.As for hidden allocations, the only hidden allocation I know of was C's now deprecated runtime length array. C++ doesn't have this feature and I don't know of any core language features in C++ that allocate dynamic memory any longer. Some older compilers used dynamic memory for exception handling but modern C++ compilers preallocate memory for all of the standard exception types. I want to say that Rust also doesn't dynamically allocate memory but I'm not 100% sure about that. D does a boat load of dynamic memory allocation in the core language which is rather unfortunate.
For bare metal support, Rust has first class support for no standard library. C++ does not but in practice clang/GCC and MSVC all support compiling without it.
As for being a portable language, that's a nice goal that Zig can establish for itself, but that's all it is right now.
For build system... well C++ has an atrocious build system, there is no justification for it nor is there any defending it. Good on Zig for wanting to make the entire developer experience pleasant and making package management a core priority.
And finally for simplicity, that word really doesn't mean anything anymore. Everyone calls their project "simple" and everyone has a different meaning for it. Zig can claim its simple, maybe it's true, maybe it isn't, I think that's something that can only be decided if and after it gets some degree of adoption.
The core D language allocates closures and exceptions which is like Java. Rust and C++ don't do any kind of dynamic allocation for these features.
Rust and C++ don't have associative arrays in the language to begin with, so if you write C++ in D you won't allocate.
I believe there's plenty of functions in C's stdlib that just call malloc as they please, like localtime, for example.
That's pretty much the whole point: allowing the programmer to know if a function might allocate or not, and allow them to customize which allocation scheme to use by passing in whatever allocator they prefer.
The article mention coroutines. There may also be an allocation when one throws an exception.
> Everyone calls their project "simple" and everyone has a different meaning for it.
So true. For example, I'd say Brainfuck is simple. And much simpler than Zig. But that does not mean I'd want to use it.
From reading it over, it looks like Zig does use dynamic memory for coroutines but it requires the memory to be passed in at the point of construction. That has some pros and cons to it and is consistent with Zig's overall approach so I can respect that choice.
C++ has no build system. It has a compiler (and a preprocessor). That's it.
You can choose from a large number of possible build systems, some of which are atrocious (not always for the same reasons) and some of which are very nice.
C++ also has no package management system. For a lot of us , that's a feature not a defect.
No audit needed since 2014 when the @nogc attribute was introduced. Under @nogc there is zero hidden allocations.
I would like a language designed to target a hierarchical-memory system. I would like a language that forces me write single-threaded batched (or "blocked") algorithms with low cache misses.
How many pieces of code are out there where an easy x4 speedup could be achieved today if there were written with batched operations from the start? (It also shows how limited are the compiler in autovectorizing)
Rust gives me guarantes regarding memory safety, I would like a language that gives me guarantees that SIMD instructions and 2 cache levels are correctly used, without having to read the source code and compiler output.
You can even align the stack memory for a function and this is all upfront in the documentation. You don't need arcane compiler specific pragmas. Zig just makes it easy. Zig's alignment options are so powerful and neat and available compared to C, right down to allocations with custom alignments, and all first class at the language level. Compare that with C's malloc() and posix_memalign(). Implementing a Direct IO system in Zig recently was also a breeze.
I also appreciate Zig's approach to memory management, where even the choice of allocator is considered important, and for things like async/await, Zig's explicitness around memory requirements is brilliant. Zig's @frameSize builtin (https://ziglang.org/documentation/0.7.1/#frameSize) will tell you exactly how much memory a whole chain of functions will need to run async, including their stack variables. You can even choose where you want their async frames to be stored: global, heap or stack.
Again and again, Zig's design decisions have just been spot on. Huge kudos to Andy Kelley and the Zig communities.
> How many pieces of code are out there where an easy x4 speedup could be achieved
> today if there were written with batched operations from the start? (It also shows
> how limited are the compiler in autovectorizing)
Even before auto-vectorization, I'd love a functional automated "loop unrolling with interleave" that works on large functions. There is a pragma for this in Clang, but when I checked it in clang-9 it didn't work. I'll have to try again as v9 is a bit old now. When this is well supported it will avoid easily "filling the pipe" on multiple issues cores when doing batched operations, without having to manually unroll the loops as is done in VPP for example:
https://gerrit.fd.io/r/gitweb?p=vpp.git;a=blob;f=src/vnet/ip...Manual unrolling works, but getting the same effect with a simple pragma on top of the loop looks so much more attractive ;)
It sounds like the complaints here are operator overloading, global allocator by default, and metaprogramming. I think the value of these features are certainly not objective, but it's not a very compelling argument to avoid a language because of them.
As for optional standard library support, this is actually really common in Rust and is a breeze to support. Just annotating [no_std] basically gets you there libc wise. There's even additional restrictions like [no_core] to remove even more. I believe this should be noted.
I think the author missed an opportunity to really drill into actual pain points like the steep learning curves to c++ and rust, and where/if Zig excels in specific examples.
</rust evangelism strike force>
D also has a really rather nice composable allocator system in the standard library.
There are also a few people who want to rethink the design of allocators to allow for inline storage. It definitely sounds interesting, but may end up being too complicated.
EDIT: thanks to the links below I found this [1] and this [2]
https://stackoverflow.com/questions/20317413/what-are-transp...
std::map<std::string, int> m;
std::string_view sv = "mykey";
auto&& iter = m.find(sv);You're looking at it from the wrong direction. Both Zig and Rust target low-level programming, i.e. domains where C and C++ are very established. You don't need a reason to avoid a language -- you need a reason to invest a great amount of effort to switch away from an established incumbent.
Now, my biggest issues with C++ are, in this order: 1. language complexity, which makes understanding and changing codebases harder, 2. long compilation times which lengthen cycles and reduce software quality, 3. lack of memory safety. Rust improves on 1 a tiny bit, and solves 3. Zig solves 1, 2, and almost completely 3. This means that I have little reason to even consider switching to Rust; I will only if it ever becomes dominant. I wouldn't switch to Zig right now -- it still needs to prove itself, but at least it's a contender, because it offers something quite radical. Rather than an improved C++, it is a whole new way to think of low-level programming, it seems to address precisely the things that bother me about C++, plus it focuses on other aspects that are very important to low-level programming, like cross-compilation.
Seems like something that will be improved over time in Rust.
Do you see any advantages of Rust over Zig, which wouldn't fall under point 3?
I would say that the biggest philosophical difference is this. While Zig and C++/Rust are all low-level languages and so suffer from low abstraction (it comes with the territory) -- i.e. implementation details are hard to hide, and changing them requires changing in the APIs clients -- C++/Rust invest a lot of complexity in trying to make the code, once written, look as if the language has good abstraction. The code is still as hard to change, but it looks like a high-level language on the page. Some people may like that, but Zig completely rejects that.
I think Bryan Cantrill had a pretty spot on description of this kind of thing in his "values" talk, even if zig is not on his list.
It used to solve 1 (although it becomes worse every year when new languages feature are introduced), it solves 2, and most of 3 (with automated reference counting)
So it is safe by default, but for those hotspots where it actually matters, you can write the most performant, unsafe code
I might still consider choosing Zig over C, but since Zig has nowhere near the embedded coverage as C, with all the obscure compilers, it seems to me that Zig will remain a niche contender.
Is that just an analogy between Kernel and userspace from operating systems? Or is it an actual term used for programming languages, when abstract from an operating system?
If I was running zig on a bare-metal embedded device, and wrote my own heap allocator, is it appropriate to call that a "userspace" heap allocator?
At a language level, zig does not have a "new" or a "malloc" or anything of the sort.
Unless Zig only does static allocation of memory (fixed stack and heap), it is equally at the mercy the Linux optimistic memory allocator.
With Linux, you can request your memory, get no error code, and then when you go to use it--BOOM.
D sometimes feels like Rodney Dangerfield =)
I used to believe Object Pascal would replace C++, but now it is not even mentioned
At a glance, this seems not possible in Zig because it's a compiletime flag?
We still need to nail down the interface, but there is no technical blocker.
Not sure what kind of thing I write in Zig. Will also try it, anyway :)
I'll admit, I don't know zig. But how would it implement 1.0*2.0 for an architecture that doesn't support float multiplication? I assume that it would do something like compiler-rt and replace that operation with a function call. See https://github.com/llvm/llvm-project/tree/main/compiler-rt/l... for examples of "code that doesn't look like a function call" that actually is.
I still haven’t tried it myself. But I see now that there are plenty of people who have made an informed choice and chosen Zig over the other systems programming languages. So obviously they must be doing something right. Obviously Zig has something that system programmers like.
So, props!
writing software is complex, you do actually need sophisticated tools. Saying the language is simple just moves the complexity somewhere else. I say having many of the hard problems be solved by the language itself and its much larger development community is much better than just heaping it on the individual programmers (who might then get it wrong).
Zig enforces correctness in a lot of places, it doesn't have C's sloppy implicit type conversions, it's impossible to accidentally use uninitialized data, it has "proper" arrays and slices with range checking, it enforces to handle return values, it has a proper error-handling system, etc etc...
But it's quite hard to balance "correctness" with "convenience", Zig has placed itself somewhere between "hippie C" and "extremist Rust". When it comes to enforcing correctness, it's much closer to Rust than C though.
Eh, in my experience people mostly use 'sophisticated tools' because either their language doesn't do enough to help them out or the just really like making things more complicated than they need to be.
Like, watch Casey Muratori develop a game in front of a live audience with C++ using a text editor, compiler, and a debugger. Or Jonathan Blow developing a compiler in C++ and a game in his own language using a text editor, a compiler, and a debugger.
> ...without needing to know the types of anything:
Point by point:
- No hidden control flow: Linus Torvalds used to argue for C over C++ for similar reasons. I think it's a valid point for some kinds of systems programming. IMO, the argument is a lot stronger for features like destructors (C++) or Drop (in Rust) because those are truly hidden. At least the + sign is visible, and experienced C++ devs read it as a function call anyway.
- No hidden allocations: This is just a library design choice in C++ and Rust, not baked into the language. The standard libraries of those languages usually favor convenience over the last bit of flexibility and performance. At least in the case of C++, there's an ecosystem of libraries like the EASTL that make different choices.
- No standard library: That's fully supported in common C++ compilers and Rust. If Zig goes beyond what those languages offer, which it might, then the article should probably explain how.
- Error handling: "Zig is designed such that the laziest thing a programmer can do is [...] properly bubble errors up." That's also true for exceptions. Presumably, Zig doesn't have exceptions. This could be explained, and contrasted with Rust's monadic approach to error handling.
- Compatibilty with C: That doesn't tell me much, because C++, D and Rust all have excellent C compatibility.
- Package manager: Great! Both Rust and D also have one, so this might mainly be a point in contrast to C++?
- No metaprogramming: I looked up how Zig treats format strings. It looks like it's possible for functions to have "compile time arguments", and there are some rules around compile-time evaluation of if statements and inlineable loops [1]. At first glance, there might be some parallels to D's metaprogramming capabilities, which also has static ifs and loops, but I don't know if that mechanism is as powerful. It certainly looks elegant, but to me, it clearly is a kind of metaprogramming, not "no metaprogramming".
Having said all this, I'm very happy that there are more languages in this space now.
Maybe at this point, the best strategy for a project like Zig is to address Rust head-on in comparisons like this. It's very unlikely that someone reads this article in 2021, is looking for a new systems language and isn't also considering Rust.
[1] https://ziglang.org/documentation/master/#Compile-Time-Expre...
Zig's @cImport/translate-c is significantly easier to use than Rust's bindgen (though it's been a few years since I used bindgen) plus it can translate actual functions to Zig and not just declarations (e.g. inline functions)
(BTW, bindgen hasn’t changed that much. The expectation seems to be that people write a safe wrapper around the generated bindings anyway, so not a lot of effort goes into making them maximally convenient. I also don’t find that ideal.)
What this article mentions about non-global allocators will fit perfectly.
I can write a variety of memory allocators, kept safe by linear haskell
more generally, anything you can do in C, you can do in Haskell. So malloc is fair game (although i'd just call the C one from Haskell)
The explicitness is supposed to be respected by libraries, so that then the writer of the final program can enjoy maximum flexibility.
It's impossible to misuse a function if it takes all its dependencies as arguments, etc..
With that said, I create structures that embed their allocator(s) and use them for their whole lifetime, with the rule that all their allocations have to come from the embedded ones or ones passed into methods (usually I don't mix argument allocators & stored ones, but this isn't a hard rule).
var a = b + c.d;
actually do?The operation itself implies that both b and c.d must be primitive types and so the semantics of the operation are defined by Zig's language rules.
edit: To clarify - this in contrast to, say C++, where nothing can be inferred about the types of the variables involved and the semantics of the operation, since '+' can be overloaded.
What looks like a field reference (`c.d`) is just a field reference; there are no getters or @property functions that are doing more complicated things.
And similarly with the addition operator, that plus sign is just addition and doesn't call a function somewhere else.
These abstractions are considered useful by the designers of other languages, but they are specifically excluded in Zig. The benefit of not having them being that it's easier to follow the execution flow of the program.
Pretty sure Go doesn't have throw/catch.
Go has panic/recover, which are basically exceptions with some different scoping rules.
rust, i believe, can also recover from panics, but it's even less ergonomic, which makes it even more of a feature
zig goes to the extreme and makes panics completely unrecoverable, tho it's unclear how practical that is for e.g. long running servers w/ many clients
interesting discussion here:
I also don't like that the discord is run like a cult. If you say anything bad about Zig, you're berated until you ultimately have to leave. It's the Rust community all over again.
Okay, but what's the advantage, when due to optimizations such as inlining and tail call elimination, this isn't reliable in the other direction to begin with?
The reason compilers can remove function calls as such as an optimization is because it doesn't alter the semantics.
Rust will certainly inline most implementations of `std::ops::Add` to begin with as they tend to be small enough, and does it really matter they not be inlined?