compare [language] with C, fairly and accurately, if you want me to believe a word you say.
Rust has never been publicized as a solution to all problems. It happens to be my current favorite language for many tasks, but I don't think any serious Rust developer harbors the impression that Rust is designed to implement everything.
That being said, the community is much larger these days, so I assume that there are users who do not have a sufficient PL background to understand the tradeoffs made in the design of Rust.
Go was aiming to be a replacement for C as well, but they didn't even try to reach that goal because Go programs require an OS to run on; something must host the runtime, even though it's included in the binary. I love Go and I dislike C quite a bit.
Zig vs. C seems fair to me, because there are no Zig zealots (or very few, at least not yet) and there are no C zealots who have lots of experience with C.
I’m darn sick of reading articles that don’t consider tradeoffs. Engineering can be defined as “a search for the optimal set of tradeoffs”. But, it seems like every article I ever read about new tech only discussed pros, not cons. I don’t get it. The cons are just as important, if not more so. Unfortunately, for some reason, people associate cons with flaws/weaknesses/failures rather than just a “point on the tradeoff curve”. Often: no perfect solution exists and those claiming otherwise are being dishonest. I would love to see this culture change at least for engineering literature where we should know better.
All that said: I DO think that Zig has selected a reasonable point in the tradeoff curve. I just really want to understand that point better. How am I supposed to use it most effectively if I don’t understand it?
If your dependencies wrap more than 2 external libraries that have poor architecture, you're about to have a miserable time in Rust.
Rust creates a lot of friction if an external library has a poor architecture. And, by default, most libraries have a very poor architecture. This leads to things like:
"Giving up on wlroots-rs"--http://way-cooler.org/blog/2019/04/29/rewriting-way-cooler-i...
Talk about the issues of RLua--https://www.reddit.com/r/rust/comments/biq864/giving_up_on_w... "The thing that was SO tiring about writing this for me wasn't Rust, it was reasoning about the C library that I was binding. I'm just... tired of using C APIs! I'm tired of not having the compiler reason for me, I'm tired of having to second guess at what invariants the API expects and guess at what of a myriad of interesting non-local behavior a function call will do. I know that the Lua C API is a really, especially bad example, but I just don't know what the general solution is here."
Consequently, this is where you get "Rewrite All The Things In Rust!" What's the point of fighting with Rust over all these nice safety guarantees and then throwing them out at the API boundary? If you can keep everything in Rust, this simply isn't an issue.
This is where Zig shines. It catches some of the main classes of bugs but still lets you talk to libraries with crap architecture. Zig gets the fact that infrastructure and ecosystem matter--unlike everybody else operating in the C space (the cross compiling is phenomenal, for example, because they put a lot of work into it). Zig is doing some interesting things, but it's mostly some very solid choices with a lot of elbow grease applied. And that's worth a LOT.
As an aside, I will also say that Zig feels better for embedded programming than Rust. Rust feels like it has stalled in embedded areas (things like sized integers, bitfield packing, placement construction of data structures, statics and globals that don't have locking overhead, lack of do-while, etc. although I would like to point out in Rust's favor that Rust finally got label-break-value which is a godsend when porting embedded code with goto error handling) while Zig has explicitly targeted those from the beginning. That makes Zig feel better for embedded from an ergonomic perspective, to my (obviously quite subjective) taste.
That said, until someone works out how to properly integrate Zig or Nim or etc. into CMake, then there’s always going to be friction. Too much of the IDF world relies on it and it’s quirks.
In Nim land I get around this by --compileOnly to C sources, which works but is a little cumbersome. Rust is rebuilding the embedded world from scratch, which is a non starter for my work. I do have hope Zig will have a better story than both for this, but we will see!
Oh, interesting, I was wondering if this (breaking with both a label and a value) existed (in a different context), and I concluded it didn't exist and so probably nobody had wanted it, but turns out it's in the next version and I didn't hear about it.
I don't understand a desire for do-while, I can't imagine what I might write where that looks reasonable and the loop equivalent in Rust is not very good.
If all you wanted was nicer C then yeah, Zig is that and Rust isn't -- I just don't think "nicer C" is a reasonable goal any more.
It has to do with porting code, generally. If I'm writing original Rust, I rarely miss it. It's not a huge deal, but you bump into it juuuust often enough to (probably unfairly) curse Rust out when you hit it yet again.
Here's code from CMSIS DAP debug probe firmware from ARM: https://github.com/ARMmbed/DAPLink/blob/main/source/daplink/...
Note the nested do { do { } while (); if (err) break;} while(); if (err) break;
Should that code be rewritten? Most certainly it should, and it should be given a proper Error type. However, when you are first porting it, you need to match semantics or you get a bunch of off-by-one, missed error, or missed end of stream bugs. And, as you point out, the Rust loop{} equivalents suck.
We've all written suboptimal code, and we all live in a suboptimal world. :)
do { ... } while (((data & DAP_Data.transfer.match_mask) != match_value) && match_retry-- && !DAP_TransferAbort);
becomes something like loop {
...
if data & DAP_Data.transfer.match_mask) == match_value {
break;
}
match_retry -= 1;
if match_retry == 0 {
break;
}
if check_transfer_abort() { // Replaces DAP_TransferAbort
break;
}
}
I think that's actually more clear, and my mental focus is DAP_TransferAbort. I think it's just C programmers being C programmers, and we just want AtomicBool for this but I'm more comfortable wrapping it in this check_transfer_abort() function while I investigate whether anybody thinks this is a counter, or there's actually physical hardware backing it despite the way it looks like it's just a global variable.If we take comptime feature away, Zig is basically Modula-2 (1978) with friendlier syntax for folks with C background.
Everything else in terms of security Modula-2 did better (yes it did have null pointers, but supports null checking).
And the tagging mechanisms for use-after-free are anyway available for C and C++ compilers for at least 20 years (Insure++, PurifyPlus and VC++ 6.0 debug allocator being part of the first ones).
Other things that are made explicit such as specifying an allocator on every allocation is an inconvenience that you pay for that flexibility. The choice of safety being added rather than assumed and later satisfied or declared an unsafe area is vastly different with a tradeoff in how developers use the language. An example given was how the self-hosted complier can use cache optimized untagged unions with the tags in a separate array.
If I had to guess what the single biggest pattern of tradeoffs is, I'd say it's about being explicit rather than implicit. You gain fine-grained control at the cost of brevity/convenience.
I assume zig has a finite bound on comptime_int, so we can in fact always know what is_odd_perfect_number() returns.
> we can't type-check zig libraries which contain generics. We can only type-check specific uses of those libraries.
I.e. you cannot say whether this compiles for all n, only for the instances you actually instantiate
- library users will find compilation errors in the library code (his is annoying, but when you get used to it, you can treat it as any kind of library bugs and submit a PR)
- library users are going to rely on accidental behaviors (that is “in the library author's mind this isn't been used this way”) and then you'll have breaking changes without noticing.
(I say that as someone currently working on a DSL for the train industry, and this language (started in 2012) has something really close to Zig's comptime and we're currently trying to move away from it, because of the aforementioned issues. And it's actually pretty hard because big chunks of the existing code don't actually compile unconditionally and only compile when a given set of compile-time conditions are met, which makes the migration process hard)
There is a lot of templatized c++ library code out there, yet in everyday practice I find it's very rare that I need to instantiate a template with a type that causes a compile error.
That's true. C++ templates are exactly like that too.
They are a bit arcane though, and many developers are afraid of them and use them very conservatively or even avoid them altogether so maybe this helps mitigate the issue.
Comptime (at least with the language I'm currently working on) are a lot less scary, and people use them quite a lot without thinking too much, so maybe it's the main difference. And I think Zig is closer to this than it is to C++ templates, but I can't be sure since I've never seen people writing Zig code.
Edit: it's late in here, and I completely missed your point. I'm personally not usually afraid with language features, but some requires more work than others to be accustomed to, and not everyone is willing to spend their spare time doing so.
I used to code in C++ and I had that problem quite often. Including in implementations of the STL.
And if you put that in you API and someone call foo with an odd perfect number, well it will fail to compile.
It's conditional compilation, exactly what C does with macros. Except here, you don't have to learn an awful and honestly, sometimes, incomprehensible parallel language.
Zig has some shortcomings but I fail to see how this is one.
That's a valid point indeed and there's an issue for that: https://github.com/ziglang/zig/issues/1268
Rust’s tradeoff in providing safe and faster code is mentioned. Why not Zig’s?
Manual memory management gives obvious downsides which should be well known. Zig does this in the best way I have seen though.
I'm using Zig and C for a good amount of time now and i disagree. Zig has simpler concepts as C, but in C you can just ignore them.
Consider
char buf[4];
float * f = buf;
*f = 32;
Seems simple, compiles, and even executes. Everyone is happy. But that code will just explode randomly on non-x86 platforms as it violates alignment rules. In Zig, this code will give you a compile error, as []u8 does have alignment 1, while a []f32 will have alignment 4. This means you have to learn about alignment before accidently writing code that will randomly crash at runtime depending on the stack position of buf.And Zig has a lot of these things that require implicit knowledge in C as explicit knowledge implemented in the language itself. Same goes for integer casting, shifting more than sizeof() bits, and so on.
It moves the footguns from the code to the user, and so the user has to learn about all of these concepts before writing Zig code. In C, the code will silently compile and misbehave at runtime. This might feel like additional complexity in a lot of places, but the complexity is there in both languages. One just makes it explicit.
But this is obviously not true for all things in Zig. comptime is a whole new feature that wasn't seen in a lot of languages before and afaik not as it is used in Zig, so it's something new to learn even if you already can do perfect C. The same goes for more precise data types (struct vs extern struct) and so on.
One hard footgun in Zig right now is implicit aliasing of parameter types, which sometimes get passed as a reference and sometimes as a value. This is a known footgun is already on the table to be adressed by better semantics and more safety guards
For me, writing Zig feels way less complex than C because a lot of brain load that went into writing correct C was offloaded to the compiler, which is good. But it also increases the friction when writing code as the compiler forces you to think about alignment and such.
I actually don't know what Andrew is getting at with that particular case though. If the situation is that the types are obvious, so the Zig isn't ever looking at the tags, the Rust won't look at the tags either. Worse, I don't think Zig has niche optimisation, so there are a bunch of cases where Zig has to choose, have tags (more space, slower, but same safety as Rust) or go without (same size as Rust, but less safety). Rust only guarantees the niche in certain narrow cases like Option<&T> (is the same size as &T) but it actually delivers a lot more than that, and this continues to improve.