> But it does not nearly approach the level of systematic prevention of memory unsafety that rust achieves.
Unless I gravely misunderstood Zig when I learned it, the Zig approach to memory safety is to just write a ton of tests fully exercising your functions and let the test allocators find and log all your bugs for you. Not my favorite approach, but your article doesn't seem to take into account this entirely different mechanism.
Yes, testing is Zig's answer. But that quote is right. Testing doesn't achieve the same kind of systematic prevention of memory bugs that rust does. (Or GC based languages like Go, Java, JS, etc.).
You can write tests to find bugs in any language. C + Valgrind will do most of the same thing for C that the debug allocator will do for zig. But that doesn't stop the avalanche of memory safety bugs in production C code.
I used to write a lot of javascript. At the time I swore by testing. "You need testing anyway - why not use it to find other kinds of bugs too?". Eventually I started writing more and more typescript, and now I can't go back. One day I ported a library I wrote for JSON based operational transform from javascript to typescript. That library has an insane 3:1 test:code ratio or something, and deterministic constraint testing. Despite all of that testing, the typescript type checker still found a previously unknown bug in my codebase.
As the saying goes, tests can only prove the presence of bugs. They cannot prove your code does not have bugs. For that, you need other approaches - like rust's borrow checker or a runtime GC.
I suppose you can even ship the test/logging allocator with your production build, and instruct your users to run your program with some option / env var set to activate it. This would allow to repro a problem right where it happens, hopefully with some info helpful for debugging attached.
Not a great approach for critical software, but may be much better than what C++ normally offers for e.g. game software, where the development speed definitely trumps correctness.
What that means, though, is that you have a choice between defining memory unsafely away completely with Rust or Swift, or trying to catch memory problems by a writing a bunch of additional code in Zig.
I’d argue that ‘a bunch of additional code’ to solve for memory safety is exactly what you’re doing in the ‘defining memory safety away’ example with Rust or Swift.
It’s just code you didn’t write and thus likely don’t understand as well.
This can potentially lead to performance and/or control flow issues that get incredibly difficult to debug.
Weird that Swift is your totem for "managed/collected runtime" and not Java (or C#/.NET, or Go, or even Javascript). I mean, it fits the bill, but it's hardly the best didactic choice.
What if -- stay with me now -- what if we solved it by just writing vastly less code, and having actually reusable code, instead of reinventing every type of wheel in every project? Maybe that's the real secret to sound code. Actual code reuse. I know it's a pipedream, but a man can dream, can't he?
False. Fil-C secures C and C++. It’s more comprehensively safe than Rust (Fil-C has no escape hatches). And it’s compatible enough with C/C++ that you can think of it as an alternate clang target.
Fil-C is impressive and neat, but it does add a runtime to enforce memory safety which has a (in most cases acceptable) cost. That's a reasonable strategy, Java and many other langs took this approach. In research, languages like Dala are applying this approach to safe concurrency.
Rust attempts to enforce its guarantees statically which has the advantage of no runtime overhead but the disadvantage of no runtime knowledge.
> Fil-C achieves this using a combination of concurrent garbage collection and invisible capabilities (each pointer in memory has a corresponding capability, not visible to the C address space)
In almost all uses of C and C++, the language already has a runtime. In the Gnu universe, it's the combination of libgcc, the loader, the various crt entrypoints, and libc. In the Apple version, it's libcompiler_rt and libSystem.
Fil-C certainly adds more to the runtime, but it's not like there was no runtime before.
There is a third category of memory and other software safety mechanisms: model checking. While it does involve compiling software to a different target -- typically an SMT solver -- it is not a compile-time mechanism like in Rust.
Kani is a model checker for Rust, and CBMC is a model checker for C. I'm not aware of one (yet!) for Zig, but it would not be difficult to build a port. Both Kani and CBMC compile down to goto-c, which is then converted to formulas in an SMT solver.
There isn't a real one yet, but to scratch an itch I tried to build one for Zig. It's not complete nor do I have plans to complete it. https://github.com/ityonemo/clr
If zig locks down the AIR (intermediate representation at the function level) it would be ideal for running model checking of various sorts. Just by looking at AIR I found it possible to:
- identify stack pointer leakage
- basic borrow checking
- detect memory leaks
- assign units to variables and track when units are incompatible
If you're filling uninitialized pointers with AAAAAAAA, it might be best to also reserve that memory page and mark it as no-access.
I'm not even joking. Any pattern used by magic numbers that fill pointers (such as HeapFree filling memory with FEEEEEEE on Windows) should have a corresponding no-access page just to ensure that the program will instantly fail, and not have a valid memory allocation mapped in there. For 32-bit programs, everything past 0x8000000 used to be reserved as kernel memory, and have an access violation when you access it, so the magic numbers were all above 0x80000000. But with large address aware programs, you don't get that anymore, only manually reserving the 4K memory pages containing the magic numbers will give you the same effect.
Maybe not Zig the language, but the fact that all allocating functions in the standard library accept an allocator (and community libraries follow this precedent) does give you much more control in practice.
For example, how would you use a Vec using stack memory for elements, instead of the heap? For the equivalent data structure in Zig (std.ArrayList), it's just a matter of using a stack allocator instead of using a heap allocator, which is an explicit decision either way.
Unless I gravely misunderstood Zig when I learned it, the Zig approach to memory safety is to just write a ton of tests fully exercising your functions and let the test allocators find and log all your bugs for you. Not my favorite approach, but your article doesn't seem to take into account this entirely different mechanism.
You can write tests to find bugs in any language. C + Valgrind will do most of the same thing for C that the debug allocator will do for zig. But that doesn't stop the avalanche of memory safety bugs in production C code.
I used to write a lot of javascript. At the time I swore by testing. "You need testing anyway - why not use it to find other kinds of bugs too?". Eventually I started writing more and more typescript, and now I can't go back. One day I ported a library I wrote for JSON based operational transform from javascript to typescript. That library has an insane 3:1 test:code ratio or something, and deterministic constraint testing. Despite all of that testing, the typescript type checker still found a previously unknown bug in my codebase.
As the saying goes, tests can only prove the presence of bugs. They cannot prove your code does not have bugs. For that, you need other approaches - like rust's borrow checker or a runtime GC.
Not a great approach for critical software, but may be much better than what C++ normally offers for e.g. game software, where the development speed definitely trumps correctness.
It’s just code you didn’t write and thus likely don’t understand as well.
This can potentially lead to performance and/or control flow issues that get incredibly difficult to debug.
False. Fil-C secures C and C++. It’s more comprehensively safe than Rust (Fil-C has no escape hatches). And it’s compatible enough with C/C++ that you can think of it as an alternate clang target.
Rust attempts to enforce its guarantees statically which has the advantage of no runtime overhead but the disadvantage of no runtime knowledge.
Fil-C doesn't "add a runtime". C already has a runtime (loader, crt, compiler runtime, libc, etc)
Yeah. By adding a runtime.
> Fil-C achieves this using a combination of concurrent garbage collection and invisible capabilities (each pointer in memory has a corresponding capability, not visible to the C address space)
https://github.com/pizlonator/llvm-project-deluge/tree/delug...
So? That doesn't make it any less safe or useful.
In almost all uses of C and C++, the language already has a runtime. In the Gnu universe, it's the combination of libgcc, the loader, the various crt entrypoints, and libc. In the Apple version, it's libcompiler_rt and libSystem.
Fil-C certainly adds more to the runtime, but it's not like there was no runtime before.
Fil-C is in the cards for my next project.
Hit me up if you have questions or issues. I’m easy to find
How safe is Zig? - https://news.ycombinator.com/item?id=31850347 - June 2022 (254 comments)
How Safe Is Zig? - https://news.ycombinator.com/item?id=26537693 - March 2021 (274 comments)
How Safe Is Zig? - https://news.ycombinator.com/item?id=26527848 - March 2021 (1 comment)
How Safe Is Zig? - https://news.ycombinator.com/item?id=26521539 - March 2021 (1 comment)
Kani is a model checker for Rust, and CBMC is a model checker for C. I'm not aware of one (yet!) for Zig, but it would not be difficult to build a port. Both Kani and CBMC compile down to goto-c, which is then converted to formulas in an SMT solver.
If zig locks down the AIR (intermediate representation at the function level) it would be ideal for running model checking of various sorts. Just by looking at AIR I found it possible to:
- identify stack pointer leakage
- basic borrow checking
- detect memory leaks
- assign units to variables and track when units are incompatible
https://smt.st/SAT_SMT_by_example.pdf
The algorithms behind SAT / SMT are actually pretty straight-forward. One of these days, I'll get around to publishing an article to demystify them.
I'm not even joking. Any pattern used by magic numbers that fill pointers (such as HeapFree filling memory with FEEEEEEE on Windows) should have a corresponding no-access page just to ensure that the program will instantly fail, and not have a valid memory allocation mapped in there. For 32-bit programs, everything past 0x8000000 used to be reserved as kernel memory, and have an access violation when you access it, so the magic numbers were all above 0x80000000. But with large address aware programs, you don't get that anymore, only manually reserving the 4K memory pages containing the magic numbers will give you the same effect.
https://ziglang.org/documentation/master/#undefined
Zig gives you the control you need if that is what you want, safety isn't something Zig is chasing.
Safer than C, yeah, but not safe.
Rust = safe Zig = control
Pick your weapon for the foe in front of you.
For example, how would you use a Vec using stack memory for elements, instead of the heap? For the equivalent data structure in Zig (std.ArrayList), it's just a matter of using a stack allocator instead of using a heap allocator, which is an explicit decision either way.