Most active commenters
  • pron(12)
  • littlestymaar(8)
  • AsyncAwait(3)

←back to thread

200 points jorangreef | 29 comments | | HN request time: 1.257s | source | bottom
Show context
logicchains ◴[] No.24293046[source]
I work in HFT, and one of the key concerns when writing low-latency code is "is this code allocating memory, and if so, how can I stop it?" Zig is the perfect language for this use case as none of the standard library implicitly allocates, rather for anything that allocates, the caller must pass in an allocator. The stdlib also provides a handy arena allocator, which is often the best choice.

This is a huge advantage over C++ and Rust, because it makes it much harder for e.g. the intern to write code that repeatedly creates a vector or dynamically allocated string in a loop. Or to use something like std::unordered_map or std::deque that allocates wantonly.

replies(8): >>24293328 #>>24293382 #>>24293469 #>>24293919 #>>24293952 #>>24294403 #>>24294507 #>>24298257 #
1. AsyncAwait ◴[] No.24293328[source]
> This is a huge advantage over C++ and Rust, because it makes it much harder for e.g. the intern to write code that repeatedly creates a vector or dynamically allocated string in a loop. Or to use something like std::unordered_map or std::deque that allocates wantonly.

True. On the other hand, Zig makes a deliberate decision not to bother itself with memory safety too much, so its a win some, loose some sort of situation.

replies(1): >>24293466 #
2. pron ◴[] No.24293466[source]
> On the other hand, Zig makes a deliberate decision not to bother itself with memory safety too much

This is not true. Zig places a strong emphasis on memory safety, it just does so in a way that's very different from either Java's or Rust's. I wrote more about this here: https://news.ycombinator.com/item?id=24293329

replies(2): >>24293966 #>>24295336 #
3. littlestymaar ◴[] No.24293966[source]
> It does so with runtime checks that are turned on in development and testing and turned off -- either globally or per code unit -- in production.

This isn't “memory safety”, with this reasoning you could say “C is memory safe if you use ASAN during debug”: it is exactly equivalent except Zig checks are less powerful than the full suite of existing sanitizers for C, but it's enabled by default in debug mode, which is nice.

replies(1): >>24294039 #
4. pron ◴[] No.24294039{3}[source]
No, this is full memory safety enforced through runtime checks. ASAN does not give you that. Zig has only arrays and slices with known sizes and no pointer arithmetic (unless you explicitly appeal to unsafe operations).
replies(2): >>24294204 #>>24294285 #
5. littlestymaar ◴[] No.24294204{4}[source]
> full memory safety enforced through runtime checks [disabled in production]

Which means you cannot guaranty your program doesn't exhibit memory unsafely unless you stumble upon it during testing. Yes there are fewer footguns in Zig[1] than in C (which is the opposite of C++), but dangling pointer dereferences, double free and race conditions will still be lurking in any reasonably-sized codebase. Calling it “memory-safe”, is dishonest. And I actually don't think it serves Zig. It's a clever language with tons of good ideas, no need to oversell it with misleading claims about memory safety.

[1]: but still plenty of them https://ziglang.org/documentation/master/#Undefined-Behavior

replies(3): >>24294443 #>>24294505 #>>24303987 #
6. rfoo ◴[] No.24294285{4}[source]
I thought after Intel MPX we can all agree "memory safety" in modern languages is more about temporal stuff (i.e. use-after-free, etc) than bounds check, but maybe I'm wrong.

How does those runtime checks kill UAF?

replies(1): >>24294637 #
7. pron ◴[] No.24294443{5}[source]
> Which means you cannot guaranty your program doesn't exhibit memory unsafely unless you stumble upon it during testing.

Right, if you disable those checks.

> Calling it “memory-safe”, is dishonest.

The language is (or will be) memory safe, at least its safe parts -- C/C++ aren't. True, that safety can be disabled selectively. Yes, there can then be undefined behaviour if you disable that safety, but so can Rust when you disable safety selectively. The design is just different. Just remember that using a language with sound guarantees is no one's goal. The goal is to produce programs without bugs. Sound guarantees in the language is one approach toward that goal; there are others.

Generally, guaranteeing that some set of bugs cannot happen does not necessarily guarantee fewer bugs overall; in fact the opposite might be true. Nobody knows if Zig's strong safety story yields to more correct programs that Rust's, but nobody knows the opposite, either. There are good arguments either way but little data. In any event, Zig is much safer than C, even with ASAN.

replies(1): >>24294595 #
8. judofyr ◴[] No.24294505{5}[source]
> Calling it “memory-safe”, is dishonest.

I'm not sure why we desperately need to classify languages into "memory-safe" or "not memory-safe". The fact is that all languages have various levels memory-safety (sun.misc.Unsafe anyone?) and I do think that Zig deserves recognization for addressing it heads on:

- There's a separate ReleaseSafe optimization level which has all the optimizations, but remains safe. If you care about memory-safety (which most people do!) then this should be your default production build.

- The documentation is very clear about what can cause undefined behavior in ReleaseFast.

- You can override the safety-level (both ways!) for a single scope. If you have something performance critical that can't be expressed in safe Zig you can opt-in to potential undefined behavior. If you have something safety-critical you can opt-in for correctness even though the overall program is compiled with ReleaseFast.

9. littlestymaar ◴[] No.24294595{6}[source]
> The language is (or will be) memory safe.

We've been going full cicle here, so I'm not interested in spending more time in this conversation.

10. pron ◴[] No.24294637{5}[source]
> https://github.com/ziglang/zig/pull/5998

TBD :)

But here's one way that's currently being tried: https://github.com/ziglang/zig/pull/5998

replies(1): >>24298056 #
11. AsyncAwait ◴[] No.24295336[source]
I meant a borrow checker, which I assumed was clear from what I was replying to. Yes, Zig does do runtime checks in dev builds and I did not mean to imply otherwise, I don't think the runtime checks provide the same set of benefits.

Just so we're clear, I like Zig :-)

replies(1): >>24297283 #
12. pron ◴[] No.24297283{3}[source]
> I don't think the runtime checks provide the same set of benefits.

Of course not, but that doesn't mean Zig is less effective at achieving correctness. It just does so in a different way -- it trades sound guarantees for less sound ones and a simpler language with a faster cycle. Is one better than the other? Hard to say. Only empirical study could settle that.

replies(1): >>24298562 #
13. littlestymaar ◴[] No.24298056{6}[source]
(Small copy past error here, you posted the same link twice.)

Regarding https://github.com/ziglang/zig/pull/5998 here we're exactly in the realm of C, changing the allocator with a custom one with additional bookkeeping to check for memory management issues. But it tanks performances so you can't generally use it for production (and if you were in a situation were you'd do it anyway, you'd be better off with completely automatic memory management: AKA a GC).

replies(1): >>24298625 #
14. fsociety ◴[] No.24298562{4}[source]
I’d argue it does mean it is less effective at achieving correctness but the trade-off made is the whole point of Zig. Simple language that is really a better C.
replies(1): >>24298692 #
15. pron ◴[] No.24298625{7}[source]
No, Zig is not in the realm of C. Zig gives you full memory safety that you can then selectively turn off. Why is it useful? For the same reason tests are useful even if they don't give you sound guarantees, and are still the primary way of achieving correctness, even in Haskell or Rust. C does not and cannot do this the same way as Zig does, because C cannot be made safe (well, it can, but that's a whole other can of worms) while Zig can. So you make Zig safe, test it, and then remove the guardrails from the performance-critical bits after you're satisfied with their correctness.

Does it provide safety in the same manner Rust does? Absolutely not. Does it provide less correctness overall? Maybe, and maybe it provides more correctness, and maybe the same. It's hard to say without an empirical study. The problem is that sound guarantees often come at a cost -- for example, to language complexity and compilation speed -- that can have a negative effect on correctness.

replies(2): >>24298722 #>>24299513 #
16. pron ◴[] No.24298692{5}[source]
It's hard to make a definitive argument one way or the other because the guarantees Rust makes come at a cost of compilation speed and language complexity that can have a negative effect on correctness. This question is impossible to answer without an empirical study.

Unlike C, Zig is (or will be) memory safe, although its safety can be turned off, and often is -- after testing. Unlike C, it provides powerful abstraction capabilities similar to those of C++. The fact that it can do all that yet be very simple seems to suggest at first glance that it's "like C" but that's because we've never had a language like that before. Zig's simplicity is misleading. It turns out you can do a lot with a very simple language. We knew that to be true for high-level languages like Scheme, but Zig shows it's possible in low-level languages, too.

replies(1): >>24299620 #
17. littlestymaar ◴[] No.24298722{8}[source]
> C does not and cannot do this the same way as Zig does

Regarding the PR you just sent, I'd like to hear why you think it cannot be applied to C?

> So you make Zig safe, test it, and then remove the guardrails from the performance-critical bits after you're satisfied with their correctness.

This isn't safety… This is “we didn't find any memory issue while fuzzing the software” and you'd get the same guarantee: if your fuzzer didn't cause the memory issue, then it remains in your code in production, waiting to explode one day with some hard to debug Heisenbug that only occurs once in a million…

replies(1): >>24298913 #
18. pron ◴[] No.24298913{9}[source]
> Regarding the PR you just sent, I'd like to hear why you think it cannot be applied to C?

I didn't mean that a safe allocator cannot be used in C; I meant that C cannot be made memory safe in its entirety as simply as Zig can. Why? Because C has pointer arithmetic while (safe) Zig doesn't, Zig has slices while C doesn't, and C has non-typesafe casts while safe Zig doesn't.

> This isn't safety… This is “we didn't find any memory issue while fuzzing the software” and you'd get the same guarantee:

No, it's not the same guarantee. Fuzzing a C program will not find all the undefined behaviour that fuzzing a Zig program can, for the reasons I mentioned.

It is true that if you use unsafe Zig, i.e. turn off safety for a whole program or some sections of it, you lose the guarantees that safe Zig gives you, and unsafe Zig is indeed not safe (neither is unsafe Rust). But because of the way it's designed, Zig has a way of improving correctness even when safety is removed. This is a tradeoff for sure, but so are sound guarantees, that can have other negative effects on correctness.

replies(1): >>24300895 #
19. ◴[] No.24299513{8}[source]
20. AsyncAwait ◴[] No.24299620{6}[source]
> It's hard to make a definitive argument one way or the other because the guarantees Rust makes come at a cost of compilation speed

I don't think this is actually true. The compiler is slow but not due to memory safety; the 'cargo check' command is rather quick and the compiler itself doesn't seem to spend a lot of time in the frontend, most of the time in spent in the backend, past the borrow checking phrase.

replies(1): >>24305338 #
21. littlestymaar ◴[] No.24300895{10}[source]
> It is true that if you use unsafe Zig, i.e. turn off safety for a whole program or some sections of it, you lose the guarantees that safe Zig gives you, and unsafe Zig is indeed not safe

Their is no such things as unsafe and safe Zig. All Zig is unsafe, but you can add additional runtime checks (disabled by default in optimized builds) that will slow down your program when used. Using a specific allocator to detect UAF is something you may do in development, but almost surely never in production. And without it your code isn't memory-safe.

> Fuzzing a C program will not find all the undefined behaviour that fuzzing a Zig program can, for the reasons I mentioned.

Zig will have less UB than C, but there will still be lurking UB in your programs no matter how long you test it. Consider the following snippet (on mobile, so this may have stupid syntax errors):

  test "this is UB, but the test won't show it" {
    const allocator = std.heap.page_allocator;
    var buf = try allocator.alloc(u8, 10);
    ohNo(allocator, 42, buf);
    allocator.free(bar);
  }

  fn ohNo(allocator: *Allocator, foo: const u8, bar: *u8) void {
    if (foo == 1337) {
        // double free awaiting to happen in production
        allocator.free(bar);
    }
  }
If you never explicitly test the value “1337” during you debug session, you won't trigger the UB and you won't know it's here, then when you ship your optimized build in production, you'll ship a program with UB in it.
replies(1): >>24302852 #
22. pron ◴[] No.24302852{11}[source]
> Their is no such things as unsafe and safe Zig. All Zig is unsafe, but you can add additional runtime checks

Zig is meant to ultimately give you full memory safety, that you can selectively turn off. In addition, there are specific unsafe operations -- clearly marked -- such as casting an integer to a pointer or other non-typesafe casts.

A code unit with safety checks on and without unsafe operations is what I call "safe Zig."

> And without it your code isn't memory-safe.

This is simply not true. Perhaps you mean that you don't have a guarantee that your code is memory-safe, but that's not the same thing.

Our goal is not to write in a language with certain guarantees but to write programs with certain properties, say, without buffer overflows. One way of achieving such a program is to write it in a language that guarantees no such error can happen. Another is to write it in a language that guarantees no such error can happen in development, do some testing, and then remove the guarantees. In the second case it is true that our confidence in the lack of such errors is lower than the first, but in each case it is not 100%, and because the static guarantees are costly, it is possible that the second approach is even more effective at getting to more correct programs overall. They're both common ways for achieving the same goal.

As someone who works with formal methods, we do these tradeoffs in formal verification all the time. It is simple false that sound guarantees are always the best way to correctness -- it would be if they were free, but they're not.

Once you realise that the goal is achieving some desired level of confidence (which is never 100%, as that cannot exist in a physical system anyway) about overall program correctness -- which includes both "safety" and functional properties, each further divided into degrees of severity -- you see that there is no obvious way with the best effectiveness at achieving that goal.

> If you never explicitly test the value “1337” during you debug session, you won't trigger the UB and you won't know it's here

But here, again, you are looking at something in isolation. Because Zig is a simple language, the chances of such paths existing without you noticing are lower; also, because the language is simpler it is easier to write concolic testers that would automatically detect this.

In fact, if such a "rare path" exists in a complex language that causes some functional bug -- ultimately, we don't care what bug breaks our program or leaves it open to security vulnerabilities -- there's a smaller chance that it will be discovered. Which is exactly what I mean by soundness coming at a cost. It guarantees the lack of certain bugs, but because it complicates the language, it can make other bugs more costly to detect.

replies(1): >>24303545 #
23. littlestymaar ◴[] No.24303545{12}[source]
> This is simply not true. Perhaps you mean that you don't have a guarantee that your code is memory-safe, but that's not the same thing.

“But people can write correct C code”. Correct Zig != memory safety. It's the opposite: MEMORY SAFETY IS THE GUARANTEE that your code won't have memory error no matter how broken it is!

> Another is to write it in a language that guarantees no such error can happen in development, do some testing, and then remove the guarantees.

That's what the same kind of design as C is with ASan, TSan, MemSan etc. Yes Zig is less broken than C, leading to fewer sources of memory issues, but for what matters most (Double Free, Use After Free[1], Data Races) Zig and C offers the same level of safety guarantees: none.

> As someone who works with formal methods, we do these tradeoffs in formal verification all the time. It is simple false that sound guarantees are always the best way to correctness -- it would be if they were free, but they're not.

This is a straw man: comparing compile-time enforced ownership (Rust borrowck) to formal method doesn't make any more sense than comparing static typing to formal methods. It adds a lot of learning friction, but that's it. I just grepped my current 90kLoc rust project. You know how many lifetime annotation ('x) there is in it? Fifty-four! Which is one every 1666 lines. Please tell me again how much it cripples productivity and the ability to write correct code!

> Because Zig is a simple language, the chances of such paths existing without you noticing are lower;

If you ever try to use shared-memory parallelism, this kind of bugs will be everywhere! That's simple: every call to allocator.free is a minefield.

> ultimately, we don't care what bug breaks our program or leaves it open to security vulnerabilities

Memory safety issues aren't just security vulnerabilities, more than anything they are horrible bugs to track down, and it costs tons of money.

> Which is exactly what I mean by soundness coming at a cost. It guarantees the lack of certain bugs, but because it complicates the language, it can make other bugs more costly to detect.

This is BS. It's not because a language has few symbols or a simple syntax that it is easier to debug. Otherwise brainfuck would be the ultimate productivity tool. Semantic is what matters, and because it has UBs, Zig semantic is more complex than most languages out there. That's why C is one of the most complex language ever in practice, even if it's really “simple” and easy to “learn”.

Again, don't get me wrong, I have nothing against Zig and I find it refreshing because it has tons of cool ergonomic tricks (and having a built-in sanitizer which “just works” out of the box in debug mode without any other programmer intervention is cool!). It's a nice programming language experiment that will probably inspire a lot of others, and it's probably a really cool language for C programmers who like to manage their memory by themselves and don't want the “nany compiler” Rust has and still have a language with a modern look and feel: that's totally legit.

But memory safe, it isn't.

[1]: which cause more than 30% of Google and Microsoft security issues by itself! (https://www.zdnet.com/article/microsoft-70-percent-of-all-se... https://www.chromium.org/Home/chromium-security/memory-safet...)

replies(2): >>24304726 #>>24305249 #
24. pjmlp ◴[] No.24303987{5}[source]
Just like you cannot guarantee safety of any Rust application with unsafe code blocks.
25. ◴[] No.24304726{13}[source]
26. pron ◴[] No.24305249{13}[source]
> Correct Zig != memory safety. It's the opposite: MEMORY SAFETY IS THE GUARANTEE that your code won't have memory error no matter how broken it is!

I think you have a missing piece of factual information here. Safe Zig (which is not "correct Zig") guarantees (or will guarantee) memory safety everywhere, no matter how broken the code is, as long as you don't use unsafe operations -- just as in Rust. Instead of eliminating some issues at compile time, it does so by panicking at runtime.

> Please tell me again how much it cripples productivity and the ability to write correct code!

If you think that you -- and your 20-person team maintaining a project for 20 years -- can be as productive in Rust as you can in Zig, then Rust is for you. That's not the case for me (maybe it's not a universal thing) and I don't think that would be the case for my team. Personally, I think that universal truths in programming are rare, and I think it is very likely that Rust might be more effective for some and Zig more effective for others, even if you only consider correctness. I'm not trying to convince you that Zig is better than Rust for you; I'm just saying that Zig is better than Rust for me.

> Zig and C offers the same level of safety guarantees: none.

I'm afraid you're simply mistaken, and repeating the same assertion over and over does not make it more correct. Safe Zig will give you the same guarantees as safe Rust except data races.

Once you turn safety off, you don't have a guarantee but you also don't have anywhere near the same level of confidence as you do in the safety of the C program. So the choice is not between 99.999999% confidence of a guarantee and, say, 50%, but there's lots in between, and Zig is in the vicinity of where Rust is -- don't know if better or worse -- but is much better than where C is. Correctness is simply not a binary position.

You accept this position yourself: when you run your Rust program, you also have no guarantees about the overall functional correctness of the program. You still don't think that you're in the same position as everyone else with no such guarantees, right? That's because there are lots of other activities needed to be done to increase confidence in correctness, and so there can be a very, very wide range of correctness within that "no guarantee" which is where we all are in most cases. I think that Zig makes some of those activities easier than C++/Rust, at least for me.

> But memory safe, it isn't.

Except it is, actually (or, rather, will be) because it guarantees no memory safety errors.

Anyway, thank you for your insight. I've been a professional programmer for nearly 25 years, working on large, long-running projects, some of which are very safety critical (many people would die on failure), some employing formal verification, and it is my opinion that Zig's approach to safety is at least as good as Rust's. It's certainly possible that my opinion is shaped by my personal experience. My software would have killed people either due to a buffer overflow or due to an incorrect logic. Sacrificing things that for me would increase the effort to prevent the latter only to increase my confidence that I don't have faults of the former kind from 99.99% to 99.99999999% doesn't seem like a good tradeoff.

While your opininon to the contrary is just as legitimate, barring empirical evidence, you won't be able to convince me. That the most effective approach to increasing correctness is by eliminating an important class of bugs at the significant cost of language complexity and that there is no more effective approach is an interesting hypothesis, but one that is far from being established. I understand why some might believe it to be true, and also why some believe it to be false. Ultimately, safety and correctness are central design goals for both Zig and Rust, but they each make different tradeoffs to achieve what they consider to be a good sweet spots. Not having any definitive evidence over which is "better" in that regard, we must make our languages choices based on other criteria.

replies(1): >>24308448 #
27. pron ◴[] No.24305338{7}[source]
https://news.ycombinator.com/item?id=24302711
28. littlestymaar ◴[] No.24308448{14}[source]
Actually what I don't understand is how you can at the same time:

- estimate that memory safety isn't paramount (you're not alone in this case, and it's usually the kind of discussion that happens over and over on Rust threads).

- and use the “Zig is memory-safe” as a marketing argument.

“Zig isn't memory safe but we think it's not what matters” is something I'm willing to hear even though I usually make a different trade-off, I know a ton of C programmers who are fine with those languages not being memory-safe. But redefining the definition of memory safety[1] so that Zig can fit in, while at the same time arguing that memory safety is just a detail in the grand scheme of things, is just incomprehensible to me.

Also, your writing in this whole thread is full of resentment towards Rust and I don't think your animosity is helpful in any way: among the really tiny group of people who are currently experimenting with Zig there are people who actually love Rust and use it every day, I'm one of them and I know I'm not the only one. Bashing other language in a thread about a language you contribute to isn't the best way to be a welcoming community.

Good day.

[1] “Zig is (will be) memory safe”*

*as soon as you disable some compiler optimizations and use an allocator which is actually a memory-management runtime but not totally a GC because you need to free things yourself and if you make a mistake it will abort your program. Terms and conditions apply.

replies(1): >>24310985 #
29. pron ◴[] No.24310985{15}[source]
1. Safe Zig is(/will be) memory safe. It is not what you deploy, but it is one interesting aspect of Zig's design which makes it very different from "C with ASAN."

2. Nobody's goal is to use a memory-safe language. The goal is a safe and correct program. A memory-safe language is one way towards memory-safe programs, but the belief that this is the most effective way to achieve safe and correct programs is an interesting hypothesis, but it is just that. It is certainly possible that trading off those guarantees for something else -- like increased confidence across the board -- could result in safer, more correct programs, or at least not any worse.

> Also, your writing in this whole thread is full of resentment towards Rust and I don't think your animosity is helpful in any way

If you go over this thread again you will see that this is simply not true. I certainly did not bash Rust once. It is a very cool language with a bright future that many people love, but it just doesn't suit my personal tastes, that's all. And because both Rust and Zig are trying to provide safe low-level programming and they do it radically different ways comparing their approaches to safety is interesting. If my points are incomprehensible to you that might be because it is you who are acting with animosity and resentment that cloud your judgement.