Hacker News new | ask | show | jobs
by Panzerschrek 9 days ago
> Resource allocation may fail

Yes it can. But it's nearly impossible to handle such cases properly. That's why checking each allocation manually is a bad idea. Other languages do this better - they provide nice abstractions, but if something fails, the language runtime terminates the process. The result is the same, but has much less friction.

Also on some systems (like Linux) memory allocation may not fail, but the "allocated" memory may not be available and a program can crash accessing this memory.

4 comments

Zig is in the space of languages where an abstraction that decides that memory allocations are irrecoverable is not good enough.

If you work in an environment where memory allocations can't fail or can't be handled if they fail, you might not want to use Zig, or C for that matter. Not every language should be designed to live in the space of "somehow low level but also a good choice for your basic web backend", like Rust.

Rust doesn't stop you from checking if memory allocation has failed. Its libstd provides many operations that don't bother to surface memory allocation failure (for the reasons given above), but that's why Rust provides a libcore that does no allocation, while continually working to push more things down from libstd into libcore, while providing alternative APIs in libstd to let you handle allocation failure if you know you actually need to.
Conversely, Rust doesn't force you to explicitly handle if memory allocation failures. The least you can do in Zig is explicitly ignore allocation failures.
You can certainly write data structures in Zig that swallow allocation failures rather than surfacing them. We're talking about library-level concerns, not language-level concerns. Both Rust and Zig give you the power to allocate raw memory and handle the result of that syscall however you want, it's the standard libraries that differ beyond that point.
Yes, it is a standard library difference. But unless you plan on rewriting the entire Rust ecosystem, you're going to be dealing with invisible failure points in Rust code that you would not be dealing with when writing the equivalent Zig code. The standard library is almost as fundamentally important to a language as it's built-in operators.

And Zig does have language-level features that combine to require you to explicitly handle propagated errors (error sets, requiring all return values be explicitly handled, requiring all switch cases be explicitly handled). Rust has a similar set of features (pattern matching, requiring all match cases be explicitly handled), but does not use them for allocation (Box::new, vec![], etc. does not return a Result value).

> But unless you plan on rewriting the entire Rust ecosystem, you're going to be dealing with invisible failure points in Rust code that you would not be dealing with when writing the equivalent Zig code.

That's what Rust's libcore is for. And the converse is that if your software is written for a system with overcommit--so every typical OS and distro these days--any error path having to do with memory allocation failure is impossible to trigger, because the OS won't honestly tell you if allocation would fail.

> And Zig does have language-level features that combine to require you to explicitly handle propagated errors

Of course, and Zig also doesn't stop you from writing a data structure that papers over allocation failures by using `std.process.exit` in the error path. Zig also discourages third-party dependencies more than Rust does, so I wouldn't be surprised if people are already wisely doing this when writing their code for programs targeting systems with overcommit.

> If you work in an environment where memory allocations can't fail or can't be handled if they fail, you might not want to use Zig,

It's most of environments. Basically any program running under a modern OS. So, why do this language exists, if its practical applicability is so small?

This language exists so you can reuse the same code in environments where memory allocations may fail, and where memory allocations can't fail.

Let's say you write an application that runs as a Unix daemon in Zig. Later you may decide that your application is really the only thing you're interested in running on the target machine, and for performance and predictability reasons, you'd prefer to boot directly to your application, instead of to an OS that launches your daemon. You can just swap out the implementation of the std.Io runtime for one that targets the hardware directly, instead of a Unix. You don't have to make any changes to your application.

That's kind of an extreme case, but it's the kind of flexibility Zig provides.

> This language exists so you can reuse the same code in environments where memory allocations may fail, and where memory allocations can't fail.

In my hypothetical example of a language where allocation fails aren't exposed it's possible too. An allocation fail just triggers a full system reboot.

On modern OSs you can write Zig and just ignore allocation errors. It doesn't force you to handle them properly.

This language exists to supercede or supplement C, not JavaScript or C#.

It's practical applicability is similar to that of C, so I struggle to comprehend how it is "so small".

> On modern OSs you can write Zig and just ignore allocation errors.

I can ignore errors, but I still need to free memory manually if I want to avoid memory leaks. Languages like C++ or Rust have destructors, which do the job for me.

> This language exists to supercede or supplement C

There are way better alternatives, like Rust. Even C++ is better.

Zig has defer, your point is quite invalid.
defer can be forgotten to be written. C++ always calls destructors for local variables without additional programmer's intervention needed.
You don’t know there’s still millions of lines of code being written for environments with no OS or much more limited OS than what’s on modern desktops/laptops/servers?
Several things:

* There are many useful ways to handle it properly, and your choice depends on your program's constraints. The very small amount of friction (once you're used to it) encourages you to consider what ways to handle it are viable, such as allocating all memory at startup.

* If your strategy is to crash immediately, there is very little additional friction but you get the benefit of it being obvious in your code that this is the case.

* There are environments where memory allocation fails immediately, including if you turn off over-commit on Linux. If your hardware is dedicated to running a high reliability system, configuring it in this way is reasonable.

* Memory is not the only resource. Indeed, removing the special call out is what changed here. That different resources are handled with the same mechanism (errors, instead of eg returning null from malloc) is good.

Autors of Zig did a choice for me. I can't use RAII in it. In C++ it's better - when I don't care, I just use standard library containers, when I do care, I can bypass them.
You might have a limited budget per incoming request in a http server for example. Then you want all of the code that http handler calls to be able to handle an error of type something like OutOfMemory.

This is a very good ability to have in an application like a database.

Such things should be solved in more nice way, not by manually checking every allocation. Like via lightweight threads, with each of them having their own memory and a scheduler, which kills threads exceeding their memory limit.
This compilicates things more compared to functions returning Result<>.

You also get locality benefits from bump style allocators. And you don't need SmallVec or similar optimization containers. And you also avoid mental overhead of managing many allocations in your head (or using a language with borrow checking).

> This compilicates things more compared to functions returning Result<>.

Checking each Result in a large program is more complicated than having a runtime library handling memory limits properly.

> And you also avoid mental overhead of managing many allocations in your head

That's exactly what languages like Zig force you to do, compared to something like C++ or Rust.

> or using a language with borrow checking

borrow checking gives memory safety. It's also important to have.

Nearly impossible in some contexts, where the trade-off makes sense.

There are many scenarios, especially in embedded systems, where it can happen and you want to handle it robustly, e.g. by evicting a cache or flushing a buffer to disk.

In embedded systems you have enough control. But as soon as an OS is involved, you have much less control. Basically an OS may do with your process whatever it wants, but it happens to be polite most of the time.