Hacker News new | past | comments | ask | show | jobs | submit login
Zig as an alternative to writing unsafe Rust (zackoverflow.dev)
308 points by zackoverflow on March 7, 2023 | hide | past | favorite | 217 comments



I buy the premise that Zig is better if you know you will have lots of pointer arithmetic going on. Having written a fair amount of unsafe C interop code in Rust, I feel like these critiques of the ergonomics are valid. The new #![feature(strict_provenance)] adds a new layer of complexity, that, I hope, improves some of this experience while adding safety. Rust's benefits are not free.

The benefits of Rust's (wonderful) model around references and lifetimes come at a significant cost to ergonomics when having to go into the Mordor of some C library and back. I usually find myself wishing I could have some macro where I just write in C and have it exposed as an unsafe back in Rust. I know I can do this by just writing a C dylib and integrating that, but now I've got two problems.

Even still, I prefer writing unsafe Rust to writing C. std::mem::ptr forces me to ask the right questions and reminds me of just how easy it is to fall into UB in C as well.


> I usually find myself wishing I could have some macro where I just write in C and have it exposed as an unsafe back in Rust.

In D you can just import a .c file mycfile.c with:

    import mycfile; // C file filled with C functions
and they'll be treated as @system code by the D semantics. They're even inlinable.


Here's one that does that: https://lib.rs/crates/inline-c

I'm not sure it's what you're looking for, but it seems like a good starting point.

As for the general thrust of your comment and the article, I agree. It'll be interesting to see what changes come to make things nicer.


This one runs the C code as a program, so the code doesn't really communicate. I made a proof of concept some time ago of a macro that really translates C to Rust at compile time: https://github.com/zdimension/embed-c


Shameless self-plug: https://github.com/zdimension/embed-c

This transpiles C to Rust at compile time, though it requires a nightly compiler and hasn't been updated in some time. But it's exactly what you're looking for. C code in, unsafe Rust code out.


You are not alone. The other day I was checking out a new programming language and the author rewrote the unsafe rust part to zig: https://github.com/roc-lang/roc/blob/main/FAQ.md#why-does-ro...


This was quite literally the motive for this article.


who reads articles? (joking ;) my bad)


One big difference between unsafe Rust and C is that C compilers have flags to turn off the UB, so you have a lot less mental load when writing it. You go from e.g. "if this index calculation overflows, we may read from outside the array, because the bounds check was deleted" to "if this index calculation overflows, we may read from the wrong index, but never outside of the array bounds". UB is Damocles's sword and the speed gains are usually not worth it. With UB your program can enter a buggy state that you cannot detect, because "it cannot happen". Without UB, your program can still enter a buggy state, but you can detect it and potentially recover or crash immediately before even more things go wrong.


Very true and worth evangelizing to others. I have unknowingly violated -fstrict-aliasing in some part of my code only to discover later that it is benign at -O0 and metastasized at -O3.

This freaks me out the most in networking code, where there is all kinds of casting of structs (esp. if you blindly copy-paste examples from StackOverflow) and performance usually matters. Rust has inspired me to take more time to profile C code to see whether strict aliasing (strict overflow, etc.) actually make a significant enough improvement to merit the UB-risk, review time, and acid in the stomach.


Some flags to turn off some UB, and not all of them have those flags.


Are all the UBs defined/documented/catalogued somewhere?


Which compiler flags do I use to turn off UB?


> I usually find myself wishing I could have some macro where I just write in C and have it exposed as an unsafe back in Rust.

There’s a macro for doing this with Assembly, I can imagine one could be made for C. But why wouldn’t you just write unsafe rust at that point?


Perhaps because you want to import something you don’t want to rewrite? There’s a lot of tested C code out there.


That's a good point.


Colocation, and avoiding the ceremony of standing up all the extern types (and integrating a separate C compiler, etc)


This pretty much mirrors my experience. Rust is the inverse of Perl: It makes the easy stuff hard.

Writing basic data structures isn't a niche, esoteric edge case. There may be a crate that "solves" what you're trying to do. But does it rely on the std---(i.e., is it unusable for systems programming)? Is it implemented making gratuitous copies of data everywhere? Does it have a hideous interface which will then pollute all of your interfaces? Does it rely on 'unstable' features?

Then, there's the 'community.' It seems to consist solely of extremely online people who get a dopamine hit from both telling people they're doing things wrong and creating the most complex solutions possible. They do this under a thin veneer of forced niceness, but it's not nice at all.


> It seems to consist solely of extremely online people who get a dopamine hit from both telling people they're doing things wrong and creating the most complex solutions possible.

I've observed that certain programming languages have a culture of complexity. I'm not sure why this is. I can only speculate its because these programmers are working on "boring" problems so they make busy work for themselves OR their beginners who think this is how "real programmers" work.

While I think calling them "idiots" is a bit strong, I think this quote from the late Terry A. Davis is worth remembering: “An idiot admires complexity, a genius admires simplicity [...] for an idiot anything the more complicated it is the more he will admire it, if you make something so clusterfucked he can't understand it he's gonna think you're a god cause you made it so complicated nobody can understand it.”


I don’t know, people who go too hard on simplicity often don’t even grasp the underlying problem in a given case (looking at this utterly dumb piece of text as an example: http://harmful.cat-v.org/software/) - sure, no sane people would want to deliberately introduce complexity/abstractions, but abstraction is the only weapon we have against complexity. Problems have an inherent complexity which simply cannot ever be reduced. If you do have to make it work, what else can you do?

I think go is a good example for this fallacy, it claims that it is a simple language, but ad absurdum asm should also be trivial to understand as every line is also easy to grasp, right? Low expressivity just creates chaotic complexity, won’t reduce it.


Some people seem to really struggle to understand that abstraction is intrinsic to computer science. Trying to get as close to the metal as possible while using your daily driver is a fun hobby but there is nothing innately virtuous about it.


I quite agree. I take it a step further: I think devs should be taught how to handle complexity better. It's a skill that some programmers have and that others sorely lack, but AFAIK, it's not a skill taught at all in CS programs.


Complex problems and complex solutions are two different things. The genius is in solving complex problems with simple solutions. While it might seem that adding a crate to solve a problem is a simple thing, from the system programmer point of view (mine), it is not.


But it is simply not possible to solve many many complex problems with simple solutions, no matter how hard you try.

Also, using a (hopefully) well-tested library that someone wrote is absolutely a good (and only significant, as per Brooks) way to increase productivity - but also correctness! Do I really have to write a matrix library myself? Sure, I can probably write a function that multiplies two matrices, but will it have sane numerical stability? [1]How can I even test that out? It is a whole separate field, where I likely overestimate my knowledge about. Was it really worth that extreme amount of time to write an almost surely buggy and slower implementation than depending on someone else’s work?

The art of programming is also about knowing when to reach for other tools.

[1] Hell, multiplying multiple matrices already “needs” a dynamic programming algorithm to decide how to associate pairs of them


I would definitely do like you and reach for math libraries/crates whenever I can, since I suck at complex math. But this also means three things: there is the chance we are both no geniuses, we added a complex "black box" solution (who knows how this library/crate work?) and now we have to babysit a dependency.


>abstraction is the only weapon we have against complexity

I don't agree with this. We also have stinginess and stubborness as potent weapons against complexity. If we are stingy with our time and effort then we insist on getting much use out of every tool we learn. We are stubborn, and so we do not add new tools quickly, or throw old ones away quickly, and we are skeptical of easy solutions. tldr: first cut away needless complexity, then use abstraction on what remains.


Do note that I was talking about essential complexity, not accidental. The latter can and should indeed be dealt with.


Terry would have probably used a word stronger than 'idiots'.


Insanely enough, he was quite lucid (at least compared to other live-streams) when he said this quote, so no n-words there. [0]

[0]: https://youtu.be/k0qmkQGqpM8


> Rust is the inverse of Perl: It makes the easy stuff hard.

It's a bargain. Rust is pretty great, but it doesn't make some things easy because it would make everything else hard.

This comment is amusing, mostly because Perl is so full of tradeoffs. Do you want to write something to do some string parsing quickly? Great language, maybe. Do you want to understand what you've written later? Maybe not so great.

> Writing basic data structures isn't a niche, esoteric edge case.

Not if you're using C, because the batteries are definitely not included. Want a resizable array, or hashmap? The answer is DIY. Not in the std library. Whereas all are provided by default in Rust. Picking a basic data structure off the shelf is a pretty nice feature for most applications.

That said -- should you really need a custom implementation of a linked list, and you need to write and rewrite such an implementation all the time -- I'd understand if Rust wasn't your first choice.

> Then, there's the 'community.'

And I'm not sure anyone loves this attitude either. Keep it technical.


> And I'm not sure anyone loves this attitude either. Keep it technical.

Still, we, the Rust community, should take the high road and respond to the criticism by assuming that it's valid and asking what we can do better. I, for one, don't want developers to reject my Rust-based library because of the community's reputation, especially once my library has a C API.


> Still, we, the Rust community, should take the high road and respond to the criticism by assuming that it's valid and asking what we can do better.

I have to disagree. I think this meme/issue has been talked to death. And I'm sure I find retrograde, sour-pussing about Rust just as distasteful as others find dewy Rust optimism. The answer is -- they're both silly. Draw the line at speaking about the tech or the comment or person in front of you. Speaking about stereotypes in "communities" is about as vacuous as a Valley Girl's/Perl programmer's head[0].

[0]: a joke!


But also, where is this malevolent, toxic Rust community that disgruntled C++ developers always engage with? I've yet to encounter a single person from this unstoppable force of sanctimonious assholes ruining everyone's fun on non-Rust projects. By their accounts Rust is a mental health catastrophe for our industry.

Anyway, shit posting aside. I do agree with them that the Rust community is a bit eager to suggest rewriting stuff in Rust.

However... I think this has always been the case for every developer who likes a language. I've seen Ruby devs talk with a passion for Ruby, and Elixir devs about Elixir, and Haskell devs about Haskell. You get the point.

Except I suspect Rust just incredibly grids the gears of some C++ devs. I see how it can be irritating, especial if they've decided C++ is the language they "settled in" with. There's the implied (and irrational) existential threat that this new technology is going to ruin their future prospects. This is probably subconscious and also completely irrational. C++ is too big and too commercialised to go away anyway.

I've seen a similar thing when TypeScript was being adopted and I was pushing for TypeScript (or Flow) adoption at work.

Some people seemed to have an allergic reaction to any mention of TypeScript.

There was people calling the TypeScript community toxic, immature, incompetent, holier-than-thou for arguing why types make code easier to maintain and write. And some of the arguments were they don't want to use the language that had such a childish community.

Edit: On second thoughts I feel a bit like an asshole too, and a bit regretful and ashamed I stooped so low to even write this comment. Others wrote more level headed replies than I did, too. Anyway, let it serve me as a reminder not to engage with trolls in the future.


As polyglot dev, with love/hate relationship with almost any language worth using, until Rust gets free of "my compiler compiles yours", there is always an attack vector from those devs.

It was like this during the Usenet flamewars on C vs C++, and while I rejoice most C compilers now being written in C++, there are a few domains where C++ failed to take over C.

Rust advocacy strike force would do better to learn from history of programming languages adoption.


> until Rust gets free of "my compiler compiles yours"

I would better argue that "your Rust program runs on top of my millions of lines of C code, so please pass to my C API a pointer or a reference to a pointer so I can screw it up, and now all your safe code guarantees are out of the window".


I wish more of the Rust Community are like you and own up to the issue. And then I wouldn't have to keep repeating the same thing like I am a troll. The "Keep it Technical" has been the Rust community answer for years, is basically asking the rest of world to tolerate RESF ( Rust Evangelism Strike Force), while any push back against Rust ( or specifically RESF ) are "un-helpful" and they take zero tolerance. So I should tolerate your ideals while you have zero tolerance with mine? Does it sound familiar to some real world Silicon Valley politics?

The only reason why some are suggesting they haven't seen RESF recently, is because it has gotten to the point where the backlash, which used to be a minority and voiceless finally becomes the mainstream. And so they back down.

And to the original question, and in my previous comment, when was the last time you saw people telling you to use Zig in a Rust thread? ZERO. The comments are mostly, if not 99% about how they couldn't deal with Rust's complexity, they call themselves an idiot and they only wanted a Better C, which the answer to that question is always Das C, Zig or some other contenders. In which Rust Fans will always find it offensive. Even mentioning Ada / SPARK being a languages aiming for correctness is somehow "trolling". And it is not the first time I have been told to stop mentioning Ada.

And yet, how many times has Rust Supporters came into a Zig thread ( or other language ) telling the world how Zig / X is memory Unsafe and Rust does it better? To the point so detrimental that they founder of Zig is crying or begging publicly to give Zig the language some breathing space?

And finally I want to add, HN has a link for front-page [1] where you could visit every post that was listed in the front page. And comments as well as user post are not deleted unless specific circumstances which requires Dang to manually operate. i.e All the evidence are still there.

[1] https://news.ycombinator.com/front


> The "Keep it Technical" has been the Rust community answer for years

Because it's a better answer than smearing a group of people.

> So I should tolerate your ideals while you have zero tolerance with mine? Does it sound familiar to some real world Silicon Valley politics?

I will say that your argument does feel like modern American politics in one way I notice -- it's all about vibes. It doesn't have anything to do with anything technical. It's all about how people (sometimes transitively) made you feel. And I'm sorry someone made you feel that way, but I'm not sure the way you're acting is conducive to feeling any better.


> And I'm not sure anyone loves this attitude either. Keep it technical.

If I didn't include that proviso, I'd have someone arguing with me to the death about how I'm completely wrong and an idiot who doesn't understand the Correct Way of doing things.


This is the same as people behaving as jerks claiming that other people are jerks.


It may be "not nice" but it's an accurate and sincere appraisal.


If you define "being a jerk" to include "calling out someone for being a jerk" then you will never be able to improve the situation.


They’re not just calling out though. They’re condescending with their “airquotes”, they’re stereotyping with their broad strokes.

It’s not useful criticism to simply say “this community was such-and-such”. I’d expect them to do better. Speak specifically to people saying something you disagree with. Don’t let it fester within you and then take it out on a broad group.

And I know you didn’t do any of this, but you did seem to be defending some pretty low quality behaviour.


[flagged]


I've actually read the full comment you're referencing and your take absolutely mischaracterizes what was said. Whether your take is malicious or merely reckless, I don't know, but I do know your take is the precise opposite of what was said.

Others can read the entire comment themselves[0], but the pertinent part re: this individual discussing 'unsafe' can be found in the second graph:

    "[W]hen you're learning new things and thus reading introductory material, you'll often see pithy statements that you may interpret as being commandments or something that is supposed to always be true, but in reality they are imprecise statements because it is just impossible to explain everything up front."
[0]: https://old.reddit.com/r/rust/comments/11eyu50/i_love_rust_i...


[flagged]


Congrats. You're the first person I know of to accuse me of dog whistling. Being "extremely online" for about two decades now, I'd say that's pretty damn good. So what am I dog whistling exactly? That people shouldn't use 'unsafe'? Lol. Wow, I really came out swinging! pats self on back

I am actually teaching my toddler how to use a knife. So I'm not sure how that follows...

The GP is right. My comment was sincere and means the exact opposite of what you accuse me of "implying" or "dog whistling."


> [Rust] makes the easy stuff hard.

I totally disagree with that claim. The easy thing to do is not use unsafe Rust. You can use stringly-typed datastructures with lots of refcounting or copying, just like Perl, without ever venturing into unsafe Rust.

> Writing basic data structures isn't a niche, esoteric edge case. There may be a crate that "solves" what you're trying to do. But does it rely on the std---(i.e., is it unusable for systems programming)?

In what world is Perl suitable for systems where possible memory allocation is a problem?


> Writing basic data structures isn't a niche, esoteric edge case

Maybe it isn't an edge case (although it should be) it also isn't `easy` in a non GC'd language, and a huge source of memory bugs.

I wouldn't say it makes the 'easy stuff hard' as much as the 'hard stuff appropriately difficult'.


why should writing a basic data structure be an edge case? …what?


All of the "basic" data structures are already provided by either the standard library or crate ecosystem, so you'll rarely (if ever) need to write your own linked list or hash map from scratch.

I say "basic" in quotes because once you factor in the concurrency, ownership and memory safety concerns that are optional in other languages, but mandatory in Rust, there really isn't a simple implementation of these structures that is provably correct anymore.


Rust is supposed to be a systems programming language. If writing basic data structures was an edge case in it, the language would be broken. Luckily, in my experience it isn’t (at least not in this way).


I'm glad you posted this, I was confused as to whether "basic data structure" meant something different to Rustfolk than it did to the rest of us.


Writing data structures is trivial in C/C++.


As long as you don't care about UB or edge cases. Every time Rust makes something hard, it's forcing you to handle an edge case up front.


It's blowing my mind how many commenters here don't understand this.

Rust isn't hard just for fun. It's hard because the code you've been writing for so long is actually bad and you've not been thinking it through appropriately. This is why we keep finding serious bugs in code that is decades-old, despite the belief that code so old must be well-tested by now.


> we keep finding serious bugs in code that is decades-old

I'm sure of one thing: we will find many and different bugs in decades old rust code one day, too. The problem with software is that it has code in it ;-)


Difference of course being that zero of these bugs will be due to buffer overflows or use after free or other memory bugs in safe rust, which is a huge source of bugs in C[1].

I don't understand why this argument keeps coming up. Not all bugs are the same and when you make entire classes of bugs unrepresentable, that's a massive win, especially when they happen to be the class containing >60% of the highest severity bugs in C.

https://www.chromium.org/Home/chromium-security/memory-safet...


> zero of these bugs will be due to buffer overflows or use after free or other memory bugs in safe rust

buffer overflows [0] [1], use after frees [2] [3], and other memory bugs [4] [5] [6] can appear in safe rust from unsound unsafe internals.

[0] https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-2887...

[1] https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-1000...

[2] https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-3116...

[3] https://github.com/rustsec/advisory-db/blob/main/crates/cros...

[4] https://github.com/rustsec/advisory-db/blob/main/crates/toki...

[5] https://github.com/rustsec/advisory-db/tree/main/crates/

[6] https://github.com/Qwaz/rust-cve


Rust has a low bar for creating CVEs, because the standards for safety are higher.

From the link you’re sharing

> Fortunately, they are context-sensitive library APIs that are not usually used in a way that the bugs can be triggered. Many of them require very specific interaction to trigger (e.g., partially consume an iterator and zip() it with another iterator) that is not likely to appear in their daily usage.

Here’s a concrete example that shows the difference between how different languages approach CVEs. Rust is not the only language affected by this TOCTOU bug (https://blog.rust-lang.org/2022/01/20/cve-2022-21658.html) but other languages finesse it by saying “all bets are off when you’re interacting with the filesystem”. Therefore no CVE for them, while Rust ends up looking insecure in the eyes of those who count CVEs.

What I’m saying is - don’t count CVEs without looking at the context and content of those CVEs. Don’t assume that some software is secure simply because they don’t file CVEs, because that’s an ostrich approach.


> don’t count CVEs without looking at the context and content of those CVEs

The CVEs are counted as they're memory issues (not only logical issues) that can technically surface in safe Rust code and are also considered UB in other languages (those which finesse other conditions to be "unspecified" instead of "undefined") too.

CVEs have rating systems to better interpret their relevance. Whether these ratings incorporate how easy it is to trigger or how practical the damage could be is subjective. They primarily report potential exploits, not a standard for judgement. All this nuance though was lost with the parent commenter claiming "zero memory bugs in safe rust".

> Don’t assume that some software is secure simply because they don’t file CVEs

I agree. This extends across all languages. Another warning to add is that high profile security bugs can still occur without memory safety issues (with wormhole/dao exploits being practical examples).


I think we’re on the same page here.

Neither of us agree with a blanket assertion that code written in Rust is flawless and free of bugs.

This can be easily disproved by looking at the bugs fixed in the Rust compiler and standard library, and looking at open soundness issues in the Rust issue tracker.

I think CVEs are useful in tracking the security of your code, but can’t be meaningfully compared between languages. Different languages and ecosystems have differing bars on what needs a CVE.

The important takeaway is that while Rust and Rust code in general aren’t perfect, they’re still substantially better than the alternatives. Better is hard concept to convey and get people to buy into. They see a couple of bugs and say “well, there’s bugs either way so what difference does it make”.


Yea, I think we're mostly on the same page.

For higher level domains where unsafe isn't required and some runtime overhead is acceptable, I believe there's real cases to be made that Rust can be substantially better than the alternatives; Can prevent logical UAF with borrowing, bound checks may be mostly elided, enforced language level checks for nullability like Option<&T> and valid values in general like enums, explicit error handling, etc.

For lower level domains however, I'm not sure that argument is as easy. Anecdotally, I work almost exclusively on systems like databases, schedulers, allocators, and similar. There, memory efficiency is an explicit feature via tricks that either Rust's borrow checker really doesn't like or are very awkward to do with its unsafe APIs; Non-linear/concurrent object lifetimes, self-referential & intrusive data structures, and reinterpret casting being the big ones.

If rust code makes it harder to do those things, while also providing little (never none) benefit over the alternatives (for reference, both Zig & Rust have checked {integer ops, slice access, nullability}, sum types, metaprogramming, rich error combinators, etc.) then it's harder to make the claim Rust is substantially better.


I can't speak about the experience of writing unsafe. I haven't written any, and hope never to do so.

That said, your concerns are pretty valid, widely echoed and are being taken seriously. Writing unsafe code is a chore and it needn't be. I'm hoping the newly created Operational Semantics team (https://www.rust-lang.org/governance/teams/lang#Operational%...) makes progress here.


I don't consider this a bug caused by safe code. The bug is in the unsafe code, sure, but not in the safe code. Maybe the safe code triggers the bug in unsafe code. Maybe the unsafe code triggers undefined behavior, then anything can happen.

My point wasn't that rust is bug-free, but that certain classes of bugs will not exist in the safe portion, which significantly reduces attack surface.

Additionally, my point was to compare languages without safety guarantees like C to safe rust. Your statement is true, but it doesn't do anything to counter what I'm saying, which is that entire classes of bugs (namely the most common critical c bugs) are not found in safe rust, which is still the case.

Worded another way, within the boundaries of safe rust, you will not find a cause of a memory corruption bug. Things that can be the cause memory bugs include: unsound unsafe rust, c, kernel-level manipulation, random bit flips, malicious hardware, etc etc. I consider none of those to be bugs in safe rust, and additionally, they can cause bugs in all other programming languages as well.


It’s not that simple. Rust is used in the system’s programming space and as a result it will be used in many places where unsafe ends up required to solve problems. There won’t be memory safety bugs in safe Rust, but there will be in the interop/unsafe layers.

I think there is a strong argument to be made that if you need a lot of unsafe, Zig is going to be the safer language.


> I think there is a strong argument to be made that if you need a lot of unsafe, Zig is going to be the safer language.

I could see that. Haven't used zig but it does look very nice.

> in many places where unsafe ends up required

Yes, I definitely agree. I was specifically referring to the boundary of safe rust. I don't know how much more or less safe unsafe rust is vs c, but I do know that safe rust is certainly safer than c.


> I don't understand why this argument keeps coming up.

Not comment should be read as argument or part of a debate for or against using Rust or any other memory safe language. I made the comment because I've seen tool after tool come about to end the scourge of bugs in software... and so far my unconscious, untrained ability to find new and spectacular ways to create bugs has exceeded the ability of the makers of bug prevention tools to stop me.


While I do like Rust, let’s not forget that it indeed “forbids” to a degree many code/lifetime structures that are extremely common in basically every other language.

I believe the tradeoff for this in Rust’s niche is absolutely acceptable, but it is still a tradeoff and might not be the correct choice in certain cases.


It's in part hard because the borrow checker isn't omniscient and accepts only a subset of safe programs. There are also some concepts it's difficult to explain to the borrow checker. Both of these are things that make writing programs harder in Rust that do not add any safety value, and it's ok to acknowledge that.


That would be true if Rust was a perfect language, but it isn't (no language is).

Sometimes Rust makes things hard because the invariants you want to express cannot be cleanly mapped onto Rust's type system. I described one such case here: https://blog.reverberate.org/2021/12/19/arenas-and-rust.html


Not strictly true. Rust makes things hard by making you design within the limitations of the borrow checker, and some of those are accidental, not a necessary edge case.


Writing correct ones is not.


> Writing basic data structures isn't a niche, esoteric edge case

writing data structures _properly/well_ was never easy

it just looks easy and is nearly always a sub-par solution

e.g. a list in many lisp like languages seems simple, until you look under the hood what magic tends to be used by more advanced compilers to make that list work fast

the think people most commonly got wrong which was supposedly easy when programming when I was school/stadium where data structures, even comparatively simple ones like double linked lists

it's like sorting, sure you find docents easy to implement sorting algorithms everywhere, but then when you look at the properly implemented sorting build ins of standard libraries their complexity is hundreds of times that of quick sort or similar


>Writing basic data structures isn't a niche, esoteric edge case.

It very much should be though! That's exactly the type of thing that should be written once by someone who knows what they're doing, and then reused 1000000 times. People slapping together a quick data structure is a huge problem in C.


On top of that Rust might be the ugliest modern language.


That is extremely subjective. I find the syntax extremely easy to read, and there are significantly fewer edge cases than nearly all other mainstream languages:

* Why does C# require a break; under switch?

* What about the "Most Vexing Parse"?

* Why does Zig change capitalization of @import and @TypeOf?

Rust is an extremely "guessable" language. It's highly likely that experimenting with syntax will succeed in Rust, which is a syntax feature that I have seen nowhere else.



> Why does Zig change capitalization of @import and @TypeOf?

TypeOf returns a type. Corresponding to how type names are uppercase.


People keep saying this and I just do not get it. It's just… not that bad?


I suspect the things that people are reacting to are a mixture of the following:

* Types in Rust use prefixing to create derived types whereas C family languages generally use postfixing. A pointer is i32, not int; an array [i32; 5], not int[5], etc. This makes special characters appear more heavily at the beginning of the scan line, and probably makes them slightly more noticeable as a result.

* Lifetimes have the form 'a, and that single quote is likely to bother a lot of people (I know it bothers me).

* Unqualified name lookup in Rust is a bit weaker than other languages, which makes the scope operator (::) more common.

* Passing in explicit generic type arguments for a function requires an extra :: for seemingly no reason.

* Similarly, macros require an explicit ! in the name to invoke. Given that println! (and formatting in general) is a macro and not a function, this means you get a lot of extra uses of ! that's unexpected for C family code.

* Again, the try operator (also decently common) is another random special character.

* Attributes also use #[] syntax, or sometimes #![]. While C++11 did use [[]] to designate its attributes, it's also something that always felt a bit ugly to me personally (I find the @Decorator() pattern from Java or Python to be a visually cleaner way to do attributes).

In short, Rust generally has a higher density of special characters than other C family languages, and I think that contributes to a sense of ugliness.


> * Passing in explicit generic type arguments for a function requires an extra :: for seemingly no reason.

There is a very good reason for this. Behold, the Bastion of the Turbofish.

https://github.com/rust-lang/rust/blob/master/tests/ui/parse...


That is not a good reason as far as end users are concerned.


It's not about actual ugliness, it's just something that is hard to articulate as something other than ugliness. https://matklad.github.io/2023/01/26/rusts-ugly-syntax.html


It's amusing to me that many of the people who complain about the aesthetics of Rust's syntax are quick to also say bad things about, like, Haskell, or Lisps, or other languages with comparatively low syntactic overhead.

I think the thing people don't like about Rust is that it looks vaguely C-like but is clearly not C. People might like it better if it was further removed (aesthetically) from C's syntax. But then they would also complain.

I think there was no way for Rust to meet all its semantic goals and also make people happy about the syntax.


Typescript and Zig are two other languages that look vaguely C-like (even though they are on two opposite ends of "C like"), but both are a whole lot easier on the eyes than most Rust code.

C++ on the other hand also is vaguely C like, but can look equally messy as typical Rust code.

OTH I find Makepad's Rust style very readable, but I can't quite put my finger on it what's different from other Rust code bases:

https://github.com/makepad/makepad


Languages on the two ends of the spectrum are "not that great", IMO. Lisp is awful because even though there are very few greeblies, it introduces a TON of cognitive overhead, because you need to be constantly thinking about what something is, because the layout is TOO uniform. Rust on the other hand has a lot of greeblies and you have to remember what it is and what they do. For example macro attributes (and all their hidden effects) as well as the turbofish. Even some things like -> for the function body are simply unnecessary.


I'm going to assume, based on when he said "As a slightly more serious and useful exercise" and then proceeds to remove each piece of the code that's intentionally there for performance and safety, that the whole post is satire?


The second sentence of the post is the thesis: "I think that most of the time when people think they have an issue with Rust’s syntax, they actually object to Rust’s semantics."

By removing the ugly "syntax" (and thus also removing important semantics), they're showing that the reason Rust has a lot going on syntactically is because the code is actually expressing important semantics. You can't have a nicer Rust syntax without losing semantics in the process.


One random thing that bugs me about Rust's syntax is array initialization. In Go I can initialize an array (or strictly speaking a slice) of structs like this:

    var foo []my_struct = {{"foo", 1, "bar"}, {"foo", 1, "bar}, ...}
This is often useful in tests (where each struct value represents a test case). Rust doesn't seem to offer any similarly compact initialization syntax for arrays or Vecs. You have to write some abomination like this:

    let foo = [MyStruct{a: "foo", b: 1, c: "amp"}, MyStruct{a: "bar", b: 1, c: "fff"}, MyStruct{a: "amp", b: 1, c: "aaa"} ];
Sure, it's more explicit. But even if I add a type annotation to 'foo' specifying the array type, I still have to repeat MyStruct for every member.


If you want compact, use an array of tuples:

  const foos = [("foo", 1, "bar"), ("foo", 1, "bar"), ...];
  for (str1, num, str2) in &foos {
      // ...
  }
For a proper struct you have to name the fields, because otherwise refactoring the fields could cause struct instances to silently get out of sync with the definition.


>otherwise refactoring the fields could cause struct instances to silently get out of sync with the definition.

Having to name the fields is only part of the pain. You also have to redundantly repeat the struct name. I don't see any fundamental reason why something like this shouldn't be valid:

    let foo: [MyStruct; _] = [{a: "foo", b: 1, c: "amp"}, {a: "bar", b: 1, c: "fff"}, {a: "amp", b: 1, c: "aaa"} ];
I can see the logic for insisting on field names. However, the builtin 'go vet' tool has a nice behavior where it will flag the use of unkeyed literals for public structs only. This strikes me as a good compromise between concision and safety.


You can use map() to turn an array of tuples into an array of structures. Unfortunately at time of writing the optimiser doesn't do a great job on this, so if you're making an array of several thousand of something, or an array of things which are themselves very large, this might have unacceptable performance, but in cases where I have say a modest N values and I want N structures based on those values...

  fn make_struct(x: (&str, u8, &str)) -> MyStruct {
     MyStruct { a: x.0, b: x.1, c: x.2 }
  }

  let foo = [("foo", 1, "amp"), ("bar", 1, "fff"), ("amp", 1, "aaa")]
    .map(make_struct);
[I have not actually compiled this code, but it should do roughly what you meant]


Dropping field names from definitions like Go would make the syntax inconsistent with destructuring and pattern matching. It's always possible to define a constructor function that takes unnamed values if you care about code verbosity in test cases. Or use `type M = MyStruct` to have an abbreviated type name within the test function.

Rust is more on the verbose/explicit side and I agree that can sometimes be annoying, but as autocompletion exists I can live with it.


>Dropping field names from definitions like Go would make the syntax inconsistent with destructuring and pattern matching

There's no reason you couldn't match on struct fields positionally as well. It would actually be quite convenient for cases like struct Point { x: f64, y: f64 }.


It might be convenient at first but could become a frustrating bug if you change the struct fields and also less readable in some cases. It has its upsides but I personally think making the syntax more complex for this one detail isn't worth it.

Side note: matching f64 by value isn't a good idea either way, is deprecated and will become an error in the future.


>matching f64 by value

I wasn't thinking about matching floats by value, just destructuring a Point to obtain the coordinates as separate variables. Come to think of it though, as Rust already allows

    let Point{x, y} = pl
there would be little benefit to allowing positional matching (and in fact it would create a nasty ambiguity). So yeah, as you said, you'd have to reserve the positional syntax for initialisation.

My overall point here isn't that Rust has made bad design decisions. It's just that the end result of a bunch of sensible design decisions is that initialising arrays of structs is clunky. It might well be that this problem isn't fixable without creating other, worse, problems. However, I think it's a problem that's emblematic of what the OP was talking about. We have here a language that's so concerned with solving difficult problems in clever ways that it's ended up backed into a corner when it comes to something as ridiculously simple as initialising an array of structs.


> It's just that the end result of a bunch of sensible design decisions is that initialising arrays of structs is clunky.

Yes, though at least the language gives you tools to make it more convenient. In the previous examples that would be constructors, and macros can help too (`println`, `vec` etc. exist for that reason).

I myself like to create tiny macros to shorten things like `UnicodeSegmentation::graphemes(s, true).count()` when I have to properly handle unicode. Sure I would prefer to have it be shorter from the start, though that wouldn't make it as obvious how expensive the operation is compared to just getting the size in bytes.


you'll have to forgive me, but isn't this why there should be a `::new()` callable attached to the struct?

I'm really new at Rust, but that was my takeaway, e.g. `String` has `String::new()`


The convention of providing a new() function isn't relevant here, that name isn't magic, the Rust compiler doesn't care whether it exists, and it won't cause Rust to do anything special with tuples (or any other data structure) that happen to have a similar shape.

String::new() just makes you an empty String, which, since an empty String doesn't own any storage and doesn't contain anything, is very cheap (and indeed constant evaluable), likewise Vec::new() makes an empty Vec.

What your parent commenter wants is for Rust to make the appropriate MyStruct, but without them needing to say MyStruct each time, which would have worked in e.g. C or Go.


Do you mean the vec! macro?


It doesn't let you drop redundant struct tags, as far as I can tell.


I think you're right, although mentioning a macro does suggest another option here, you could write a macro which transforms [{foo1, "bar1", baz1}, {foo2, "bar2", baz2}] into the named structure version.

Since it's a macro there's no impact on runtime performance, but on the other hand it's more work to debug it. Learning declarative macros in Rust is much nicer and safer than learning C macros, but nowhere near as powerful as Rust's horribly unsafe proc macros.


The core language is fine, but when you actually start building things you need to introduce lifetimes, all the traits that you polluted your interface with and then add on async, it gets out-of-hand quickly.


The top comment gave a perfect example: #![feature(strict_provenance)]

These feature enablement blocks drive me crazy. You could go from codebase to codebase and it's almost like you are working in a different language depending on how many of these are enabled or not. I've been trying rust on and off since it's release, and I still have yet to feel like I have a grasp on some "core" subset of the language I can fall back on to solve most of my problems. I always have to scour documentation for the hot new thing to turn on or do, and this isn't to scorn innovation and change, but it does get exhausting at some point.

(Please release a spec)


A spec is not a replacement for #![feature] attributes. It's rather the opposite: #![feature] indicates that you are stepping out of the stable, specified core language and into an area that is still a work in progress and thus cannot have a committed specification yet. You shouldn't need it at all unless you are actively experimenting with some unfinished proposal.


Those feature blocks exist only for nightly/unstable Rust. You don't have those on stable Rust. The most you might have are derive blocks but those just automate what you'd write by hand anyway.


I think it is just inherent complexity in the low-level space.

(Though I do agree that Rust definitely moves fast, which is sort of understandable as a relatively newish language)


There are a number of sources of noise in rust, but the one I find most annoying (because it's also so common) is the double colon. My current theory is that it's because the colon is the same height as lowercase letters.

If you end up with a long::run::of::module::names, I find it all just blurs into one.


Okay but who is writing code like this instead of using `use`?

Also this is an no-win situation. C++, Ruby, Perl, and others have used `::` as module-scoping syntax for decades. If Rust does something novel, it's penalized for being unfamiliar. If Rust uses syntax for which there's ample prior art, it's apparently line noise. If rust used a `.` as a separator, it's unclear if you're descending into modules or calling a function chain.

There's literally no way to win.


If an import is used rarely, then i prefer qualifying paths instead of importing them.

This is especially true when there's similarly named items from different paths. Best example is `Result`/`Error` types. eg: std::io::Result vs normal default Result vs other crate's Result type. Another example would be math types like Vec2 between game engine and egui. String in mlua vs std rust etc..

Usually you can get around this by importing it with a different name like `use mlua::String as LuaString`. but it is still something that you need to actively do.


I did use the example of long run of module names, and I do take your point about `use` (though I have to sometimes read the very top of a file too!)...but the same applies to a lesser extent to the "last mile" module (e.g. `String::from`, `Vec::new`) which will be seen throughout any rust code.

> If rust used a `.` as a separator, it's unclear if you're descending into modules or calling a function chain

Interestingly, from the zig documentation:

    Zig source files are implicitly structs, with a name equal to the file's basename with the extension truncated. @import returns the struct type corresponding to the file.
This means that there isn't really any difference between accessing a struct field and accessing something in module...the module is also a kind of struct (and rust, as in zig, differentiating struct field access vs function invocation is possible because of the parens in the latter).


> There's literally no way to win.

Yes there is? Follow what C#, F#, Java, TypeScript, etc. do


That is a very interesting observation. I am starting to have a similar distaste for the visual noise caused by the ':' in my fully type-annotated Python code. This is particularly noticeable after spending a few weeks writing Golang, then going back to my Python code. The Go code feels far cleaner syntactically and visually.


I wonder if this could be ameliorated through font choice and highlighting.


can you please point out a few more annoyances in rust syntax? I am a newbie playing with language design and could use some perspective :)


To be honest that's the major one for me. Someone else has mentioned lifetime annotations, but those uncommon enough that I don't find it much of an issue.

If I was to think about it: comparing rust to zig, zig benefits a lot from error and optional types having their own syntax rather than being treated like any other type. For example a return type of:

    Result<Option<usize>, Error>>
in rust is rendered (mostly) equivalently in zig as

    !?usize
Note: there are some options in rust for cleaning up errors such as the anyhow library.

Disclaimer: I'm a zig fan and contribute monetarily so please weight anything I say on zig vs rust with that in mind. (I do write rust at work though)


i'm curious, how do you specify the error type in the zig version?


So usually you don't have to specify the error type. The Zig compiler works out what errors are returned from the function by looking at any errors returned directly or errors returned from other functions called within the body of the function. That set of errors forms an enum that is the actual error type, you just don't have to write that out explicitly. An example might be:

    fn someFunction(a: usize) !usize {
        if (a < 10) return error.LessThanTen;
    
        const b = try anotherFunction(a);
    
        return 2 * b;
    }
    
    fn anotherFunction(x: usize) !usize {
        if (x < 20) return error.LessThanTwenty;
    
        return x * 3;
    }
The compiler infers the error type of `someFunction` as:

    error {
        LessThanTen,
        LessThanTwenty
    }
When you then `switch` on an error type, the compiler will exhaustively check that you have handled all the cases.

Note the "(mostly) equivalently" was a reference to the fact that Zig errors can't (currently) contain any other information, whereas an error in rust can carry other information.

Also note that the compiler can't infer the error type in all case, for example in the case of a recursive function. In that case you do need to explicitly write out the error set.


Ah interesting approach, thx for the answer.

1. Doesn't that risk introducing accidental breaking changes by adding a new error to the set in the implementation, since the set of errors is inferred from the implementation? Having a compile error in this case in Rust is often the last barrier standing between me and an accidental major semver bump (since callers have to exhaustively match on the error conditions) 2. Can you have data in the variants of the error enumeration?


> Doesn't that risk introducing accidental breaking changes by adding a new error to the set in the implementation, since the set of errors is inferred from the implementation?

Do you have an illustrative example? (I'm not implying it's not possible, just trying to think of a good example so I can give a good answer)

> since callers have to exhaustively match on the error conditions

If it's exhaustiveness that you're referring to, zig will make you handle all the possible errors (you can still do a catch all type thing when handling errors, which has the potential to "hide" an error that you otherwise wanted to handle explicitly).

> Can you have data in the variants of the error enumeration

No, they compile to just integers. Essentially it compiles to the same as C function that returns an `int` representing the error (with your actual return type passed in as a pointer, say).

Another limitation of zig error values is I think they're globally scoped, so you potentially could have two libraries have clashing error names that you then can't differentiate (I don't know if there are any plans to try and resolve that).

I will say that this automatic error set inference gives writing zig code this lovely "flow", where I do some error checks at the top of the function and early return some errors (which I just invent the names of there and then) and then move onto the happy path of the function, happy in the knowledge that the error handling is already "correct" (in that if the error isn't handled it'll (typically) bubble all the way up to main and exit the program). Any refinement on how a specific error is handled, I can go back to an appropriate place in the call stack and handle it. I always feel like it's helping me write correct code.


> Do you have an illustrative example?

Illustrative, I don't know, but I'll try to give more context.

When writing a library, it is important that public items (like functions and enum) don't change between minor versions so that client code doesn't need to update their calls to the library.

Sometimes when refactoring code you end up modifying how a library function is implemented. Maybe it will now depend on some file being present on the system, while previously it wouldn't, meaning that the absence of that file adds a new error variant to this function.

In today's Rust, since the Error type of a Result is an explicit part of a function's signature, such a change is very noisy to the library's maintainer: it entails either modifying the signature of the public function to return a different error type, or modifying the Error type itself, which is also public.

When this happens, the change needs to be reconsidered: either you can defer it to later, provide an additional function with that new implementation and error variant, try to make it work with the error types you already have, or decide in that it actually warrants a major version bump, in conscience.

By contrast, if the set of errors of a function is inferred rather than part of its explicit signature, it means that modifying the implementation you can add a new variant without even realising it (for instance, by mixing the variant name with a variant returned by a sibling function that you thought was already used by this function) and break semver in a much more silent way.

I guess it also makes life harder for tooling, since it has to parse the implementation of a function (and all its subfunctions) to rebuild the set of errors, as opposed to simply parse the signature of the top-level function.

> Essentially it compiles to the same as C function that returns an `int` representing the error

That feels very limiting, I often use error types to e.g., attach data about the error. Is there a more general mechanism for sum types for when this shorthand doesn't apply?


I see you what you mean. Yeah, I suppose if you are writing a library you might want to be more deliberate in the error set. Maybe explicitly writing out the error set is what you want in that situation. Rewriting the example:

    fn someFunction(a: usize) MyError!usize {
        if (a < 10) return error.LessThanTen;
    
        const b = try anotherFunction(a);
    
        return 2 * b;
    }
    
    fn anotherFunction(x: usize) MyError!usize {
        if (x < 20) return error.LessThanTwenty;
    
        return x * 3;
    }

    const MyError = error {
        LessThanTen,
        LessThanTwenty
    }
> That feels very limiting, I often use error types to e.g., attach data about the error. Is there a more general mechanism for sum types for when this shorthand doesn't apply?

I agree it's limiting. It obviously is going to depend on your application, but I have largely done without annotating errors with extra information (that maybe speaks more to the seriousness of my zig projects than to that approach to error handling as being sufficient!).

One pattern I have used (in e.g. a parser) is additionally passing in a pointer to a sum type:

    fn parse(allocator: *mem.Allocator, tokens: Tokens, parse_error: *ParseError) !AST
The `parse_error` can be set if an error condition occurs. I concede that that's a little clumsy


> In today's Rust, since the Error type of a Result is an explicit part of a function's signature, such a change is very noisy to the library's maintainer: it entails either modifying the signature of the public function to return a different error type, or modifying the Error type itself, which is also public.

For what it's worth, if you expect this might happen, you should give the enum the [[non_exhaustive]] attribute. This attribute says I, the implementer, know how many of these there are, and in my code I can exhaustively enumerate them e.g. in a pattern match, however, you the 3rd party programmer using this crate, must assume you can't know how many there are, and therefore must write a default match to handle others, even if there seem to be no others when you write it.

Once you do this, you don't cause a compatibility break by adding a new item.


ErrorType!usize


My biggest peeve is types on the right. I would say that C is "humanistic" in its type declarations, while Rust and the rest of Pascal's lineage are "mechanistic". Concretely, look at this:

    int foo(int a, float b);
vs

    fn foo(a: i32, b: f32) -> i32;
In Rust's case you're specifying to the machine what the thing is, i.e. "I am declaring a function called foo. The function has a first parameter called a, of type i32, ... The function returns an int". Whereas in C you have a "declaration follows usage" idiom, such that what you're saying is "typing foo(a, b) produces an int on the left side, within foo a produces an int lvalue, ...". The function arguments flow from right to left and the result emerges on the left with the given type. The order of tokens in declaring foo matches the order of tokens when calling foo. It also matches the order of importance - first is the return type, then the function name, then each parameter type followed (optionally) by the parameter name.

Or look at an array declaration:

    int arr[5];
"You get an int when you type arr[N] where N is less than 5".

This kind of argument of course goes all the way back to AT&T vs Intel assembly syntax and I am firmly on Intel's side.


This isn’t a Rust thing. Lot’s of new language have the type declaration after the variable because of type inference which makes such declarations optional.


Algol 60, Algol W, and Algol 68 were influential in the historical development of subsequent programming languages. C gets its type on the left order from the Algol family.

Pascal, Modula, and Ada are all type on the right languages and were also very influential in the historical development of programming languages. I happen to prefer types on the right for complex declarations. Note that Pascal and Modula were designed by Niklaus Wirth after he created Algol W; it seems he preferred types on the right.


I'll simply say that you are the first person I've seen come to the defense of C's inside-out variable syntax.


No arrow operator, no default args, no named parameters, no structure defaults and instead the incredibly verbose ..Default::default() + a trait impl as a substitute, no variadics, overall weak generics compared to C++ and a huge reliance on macros, etc etc. Rust programmers address this by calling everything rust does poorly an antipattern, ie. the “why would you do that” card.


The arrow operator is just a terrible idea, and it's weird that people defend it. It makes sense that C did this, it was a long time ago and compilers weren't very smart so C needs to make up for that, in C++ it's just carried over from C.

The absence of default args is a deliberate choice, notice that Rust does have default type arguments in polymorphism, the absence of defaults for function parameters -- which would be technically easy to implement -- reflects a belief which I've come to agree with that overloading is a bad idea, and defaults most often in practice mean you're overloading.

For example, C++ std::ranges::binary_search uses defaults to present what are in effect at least two distinct features, as a single function, suiting C++ sensibilities, whereas Rust reflect almost the same capabilities as three functions []::binary_search []::binary_search_by and []::binary_search_by_key

For a very simple binary search, things seem pretty similar. In Rust we have a single parameter, for our searched-for element, and in C++ we can stop after that parameter, leaving the comparison function and projection as default for similar effect.

However for binary_search_by the C++ is contorted by this API shape. Instead of a callable to decide whether our search found what it was looking for, and if not where it is relative to the searched-for element, the C++ is obliged to carry that element (because it was an earlier parameter) even if it's unused - and then a comparison function which takes the element, and only then optionally a projection which you may or may not use.

And for binary_search_by_key the C++ is even more awkward, we have a good reason to use a value here, but we're obliged to specify the comparison function even though we only want to write a callable to make suitable keys (ie a projection), because of the order of the parameters.

These would be better served, as in Rust, by three distinct functions, with only the appropriate parameters for each function - even if you choose to actually implement the simplest in terms of the others, because of the documentation and the API shape afforded if you think about it as three things not one with defaults carefully tailored to allow all three uses.

Variadics is a genuinely useful feature, but to do it properly is very difficult, C++ 98 doesn't have anything better than Rust [C compatibility, with no real type checking], C++ 11 does have the outline of what you'd actually want, and C++ 17 has much closer to what I'd want to see in Rust some day.

Much of what you're thinking of in "weak generics" is probably deliberate constraints to only allow coherent things, in C++ they don't care if you want to make a Foo<NaN> even though that's nonsense, IFNDR gives them the ultimate out, your program has no defined meaning, so too bad.

There are some obvious things Rust wants to have but doesn't yet in this space, including broader const Generics (e.g. my OnewayEqual ought to be Oneway<Ordering::Equal>) but it's not going to pursue the irrational C++ exuberance because it's so quickly unsound. C++ doesn't care about that while Rust does.


Why do you think the arrow operator is a terrible idea? I feel that it costs very little of my time, and that it's so much easier to intuit what's going on from an expression like Foo->Bar.Baz->Quux compared to having only dots on there.


The arrow operator is far better than what you currently have to deal with in unsafe rust when trying to access a pointer member of a pointer to a struct. Rust’s way is more verbose and less clear.

I don’t really care for your examples of defaults being abused in C++, because all of rust’s workarounds like builder pattern and the default trait have the same potential to be misused while also impeding performance. They also suck for ergonomics, see bevy and polars. Rust already has a huge function colouring problem with async and mut and the lack of defaults only makes it worse.

Similarly for generics. Templates are simply better. Don’t use the power if you’re scared if it, that’s the great thing about freedom, you won’t be forced to. C++ can always do something the Rust way, nullifying everything you’ve said, but the other way around is not true. Rust sacrifices the complex case to make the simple case a little simpler and just leans on macros to do everything else. “Rust just allows coherent things” is a typical example of the bullshit rust programmers spew when they don’t have an actual response but want to say something anyways because it’s completely wrong. Plenty of useful template functionality is impossible to replicate in rust. For example:

https://youtu.be/gOdcNko2xc8


C compilers weren't smart.


Agreed, using Rust is like switching a monster (C++) for another even worse. And notice that Rust is not even a mature language, it will get much worse. I will stay with C, thank you.


Can you write a performant, generic vector data structure in C? Until then it is useless in my book.


So useless that Linux, gcc, and all UNIX system commands is written in C. But you're probably doing much more important things, right?


We should definitely appreciate the craftsmanship. This comment says everything, regardless of how trendy Rust might be, let’s respect the craft, your favourite language doesn’t make you more important. Legends!


Rust is not my favorite language and I didn’t mean to speak anything negative about projects written in C, but I do think that those that become great did so in spite of C, not because of it.

No access to performant generic data structures is a major reason why truly performance oriented programs rather choose C++ over C.


'constexpr' and 'auto' in modern C++ can eliminate a large portion of the ugliness. In some cases it can be much more ergonomic than the equivalent Rust.


Indeed, one gets to write macro like code on the same language.


You can write Rust macros in Rust too, if you wish, but it's a bit more involved (https://doc.rust-lang.org/reference/procedural-macros.html). I realise this isn't the same as constexpr, but I'd argue it's nicer.

Rust also has const generics (but they are still a bit shabby in places last time I used them).

I for one never liked the constexpr semantic and syntax. Always felt like a... Little pebble in my shoe. Didn't quite annoy me enough not to use it since it was useful, but it was never a comfortable experience.


At least C++ is enormous and you can find a sane subset that you enjoy using. And C++ can be pretty safe with smart pointers.


This almost works until you have to integrate with other people's C++ code and find out they picked a different subset to you.


Unfortunately there is always that clever one than can't let go of writing C style code, regardless of what kind of pointers are being used.

It is one of my favourite languages, but I also acknowledge that C compatibility is a big pain point for security.


IMO Zig isn't exactly a beauty either.


That's entirely subjective. I personally find Zig, Nim and even Go much uglier than Rust.


One technical criticism I have of the "community" is that a non-trivial portion of the community thinks that if the borrow checker forbids a program then the program is a bad program. None of the core contributors do this and many prominent library authors are upfront that this is not true, but I've read lots of comments and posts in Rust rooms about how the borrow checker should be the way to write and architect correct programs.

The Rust borrow checker imposes a style that is safe, but is not the only safe way to write that program.


> It makes the easy stuff hard.

> Writing basic data structures isn't a niche, esoteric edge case.

Writing basic data structures isn't "easy stuff" in any low level language. It isn't in Zig by any means, nor in C nor in C++


There is already C for when one wants to do otherwise.


> Unsafe Rust is hard. A lot harder than C, this is because unsafe Rust has a lot of nuanced rules about undefined behaviour (UB) — thanks to the borrow checker — that make it easy to perniciously break things and introduce bugs.

I don't think this is correct: Rust makes writing unsafe Rust correctly more onerous than writing C, but the actual rules for undefined behavior are the virtually same as in C: if you alias where you must not, or mutate where you must not, etc. you're in exactly the same boat.

In other words: Rust makes it hard to write unsafe Rust correctly, but no harder than writing well-defined C. The only difference is that Rust raises the safety expectations by default, making unsafe Rust look more difficult than C.


I don't agree that the rules for UB are virtually the same as in C. One example: if your unsafe Rust code modifies any memory address for which there exists a reference elsewhere, that is instantly UB. In C, that is not necessarily the case. https://www.youtube.com/watch?v=DG-VLezRkYQ has some good details on this.

Similarly, in Rust you have to be careful to never instantiate a value that is out-of-range for a given type (e.g. a bool with value > 1), even if you will never read or access that value before it is changed to something valid. In C this same concern does not exist since it is not insta-UB in the same way.


This is not correct. Here's a really good video that goes into the differences: https://youtu.be/DG-VLezRkYQ


The rust undefined behaviour rules are stricter than C: mutating a non mutable reference is UB, for example. Non mutable references don't exist in C.


> Non mutable references don't exist in C.

Sure they do: C has a well-defined notion of const-correctness. If you mutate through a `const`, you're invoking undefined behavior.

Both C and C++ allow you to strip `const` from a const-qualified value or reference, but only under the condition that you don't actually modify that value.

Edit: which, in case it isn't clear, means that Rust's UB is exactly the same as C's in this case.


Actually in C++ you are allowed to strip const and modify the value as long as the original object (not necessarily object in an OO sense) isn't const [1].

[1] https://en.cppreference.com/w/cpp/language/const_cast


Yes: the implication was that the original object was `const`. If you both add and remove const, that's well-defined.

(I've yet to see a C or C++ codebase where object provenance actually guarantees this; I've see a lot of C and C++ codebases with const-stripping induced UB.)


In rust, stripping const is UB - even if the original location is mut.


C and Rust don't have more strict rules than each other; each has some things that the other disallows, and doesn't allow some things the other allows. It's not that simple.


Rust has raw pointers and UnsafeCell, but they're quite unidiomatic compared to the C/C++ equivalent. A lot of Rust library code only takes safe references so it's hard to use from an unsafe context.


I would think that modifying a pointer to a const object, e.g.

    const int x = 123;
    const int *px = &x;
    (*(int*)px) = 456;
is very, very UB in C (and most likely will crash on most platforms)


    int x = 123;
    const int *px = &x;
    (*(int*)px) = 456;
is legal in C. The Rust equivalent using & and &mut is UB. Writing this in Rust using raw pointers requires unsafe blocks everywhere, loses method syntax, has no -> operator, etc.


> the actual rules for undefined behavior are the virtually same as in C

1. Creating a mutable reference when there's other references to the same memory around, even if you don't use/deref that mutable reference, is considered UB in Rust; References there have the `dereferenceable` LLVM attribute, so the compiler is allowed to insert use/derefs at will to facilitate optimizations [0]. C's pointers are more like Rust's raw pointers: they only have to be valid upon use not at creation.

2. References in Rust are transient (as noted in the blogpost) so holding a mut ref to T means you also hold a mut ref to all its fields/subfields semantically. If you're doing intrusive or self-referential data structures, it often requires having UnsafeCell fields to soundly create isolated mut refs from top-level shared refs. Problem being that core, language-level traits in Rust like Iterator and Future (generated by async blocks) take mut refs so implementing them (which is practically useful) on types with intrusive fields potentially being used elsewhere is UB [1]. This doesn't exist in C with no `dereferenceable` & opt-in `restrict`. It's still an unresolved issue in Rust though [2] where they had to disable LLVM annotations on problematic types/traits to avoid miscompilations [3]. Some of these footguns can be avoided by not using references and the core language traits (like the blogpost did), but they found that to not be a great programming experience.

3. Because of `dereferenceable` (again) instances of a type must be valid in-memory representations at all times, even when unused [4]. If you want invalid/uninit representations, you wrap the type in `MaybeUninit` which is fairly unergonomic. C doesn't have this issue as its only UB to deref invalid pointers or branch on invalid values (same case in Rust), not have invalid values at all.

[0]: https://github.com/rust-lang/rust/issues/94133

[1]: https://gist.github.com/Darksonn/1567538f56af1a8038ecc3c664a...

[2]: https://github.com/rust-lang/rust/issues/63818

[3]: https://github.com/rust-lang/rust/pull/106180

[4]: https://doc.rust-lang.org/std/primitive.reference.html


If you're writing much unsafe code in Rust, you're doing it wrong.

OK, for a garbage collector, maybe you have to, because you're taking over memory management yourself. But very, very rarely do you need to do that. And when you do, you need very thorough testing, test tools, and documentation.

I just got done chasing someone else's pointer bugs with valgrind and gdb, in C code from a public crate three levels down from my code. Valgrind was useful in locating the area of trouble. The code there had too much unnecessary pointer manipulation, and offsets obtained from input which might be un-initialized memory. This never happens in safe Rust. Which is the whole point.

Most things for which C programmers use pointer arithmetic can be expressed as slices. Slices are pointer arithmetic, but with size information and sound rules.

(I'm a bit cranky this week. I've spent the last few weeks finding bugs in Rust crates that ought to Just Work.)


Note, this is specifically talking about unsafe Rust versus Zig. Personally unsafe does have some rough edges, I'm looking forward to seeing how the Rust team manages to make it better in the future.


UB in unsafe rust sometimes "leaks" outside the unsafe scope and cause crashing elsewhere.

If rust can pair with a proof checker and let user write some correctness proof, it can be way more useful than the current borrow checker.


Isn't there already a project for that?

Though I think at that point you might as well formally verify zig as a means to get memory safety, resource provenance, and data race safety.

The idea that memory safety must be baked into the type system is belief by the existence of provenance tracking macro/crate in rust since that exists specifically to track memory in unsafe; rust is basically 'discovering' a need to inventing what zig will need to invent for memory safety.


Correctness proof? That’s like almost impossible, formal verification is just not scalable, insanely complex and it will never be done by your average developer.

The only way we can do proofs (on certain things) is restricting code, like non unsafe rust.

If it would be feasible, why wouldn’t we just continue to use C and be happy with our verified C codes?


The correctness proof idea sounds intriguing. Ada does this already yes?

And do you have any examples of where UB from unsafe rust leaks to outside the program? Surely you would look back at the unsafe code always to fix it.


There is a huge body of work around proof-based programming.

If you're looking for a full blown SMT solver and general purpose dependently-typed PL have a look at F*

https://fstar-lang.org


Are you talking about Miri, or something even stronger?


Miri doesn't do proofs. It only checks the test cases that you run under it.


Interesting, I wonder if there are any good proof verifiers for Rust.


I think most of the difficulty people experience is when they try to naïvely use references anywhere they would normally use a pointer. That mostly works for functions, but this ends up getting really confusing and difficult for data objects. Instead, people should really be using things like Rc<T> which makes certain patterns much simpler. People seem to have this ridiculous notion that using Rc<T> or heaven forbid Rc<Box<T>> is going to make their code slow, but in reality it can greatly simplify code at minimal performance cost when used places that references would get complex. People generally don't say Swift is slow, but it uses reference counting all over the place.


People do say that Swift is slow though, and it has a whole bunch of optimisations to ensure the RC is elided whenever possible.


There are two differences from Swift:

1. You don't need to refcount everything. In Swift objects have to be refcounted and heap allocated. In Rust you only use it selectively in cases where shared ownership is really necessary, and otherwise still have an option of using exclusive ownership, value types, etc.

2. You can still borrow Rc's contents locally and use it as a plain reference, so hot loops and leaf functions don't pay the price. Often you may need to touch the refcount only once when building a data structure or passing data to a closure, and then on use it via a temporary reference. In Swift the refcount is updated much more often, and there are only limited cases where ARC optimizer can skip it.


Rc<Box<T>> really doesn't make sense (Rc is like a specialized Box), I guess you meant Rc<RefCell<T>>?


Using custom allocators to taint memory for security checks is no better than what C and C++ toolchains have been providing for decades.

I was already using debug allocators in Visual C++ 5.0, with a memory report at the program exit.

For the latest documentation,

https://learn.microsoft.com/en-us/cpp/c-runtime-library/crt-...


Wait, the benchmark to find the 35th fibonacci number took 1.077s for the Zig VM vs 1.657s for the Rust VM?

I realise that these VMs are going to be totally idiomatic Zig/Rust, with the most straightforward implementation possible, and little-to-no performance tuning, but even so - that's gotta be a typo, right? Or it's actually finding `fib(350)`? Or each "run" is actually finding `fib(35)` 100 (1000?) times?


I think the VM is operative here. My native Rust implementation found fib(35) in 51ms, but my Python implementation took about 1500ms (similar to their measurements).

(I eyeballed the assembly to make sure the native implementation was recursive, but that's the extent of my rigor - consider these napkin numbers.)


Yeah, it was using the naive recursive implementation instead of a tail-call recursive variant that I wasn't expecting.

I should have been expecting it. You're testing the performance of the runtime, not the program, so of course you want a bad implementation of the algorithm to stress the runtime as much as possible.

(My tail-call Python implementation tops out at fib(997) in 720ms. Calling fib(998) gives me a "RecursionError: maximum recursion depth exceeded". AIUI python is never going to optimise away tail calls, but because the tail-call variant is O(n) anyway it still works out orders of magnitude faster.)


No caching, every recursive call runs the entire chain from scratch. fib(35) without caching takes roughly 14 million calls to resolve.


It's intentionally using the naive 2^N solution to stress test lots of tiny function calls.


There is no way it will take a whole second regardless. Even when compiled in debug mode, it takes about 100 ms.

Try it here, I had Bing AI write it out - https://play.rust-lang.org/?version=stable&mode=release&edit...


OP isn't talking about the performance of Rust code to calculate fib(35), but rather the performance of a Rust implementation vs a Zig implementation of an interpreted language executing code to calculate fib(35). They are saying that the Rust VM they wrote is slower than the Zig VM they wrote.


And, in particular, an interpreted dynamically typed language.


From a book that was a real joy to read and take one’s first steps into the magical world of compilers —- thank you for writing it!


You're welcome! I'm glad you liked it.


The way I would frame this is that Rust has static (compile-time) memory management, and that conflicts with dynamic memory management (garbage collection).

The boundary is awkward and creates complexity.

I wrote a post about problems writing a garbage collector in C++, e.g. annotating the root set, and having precise metadata for tracing.

http://www.oilshell.org/blog/2023/01/garbage-collector.html

https://news.ycombinator.com/item?id=34350260

I linked to this 2016 post about Rust, which makes me think the problem could be worse in Rust, although I haven't tried it:

http://blog.pnkfx.org/blog/2016/01/01/gc-and-rust-part-2-roo...

I didn't write as much about bindings to native C++ code, but that's also an issue that you have to think about carefully. CPython has kind of been "stuck" with their API for decades, which exposes reference counting. So it's extraordinarily difficult to move to tracing GC, let alone moving GC, etc.

---

On the other hand, there was also a paper that said Rust can be good for writing GCs.

Rust as a Language for High Performance GC Implementation

https://dl.acm.org/doi/pdf/10.1145/2926697.2926707

However, I'm not sure it addresses the interface issue. One lesson I learned is that GCs are NOT modular pieces of code -- they have "tentacles" that touch the entire program!

That said, C++ is pretty good at "typed memory" as well, and I think it's more pleasant than C. That is, you get more than void* and macros. So I can believe that Rust has benefits for writing GC.

Not sure about Zig -- I can believe it's a nice middle ground.


Dynamic memory management is an entirely different axes from manual/automatic, though.

(Safe) Rust indeed limits the user to a compile-time deallocable subset of what’s expressible (RC being an escape hatch), which depending on the problem domain may be too limiting.


Zig seems more simple than Rust.

I’m willing to forgoe certain rust features in exchange for simplicity.


The link is down for me. Perhaps HN hug of death? Below is link to the site on the Wayback Machine.

https://web.archive.org/web/20230307172822/https://zackoverf...


Try ping "zackoverflow.dev" and "cname.vercel-dns.com". If only the former times out, it's possible your local ISP is blocking Vercel's IP. If that's the case, you could contact Vercel support to help assist.


Maybe you don't have *.dev lookups working properly?


I don't have a problem opening other *.dev sites. I should note that I couldn't open the link on iOS Safari over LTE.


Can confirm that I can pull the site here.


Check your hosts file, you might be mapping *.dev to localhost


I did not know host file takes domain name patterns


By default, macOS and Linux don’t, but dnsmasq does and it’s quite popular. Don’t know about Windows


This claim just killed me: "Apart from [Zig] not having crazy UB like in unsafe Rust". Zig has more UB than even C. Yes, Zig has safety checks you can turn on, but then it's not fast anymore. The claim saying Zig has no UB is like saying C has no UB because you can run it with UB-sanitizers.

Don't get me wrong, it's great to have this directly enabled in "safe" mode like Zig does it. But to use that to say Zig is more safe is extremely misleading.


You should read the rest of the article, where the appropriate context is given to that sentence.


Isn’t

    var ptr: [*]u8 = @ptrCast([*]u8, &slice[0]);
the same as

    var ptr = slice.ptr;
?

Or am I missing something


The `slice.ptr` version is allowed in D, but `&slice[0]` is preferred because that comes with a check that the slice has a non-zero length and the pointer will actually point to something valid. That's why the former is allowed in @system code, and the latter is used in @safe code.


yes it would be the same


> If I have a raw pointer to an array of data (*mut T), I can turn it into a slice &mut [T], and I get to use a for ... in loop on it or any of the handy iterators (.for_each(), .map(), etc.).

I wonder if *mut [u8] would be a workable alternative to &mut [u8]. I haven't looked into creating such pointers yet, but `fn f(a: *mut [u8]) {}` is legal while `fn f(a: *mut [u8]) { a.len(); }` doesn't compile on stable due to https://github.com/rust-lang/rust/issues/71146. Looks like raw slice pointers aren't fully baked yet.


I you look at Deno and Bun, TS/JS runtimes, one written in rust and the other in Zig.

Bun repo is filled with issues surrounding segfaults, but I guess it gave them the advantage to get up and running quickly.


How significant is this to embedded development, eg. automotive software? I've been learning Rust on the side, and one of the main applications I have in mind is to get back into some embedded programming. I saw that there were libraries and even whole books about this [1], but I'm curious what the actual experience is like, and how much you have to wrangle raw pointers there.

[1] https://docs.rust-embedded.org/book/


We do embedded work at Oxide. There's unsafe code, but not a ton of it. I haven't dug into this project's codebase at all, but the way it's described it sounds like there's way more than we use.


A hammer better at hammering nails than a screwdriver


I get that Zig is better than unsafe Rust. But what about the general use? Is Zig simplicity and speed of writing code worth the trade-off in safety?


I would say that different technologies serve a different purpose.

So, saying, that e.g., Zig is generally better or worse than Rust doesn't make any sense to me, as both Languages have a different purpose.

I would much rather use Rust to write a Webserver (in production) than in Zig, simply because Rust serves this purpose better than Zig does. And i personally can consider Zig to be my favourite Language.

I would also never (at least right now) write Website frontends in either Zig or Rust, i think JavaScript in this case, is the obvious choice, because it has been developed (over years) for this (and unfortunately other) use case(s).


>There are endless debates online about Rust vs. Zig

I have never read anything that suggest or argued Zig as better than Rust, or "Rust vs Zig". Not on HN, not on Reddit, not on Twitter. In fact this link / title is the first one. ( I do wish the title was "Unsafe Rust" to better reflect on the content. )

There are however plenty who still prefer Zig over Rust, even knowing when Rust is better.

I also want to note RESF generally does not consider "unsafe" Rust to be Rust.

Edit: LOL I knew this would be heavily downvoted.


[flagged]


My experience is that I see a LOT more complaints about these sorts of behaviors than I see the actual behavior. I think it's honestly just a meme that's gotten out of hand at this point. Speaking as someone who is not part of the Rust community and knows comparatively little about the language.


In my surface-level interactions with the community I have found it to be absolutely kind and helpful.


You must not see the C or C++ threads. :)

Virtually every C or C++ thread on HN for the last several years is littered with comments from Rust zealots demonizing and condemning other humans for being working in C or C++ codebases.


C or C++ threads on HN are often not about something great. Recend thread on SHA-3 vulnerabilities as example [0]. If security standards providers fail to write secure C code where does this put average Joe?

I mean the whole 'pride' threads of people who 'enjoy writing C' or C++ look as strange on HN as Rust brigading. Its funny how there is no C or C++ pride thread on a day when vulnerabilities hit frontpage.

PS. I have never wrote a line of Rust in my life. 20+ years of C++ work.

[0] https://news.ycombinator.com/item?id=35050307


You're arguing straw men rather than engaging in the points being made.

Virtually every thread means both positive, neutral, and negative stories.

Something about Rust makes people feel justified bullying people supporting their families laboring in C or C++ codebases. No other language community has this.

The other fallacy is equating laboring with pridefulness. I never said anything about pride. Most C and C++ programmers are very aware of the problems in their ecosystem and they are starting to solve the problems. The pace of those improvements might not be to your liking, but that doesn't mean it's even plausible to convince my boss to rewrite software in Rust or introduce Rust incrementally.

We are four levels deeper into this discussion and it seems the points in my original comment are only strengthened, as evidence in the replies.


Yes, you never said anything about pride and I am not really arguing with points you have made.

What I wanted to say is there are many threads on HN where people who 'aware of problems' do not write and people who are 'having fun writing' code gather in hundreds. These threads are equally bad in my opinion as ones you call 'bullying'. I wish HN would discuss improvements in both C and C++ at the same time with improvements in Rust ecosystem without mixing it up but I understand why it gets mixed.


> Worse, even when software is written in Rust, if it's not to some mythical standard, the community will unleash a hurricane of hatred (see: Actix fallout).

Using unsafe correctly isn't a mythical standard, that's the bare minimum for safety.


[flagged]


We've already asked you to stop taking HN threads into programming language flamewar. We don't want that here—it's tedious, and leads to repetitive discussions and then nasty ones.

No more of this on HN, please.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: