Thoughts on Go vs. Rust vs. Zig
2 points by ucirello
2 points by ucirello
What is the dreaded UB? I think the best way to understand it is to remember that, for any running program, there are FATES WORSE THAN DEATH. If something goes wrong in your program, immediate termination is great actually! Because the alternative, if the error isn’t caught, is that your program crosses over into a twilight zone of unpredictability, where its behavior might be determined by which thread wins the next data race or by what garbage happens to be at a particular memory address. Now you have heisenbugs and security vulnerabilities. Very bad.
Even that undersells it. "Undefined Behaviour" is behaviour that the compiler's optimizers have been promised will never happen. In the world of C, it's sometimes referred to as "nasal demons" because, upon encountering undefined behaviour, the compiler is allowed to do anything including making demons fly out of your nose.
You can't check for it at runtime because it's not a property of your runtime semantics. It's a property of the "algebra" that the compiler performs to transform your code into a more efficient shape while preserving its observable properties before lowering it into machine instructions.
It's, "If you do this, then the compiler is allowed to see printf("Hello, World!"); and silently emit the machine instructions corresponding to system("rm -rf /")" and it's done that way because the pathological cases are emergent behaviour... unanticipated crazy results from stacking up simple, reasonable changes. (It's basically the programming language version of an ant mill.)
See Why undefined behavior may call a never-called function for an example of how this can happen in one shocking way in real C++ code. (The gist is "I've been promised that this function pointer will not be called before it's been initialized. It's a local variable, so it can only be set by a function within this compilation unit. Logically, even if I can't see it, something must call this externally visible initializer before this code is reached, so I'll inline it.")
In Rust, creating a mutable global variable is so hard that there are long forum discussions on how to do it. In Zig, you can just create one, no problem.
Creating an unguarded/unsynchronized mutable global variable (i.e. static mut) is so risky that there's an open RFC to deprecate the Rust language feature entirely.
(It has to do with how easy it is to accidentally violate the aliasing rules the optimizer has been promised it can rely on via the same underlying LLVM IR annotations that C's restrict lowers to.)
Creating a mutable static through the use of interior mutability is trivial. Just create a static that is non-mut and put your type T inside a Mutex<T> or RwLock<T>. There are even example code snippets in the Disallow references to static mut section of the migration guide for Rust 2024, because they switched the static_mut_refs lint to deny.
See also:
Bear in mind that this sort of thing isn't just limited to the programming language itself. Undefined behaviour can also lead to the programming language not emitting the correct hints to the CPU that allow it to pretend it's a fast PDP/11 when it's really got a complex hierarchy of caches, pipelines, and speculative execution under the hood to fake a faster speed of light between the CPU and main RAM. ...when that happens, you can wind up with cache coherency bugs like this: