You can just do that, and then Zig is really no less robust than Rust. But if you want to do "managed language" style allocation patterns (like what llms generally prefer), it doesn't make sense to use it.
Grouped allocations works especially well in parsers & ASTs where the lifetime is very bounded. Since the Rust rewrite, we still use arenas for Bun’s parsers and the bundler but not a ton elsewhere.
Not if you use segmented arrays
If you just don't write bugs, then yes all languages are equally robust, including assembly.
Zig, like C, is simply not a robust language. I don't know why this feels like something contentious? It's clearly not intended to be robust?
I don’t think it makes sense to say Zig is or isn’t intended to be robust in general. Like, we don’t say Rust isn’t robust since it doesn’t add dependent types and general purpose static verification that can do more general proofs. It’s focused on eliminating one class of memory bugs in particular, exactly the class of bugs that are the biggest challenge for software like Bun, and other software with complex lifetimes (it originated from Mozilla and Rust is perfect for browsers)
Zig is intended to be robust for software like TigerBeetle, or the Zig compiler itself, where memory lifetimes are simple.
I’d say the focus on built in tests, fuzzing, debug memory allocators and safe mode shows that Zig is absolutely intended to be robust, within the scope of what the language aims to be. Far more than C itself or most of its popular compilers ever did.
Despite TigerBeetle being one of the highest-profile remaining Zig projects, I actually don't think they're representative of the average Zig project at all.
And I think embedded software is a field where Zig will be at its best. The only thing it is missing is maturity. When project lifetimes are measured in decades and changing a single byte can cost millions, no one in his right mind will pick a language that is still in development. Things will become interesting when it reaches 1.0.
Not a lot of people write programs this way.
I've even seen it on some simulation software's core that was written in the 80s originally; at the time memory was much more constrained so allocating upfront meant you could check upfront whether the simulation could actually run or not vs crashing out part way through.
"Kelley describes why he created Zig, when other options including C, C++, Rust, and Go already exist. He said he set out to develop a digital audio workstation. He tried Go, but found interoperability with C libraries difficult, and said the garbage collector caused audio delays. He tried C++, or coding C-style using a C++ compiler, but found that small mistakes led to memory corruption bugs that took weeks to fix. He tried Rust but "really struggled to write code that would satisfy Rust's rules," and spent a month trying to make font rendering work."
https://www.theregister.com/software/2026/05/28/zig-creator-...
Give it a few years! I've noticed an explosion in interest in formal verification recently, especially since nowadays the bar to entry is so low: just ask your LLM agent to give it a go.
Of course you could argue that on average, most programmers are not going to have the right practices and skill, so on average you should prefer Rust. But that's unrelated to the argument I was making, and in any case not a very interesting point in my opinion.
The problem is mostly people graduating from school thinking that somehow there is only stack and heap, and malloc/free is how you do heap. That view completely ignores that the essence of programming systems is mostly to understand the machine, and then doing conceptual and architectural work on a solution (and also on a problem). The act of writing actual code is then mostly just translating those concepts into the digital world verbatim.
Allocating in large chunks is often not very performant which is why people came up with tools like the borrow checker, you often want to allocate and deallocate dynamically on a need-basis but that's exactly where bugs occur.
"Extraordinary claims require extraordinary evidence" -- Carl Sagan
That's just it, using Zig required more rigorous engineering than the Bun team were capable of.
Who are these "people" you speak of? It's possible to write software in low level languages that don't have these problems. Not a "non-zero" it might be possible, it can be done thoughtfully, and the popular notion it can't be done is backed only by incomplete anecdotes.
Should everything be written in low-level languages? No, that would be absurd. Is it a simple fact of life that not every person/team/organisation is capable of meeting certain standards of rigour? Yes. That's not to say anyone in the Bun team could not become sufficiently competent in the future. For whatever reason, current experience, incentives, and personal motivations did not make for a conducive environment to make Bun watertight in Zig.
Zig does help you. Array slices, explicit nullability of pointers, defer errdefer, explicit allocators, built-in leak detection, bounds checks, overflow detection, the list goes on. If you need to play around on that side of the fence, Zig gives you a lot to make sure you don't mess it up. If we were talking about C I'd give you your flowers, but we're not. The most common issues and vulnerabilities that crop in C from manually managing memory are strongly mitigated by a quarter of that list.
The good news here is that we have more than just anecdotes to support this, we have empirical evidence.
You could write a JS engine with Zig-like idioms (arena allocation, static initialization), but that would require re-writing the whole JS engine from the ground-up (though I would definitely be interested in it if someone actually tries to do it!)
Arena allocators & static initializers are not novel. You'll find them in high performance C++ projects as well, such as LLVM or JavaScriptCore. But arena allocators have the quite significant limitation that they only help when everything being allocated in them have approximately the same lifetime. So they don't help when you need to allocate memory to provide the native implementation of a JavaScript object, for example (eg, FFI).
Could you actually? That seems like a bad fit for a JS engine to me. Predictable memory requirements are great when you can have them, perhaps you can avoid complexity then, but for a JS engine?
The C++ interop of Swift is perfectly fine, to such a degree that FoundationDB is now using it effectively alongside its C++ origins.
I cannot take this seriously as tutorials on robust Zig Allocation Pools will store a deinit method for each item within the pool, so when the pool deinits, all internal objects can be deinit'd.
That is just RAII & dtors from first principles, except with extra overhead of manually storing fat pointers yourself (and the bugs that come with this). Instead of using a language with builtin guarantees & optimizations around handling this so your object pools don't need to carry around a bunch of function pointers. C++ has aggressive de-virtualization passes so at runtime a lot of the 'complex object hierarchies' can be flattened to purely static function calls.
Here I would just like to mention that if you have to rely on "de-virtualization" passes, you're in a miserable situation architecturally. If you have code where the overhead of virtual function calls might be too much to pay, don't do virtual functions then. End of story.
To deconstruct a pool of objects, I don't see what should ever be wrong with a function pointer. The overhead of loading the function pointer will get divided by the number of objects being deconstructed. Care to explain what's the issue here?
1. You're writing code you don't have to
2. That adds runtime overhead
3. That when you screw up has non-trivial security & resource management side effects
This is objectively indefeasible in nearly any vaguely professional context.
2. And the code being compiled is abstract & generic, it won't be instantiated for every type and bloat the executable or instruction cache.
3. Security concerns: With C++ virtual methods every object carries a mutable pointer too (to a vtable containing function pointers). What resource management side effects please?
A garbage collected language does this automatically. Rust still requires thinking about and tracking memory lifecycles, but the borrow checker will complain and keep you from doing it wrong. That's why LLMs like Rust. It gives immediate feedback on what to fix. By-default constant reference parameters helps prevent major performance problems.
Ownership is automatic. You don't have to explicitly drop things when they go out of scope. (although the responsibility for that is split between the compiler and the library code).
Lifetimes are not automatic, but also have no effect on memory allocation. They are purely a static analysis path and you can make a functioning rust compiler that completely ignores lifetimes.
That's a semantic distinction which does not matter in the point OP is making. And contrasting rust static approach to general GC.
Once raw pointer is turned into a T, &T or &mut T, the borrow checker is on.
The only thing unsafe does is let you have an unbounded lifetime. As I said, it doesn't check those:
fn get_str<'a>(s: *const String) -> &'a str {
unsafe { &*s }
}
https://doc.rust-lang.org/nomicon/unbounded-lifetimes.html> if you generously sprinkle pointer dereferences in unsafe code, you effectively disable the protection provided by the borrow checker
You don't disable anything. You wrote a "trust me compiler" block, and compiler trusted you.
Rust won't ever protect from all possible problems, just the ones the compiler handles.
That’s the same thing.
With unsafe, you’re telling the compiler “I’ve taken extra care to make sure that what I’m doing is safe and doesn’t break your rules” and the compiler can go ahead and assume that you don’t, in fact, break the rules, and therefore can verify everything else as if the rules never got broken.
In less safe languages, the entire program is “trust me, it’s safe”, while in rust only the parts flagged as unsafe are.
The point is that you should only use unsafe when 1. It’s absolutely necessary for functionality or performance and 2. You have verified and are very certain that the code is correct.
That’s a very useful property to have.
Obviously you should try to avoid writing unsafe Rust to begin with.
As for the actual unloaded question, "What's the point of unsafe in Rust?" it is to contain and make it easier to identify sources of UB.
Maybe in the future the unsafe code will go down to 1%, bringing that to two orders of magnitude.
Of course, only time will tell if that is true or not, but from experience I’d be willing to bet it is.
No, you're wrong: You can create any lifetime. Proof:
fn oof<'desired>(x: &u32) -> &'desired u32 {
let ptr = x as *const u32;
unsafe { &*ptr }
}
This will take a reference and return it with any lifetime specified by the caller.> You don't disable anything.
I said "effectively disable". For example:
fn trust_me_bro<'a>(x: mut u32) -> &'a mut u32 { unsafe { &mut x } }
fn main() {
let mut x = 1_u32;
let reference_a = &mut x;
let reference_b = trust_me_bro(reference_a);
*reference_b = 2; -- Whoops
println!("reference_a: {reference_a} reference_b: {reference_b}");
}
After the call to trust_me_bro, two aliasing, mutable references exist simultaneously. This would usually be prevented by the borrow checker, but the unsafe code has effectively disabled it.You just listed examples of unbounded lifetimes.
> I said "effectively disable". For example:
Not sure what you meant by this example since it doesn't compile. It seems the borrow checker caught your mischief. So much for effectively disabling stuff :P
You haven't effectively disabled anything; you just (tried to) wrote unsound code that washes one mutable ref as another. This stuff is allowed provided shared refs are never accessed at the same time (for example, panicking upon reading reference_b).
What you probably meant is https://play.rust-lang.org/?version=stable&mode=debug&editio...
But you know what? If you're dabbling in unsafe, you have this big button called Tools in the playground. Choose Miri, then run your code; it will display large Undefined behavior. It even highlights the `trust_me_bro` function.
Hell, run this with UBSAN, ASAN, and other C tools. They will probably catch any such behavior.
You're just splitting hairs and trying to weasel around the fact that yes, you really can create any lifetime you want using unsafe.
> Not sure what you meant by this example since it doesn't compile.
That's because the crappy HN formatting ate some of the characters. Here's the original version:
https://play.rust-lang.org/?version=stable&mode=debug&editio...
> What you probably meant is […]
If you know what I meant, then what's up with your snarky comment about "So much for effectively disabling stuff"? In your playground link, you did effectively disable the borrow checker in safe parts of the code. Next thing you're moving the goalpost, now it's not about external, static analysis tools: "run this with UBSAN, ASAN, and other C tools". I don't think you're arguing in good faith here. Goodbye.
No. I said borrow checker allows unbound lifetime. I didn't disagree with you. I noted you didn't read the argument.
> If you know what I meant, then what's up with your snarky comment about "So much for effectively disabling stuff"?
Because your original code looked like trying to cast mut u32 to pointer.
> you did effectively disable the borrow checker in safe parts
So this code (https://play.rust-lang.org/?version=stable&mode=debug&editio...) runs now?
Effectively means achieving the goal in satisfying manner.
Writing unsound code is less of effectively disabling borrow checker and more of a hack.
Think about it, if using Java unsafe I gain access to underlying HashMap array and do weird stuff to the HashMap invariants am I effectively turning HashMap into Array or am I doing a hack job?
Edit: By that logic since there is so much unsafe in the code base isn't borrow checker effectively already disabled? You can argue semantics but no, borrow checker isn't de facto or de jure invalidated by unsafe blocks.
It's invalidated by unsound unsafe and that's on code writer to fix.
E.g. in C you can write code and say "don't call it outside the situation that this function was written for" and then blame [0] future users of the API.
In Rust you need to actually make sure that your code works, i.e. your safe interface is not unsafe.
[0] The blame game is not a technical solution to a technical problem...
Garbage collection is a method to make programming easier, not to make the resulting program better.
Even with the huge amount of "unsafe" rust currently in bun? https://news.ycombinator.com/item?id=48967630
Of course they’ll iterate and remove the unsafe bits which were necessary for the transition
[0] https://andrewkelley.me/post/my-thoughts-bun-rust-rewrite.ht...
News at 11.
“Safe rust isn’t expressive enough!” -> then use unsafe rust.
“Unsafe rust lets you do anything! Even crazy things!” -> then use safe rust. Or just don’t write crazy code?
Does bun actually do anything insane like that in its unsafe blocks? Or are you just fear mongering?
> Does bun actually do anything insane like that in its unsafe blocks?
Who knows? At 10k unreviewed uses of unsafe, I'd guess there are quite a few incorrect ones. LLMs don't produce perfect code (neither do humans), so there's a high probability that at least some of those create UB.
How would anyone even know, it's vibe coded.
"At the time of writing, about 4% of Bun's Rust code sits inside an unsafe block (~13,000 unsafe keywords across ~27,000 lines / ~780,000 lines), and 78% of those blocks are a single line — a pointer that came from C++, or one call into a C library. I expect this number to go down over time as we refactor from a faithful Zig port (which had no greppable unsafe keyword) to idiomatic Rust, but we are going to continue using C & C++ libraries like JavaScriptCore so it will always have more unsafe than pure Rust projects."
It's unsafe to call a C library that gives you a raw pointer, that can be a single line. It's unsafe to use that pointer, that could be a second single line. Carrying that pointer around, the data structures it's in, that's all safe, and doesn't implicate lifetime checking at all, so Rust will let you do silly things with the actual lifetime.
A better metric would be absolute unsafe keywords (because each carries a need to review), or "code in a module that uses unsafe anywhere" vs total code, because module boundaries isolate unsafe.
In short, if you use an unsafe block, then potentially any part of your code is unsafe.
The way I think about it is that the unsafe keyword is a promise that you’re going to maintain the safety invariants yourself, manually. The program is memory correct if it correctly maintains a certain set of invariants. Unsafe punts responsibility over those invariants to the programmer. If you use unsafe and mess up, the program may be in an invalid state. Yes - it might crash, anywhere. But the bug is still almost always in an unsafe block.
It’s often possible to make fully safe wrappers around unsafe code which maintains all those invariants manually. (Either statically or dynamically.). A lot of the rust standard library does this. For example, Vec, Box and slice all use unsafe code internally to create safe APIs.
That's entirely dependent on how you write your Rust code. If you're derefing an invalid pointer then the bug is usually in how you calculated that pointer value, but the only part that actually requires 'unsafe' is the deref, not the bugged pointer calculation.
Now in properly written Rust code you should be marking all of that code as 'unsafe' in that case and documenting what invariants need to be maintained, but that's entirely on you to do. The only part the compiler actually enforces is that you mark the specific spots where you make use of the operations that 'unsafe' allows.
Yes, I wish there was a way to mark code as unsafe without also allowing unsafe operations. Its quite common I have some "safe" code that generates values which are eventually used in an unsafe block. If the "safe" code is wrong, my unsafe code will fail in memory-unsafe ways. But there's currently no way to annotate this sort of "safe" code. Rust provides the unsafe keyword - but that keyword is generally reserved for code which needs to actually make unsafe operations (like derefing pointers or calling other unsafe functions).
There was a world where everyone just used the unsafe keyword everywhere, and Rust code crashed & had memory issues all the time.
The big thing Rust did wasn't invent borrow checking or memory safety; it's the ease of use, the defaults, and the social aspect of "if unsafe is used, a memory bug is in the unsafe part".
This is a common misconception, in a literal interpretation. It doesn't invalidate your point, but since people get the wrong impression: unsafe doesn't turn off any of the checks the Rust compiler does.
It allows you to perform 5 additional operations, and that's it. Using those operations wrong is what breaks the safety promises of the language. As an example, you could dereference a raw pointer and tell the Rust compiler it lives forever, when it's really a pointer to an object that's about to be freed.
All of this is just a (succesful) marketing stunt by Anthropic.
Yes, and again the "you're holding it wrong" people or "you are not a good enough developer" people will try to do juggling with a chainsaw and lose a couple of fingers in the process
Claude Code's endorsement (and real-world testing) speaks louder than internet discussions that are at this point 30 years old (and probably more)
But only as far as "make it compile" is a good predictor of runtime behavior. In C++ for instance, I can "make it compile" and it still crashes at runtime or does other undesirable things.