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.
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 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.
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.
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.
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.
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?
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'.
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
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.
* 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.
The Rust borrow checker imposes a style that is safe, but is not the only safe way to write that program.
> 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++
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.
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.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 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.
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.
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?
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.
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.
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.
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)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
https://web.archive.org/web/20230307172822/https://zackoverf...
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.
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.
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?
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.
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 eyeballed the assembly to make sure the native implementation was recursive, but that's the extent of my rigor - consider these napkin numbers.)
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.)
Try it here, I had Bing AI write it out - https://play.rust-lang.org/?version=stable&mode=release&edit...
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.
var ptr: [*]u8 = @ptrCast([*]u8, &slice[0]);
the same as var ptr = slice.ptr;
?Or am I missing something
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.
(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.
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.
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).
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.)
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-...
I’m willing to forgoe certain rust features in exchange for simplicity.
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.
Bun repo is filled with issues surrounding segfaults, but I guess it gave them the advantage to get up and running quickly.
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.