If malloc() fails, there is no need to exit the program completely with exit(), only return from the current function with an error.
If malloc() fails, there is no need to exit the program completely with exit(), only return from the current function with an error.
So it is a good advice for beginners.
In fact I've written a whole interpreter that can recover from OOM by raising a recoverable exception to the user. It's really only because Zig made recovering idiomatic, and I'm not sure I could've done it in another language (maybe Rust but I'd have to rewrite large parts of stdlib to both return an error and take a custom allocator).
I’m not sure when those are becoming stable, but Rust for Linux has been driving a bunch of this work, in my understanding, so that’s helped a lot.
What Zig does is great, but it is not actually comprehensively checking that all allocations are fallible and handled. That's not possible on Linux.
I think it's because the new_uninit_slice call Rust will trigger a panic? Or abort? With little-to-no chance for recovery? (I know little about Rust.)
If so, I can see why someone that someone coming from Rust might consider exit() to be the appropriate solution for C, even for library code which should never be in charge of deciding how a program should exit.
I think the essay could be improved by highlighting the different worldviews.
I'm also old enough that
// Allocate enough room for the string and its null terminator.
char* reversed = malloc(len + 1);
makes me nervous. Even for char -- I've never been on a system where sizeof(char) != 1 -- I want to see the sizeof included in the calculation, like: char* reversed = malloc((len + 1) * sizeof(*reversed))
so I don't have to think about sizeof(char) being special.As long as I'm here, I'm a bit confused about the purpose of the "char* error_message" in the proposed Result. Why a char* vs a const char * or even better, an int with an error code? Who sees the message? Do we expect they know English, or will they be localized? Will the error message text be frozen forever, or might it change in the future?
It’s not any of that. It’s because of overcommit being the default for basically every Linux system. With that, malloc will never fail, and it’s the later access of that memory that will. In practice, you’ll virtually never see malloc actually return a failure, and so most software, no matter the language, is generally not robust to this condition.
$ uname -a
Linux boxcar 7.0.0-31-generic #31~24.04.1-Ubuntu SMP PREEMPT_DYNAMIC Mon Aug 10 09:38:02 UTC 2 x86_64 x86_64 x86_64 GNU/Linux
$ cat tmp.c
#include <stdio.h>
#include <stdlib.h>
int main() {
char *s = malloc(50000000000ULL);
if (s == NULL) {
printf("Boo, hoo!\n");
} else {
printf("Look at all that memory!\n");
}
return 0;
}
$ cc tmp.c
$ ./a.out
Boo, hoo!
As to the lack of robustness of most software, that's a fact. But, for example, Daniel Stenberg of curl fame is a developer of robust software who does not appreciate how Rust handles out-of-memory errors makes it impossible to implement libcurl in the way he expects a library to work. See https://www.youtube.com/watch?v=HFH2vZRTKrA&t=2080s from 4.5 years ago as an example.To get back to the essay, I can easily understand why someone who has Rust as their first-and-only systems language, and therefore expects abort-when-out-of-memory, will not immediately consider how C allows a different approach to how to handle that condition, but instead will try to replicate Rust's behavior in C.
"I can program FORTRAN in any language." :)
Assuming that it has the default overcommit setting, it's based on a heuristic. Currently, that heuristic is something like "is this single allocation larger than all of physical memory + swap", if I remember correctly.
However, most real software does not try to allocate like this. It allocates reasonably small chunks of memory regularly. So again, in realistic conditions, you generally won't see a failure.
> Daniel Stenberg of curl fame is a developer of robust software who does not appreciate how Rust handles out-of-memory errors makes it impossible to implement libcurl in the way he expects a library to work
I am not re-watching this video right now, and of course have a lot of respect for Stenberg, but either he or you misrepresents the situation here: allocation is purely a library concern in Rust, not a language one, and so you can absolutely write your own code that allocates memory however you'd like. This is a concern about Rust's ecosystem, not about the language itself. It is surely not impossible to do this, embedded folks write Rust that doesn't even have an allocator present regularly, let alone infallibly allocate.
> 4.5 years ago
As I mentioned elsewhere in this thread, the standard library types are gaining methods that return Result instead of panic, and have had them for a long time, though they've been nightly only, since it's a pretty niche case.
For one example, my library uses memory-mapped access, which is great for files which push the memory bounds. However, using memory-mapped files, especially over a networked file system, are slower than simply using RAM, so I have an option to read the entire contents into memory. I know the file size, I allocate space, and read in the data. If malloc fails, I report the failure, and let the caller figure out how to resolve things.
I don't think my software is all that special. A well-written library is not supposed to cause the entire program to panic. At the very least, the library doesn't know how to clean up all of the resources opened by the rest of the system. (Especially in a polyglot program where Python with a finally: cleanup calls C++ with a RAII object, then calls Rust.)
Concerning "I am not re-watching this video right now", I linked to the specific timestamp where he talked about it, so you wouldn't need to re-watch the entire video. I did that because the only text version I found was from 2020, at https://github.com/hyperium/hyper/issues/2265#issuecomment-6... , and I wanted something more recent than that.
> and so you can absolutely write your own code that allocates memory however you'd like
The linked-to essay uses new_uninit_slice in Rust, compares it to malloc in C, points out the issues should malloc fail, and how it needs something like an exit() to handle malloc failures.
My observation is that this would be an appropriate time to point out a philosophical difference between the Rust approach, where the default is for Rust to handle OOM errors, and the C approach where you are in charge of checking and handling malloc failures, and how some C library developers prefer different behavior than what new_uninit_slice provides.
Your suggestion that you can absolutely write your own Rust code this purpose also holds for the C code. You can write your own malloc wrapper function which does the check-and-exit() for you, if that fits your needs, then only call "malloc_or_die()".
I think that regularly requesting a single chunk of memory that is likely to be larger than total RAM + total swap in the desktop context is unusual, but it's true this is an opinion.
> The linked-to essay uses new_uninit_slice in Rust
If you wanted to check the allocation, you'd use try_new_uninit_slice, which is the same thing but returning a Result instead of panic-ing.
> where the default is for Rust to handle OOM errors,
Again, it's worth splitting out the language from the library and or culture here: Rust itself never allocates, and so never OOMs.
> also holds for the C code.
I don't mean to imply Rust is better than C in this regard, only that "you can't check for OOM in Rust and therefore it's impossible to build the library how I want" is untrue.
While I am certainly ignorant of Rust, my overall issue is that this statement:
"In order to avoid unhelpful segfaults, the program should check for a null pointer and gracefully exit if one is found:"
The following example shows that "gracefully exit" means a perror() and exit() call, which is not appropriate for a C library.
I presumed this is because the author is someone with Rust experience, and who wants to replicate the expected Rust behavior in C. I think the better alternative is to explain that a well-written C library can have different expectations.
I'll add that the essay has a footnote saying "The corresponding Rust function is not idiomatic, and can only correctly handle ASCII text. If I were writing a production version of this function, it would be a single line of code: forward.chars().rev().collect::<String>()."
I see that production version doesn't handle allocation failures either.
My follow-on presumption is that most Rust libraries use this sort of idiomatic production code, where allocation failures simply panic.
I could well be wrong, but this essay certainly doesn't clearly show that I am wrong in my belief.
> I'll add that the essay has a footnote saying
Honestly I have an entirely different beef with this footnote: `chars` is for codepoints, not really "characters", because what a "character" is in Unicode is very complicated.
> My follow-on presumption is that most Rust libraries use this sort of idiomatic production code, where allocation failures simply panic.
Yes, I wouldn't argue that most Rust code uses these try_ variants, because of the whole overcommit thing. But in contexts where it does matter, people do. The expectations are different based on context. Lots of Rust code expects (and enforces) no allocations at all, but that doesn't mean I'd claim that all Rust code does.
I also think it's fine to want to hold libraries, C or Rust, to a higher standard with regards to allocation failure, but in practice, most people (in both languages) do not find the juice worth the squeeze, and so don't really do it.
I can't speak to the specific implementations, however servicing an allocation request has multiple fallible steps: reserve address space, then confirm/acquire/defer the physical memory backing the allocation.
My understanding is that overcommit allows deferring assigning physical memory to the allocation, however it could still fail to find a chunk of address space to service the allocation request.
For context, steveklabnik suggested that malloc will virtually never fail on basically every Linux system, because of overcommit being the default.
I showed a trivial example of malloc failing, when trying to allocate more space than on my machine.
Another example is when using resource limits. I've modified my code to malloc only 500M bytes, the set a virtual limit of 500000KiB, which works, then 40000KiB, which doesn't
$ grep malloc tmp.c
char *s = malloc(500000000ULL);
$ cc tmp.c
$ ulimit -S -v 500000
$ ./a.out
Look at all that memory!
$ ulimit -S -v 400000
$ ./a.out
Boo, hoo!
I clearly disagree with steveklabnik's because it's easy to demonstrate cases where malloc fails on Linux-based machines."It's hard to handle correctly for beginners" is a reason for beginners to ignore a return value. It's not a reason for the system to (by default) not do the bookkeeping to know this will (likely) fail.
Sparse mapping & reserving address space then deliberately faulting in pages is a good & advanced technique, but it shouldn't share interface with the beginner/default API.
And you never will, since sizeof(char) is guaranteed to always be 1.
I'm guessing you were thinking of CHAR_BIT != 8, but even then I'm not sure it would make a difference since malloc takes its argument size in bytes and a char more or less is a byte in C.
(Consider that char*s are also how you access the byte-level representation of objects in C. If chars were not the minimum addressable unit then that use wouldn't work)
https://smd.hu/Data/Analog/DSP/SHARC/C&C++%20Compiler%20&%20... says the cc21k compiler for ADSP-21xxx DSP systems has char as 32 bits signed, and that the compiler handles ANSI/ISO standard C.
So I don't believe your statement "sizeof(char) is guaranteed to always be 1" is correct.
> chars are also how you access the byte-level representation of objects in C
Where does the spec say that a char can be used to address any point in an object?
There's all sorts of oddities like tagged architectures which the C spec handles which I know essentially nothing about, but which break common expectations about how C works. I believe this is one of them.
I believe the following is undefined behavior in C, even though your compiler may let you do it, at least sometimes, and on modern desktop hardware:
int i = 12345;
char *s = ((char *)&i) + 1;
char c = *s;
I believe the following is the correct (or less incorrect) way to do it: char tmp[sizeof(int)];
memcpy(tmp, &i, sizeof(int));
char c = tmp[1];Yes, but in C standardese a "byte" is not necessarily the 8 bits that it's normally thought to be these days. From C89 Section 2.2.4.2 Numerical Limits [-1]:
> maximum number of bits for smallest object that is not a bit-field (byte) CHAR_BIT 8
i.e., CHAR_BIT is the number of bits in a byte. C23 has a similar definition, and further defines CHAR_WIDTH that is defined to expand to the same value as CHAR_BIT.
> So I don't believe your statement "sizeof(char) is guaranteed to always be 1" is correct.
From C89 section 3.3.3.4 The sizeof operator [0]:
> When applied to an operand that has type char, unsigned char, or signed char, (or a qualified version thereof) the result is 1.
This wording remains basically identical through C23 [1].
> Where does the spec say that a char* can be used to address any point in an object?
From C89 section 3.3 Expressions:
> An object shall have its stored value accessed only by an lvalue that has one of the following types:
> <snip>
> * a character type.
This also remains the case up through C23.
I think you're thinking of the strict aliasing rule with your example, but character types are one of the exceptions to said rule so I think your example is actually fully defined. It'd be UB if you casted to an incompatible type like a float, I believe.
[-1]: https://port70.net/%7Ensz/c/c89/c89-draft.html#2.2.4.2
[0]: https://port70.net/%7Ensz/c/c89/c89-draft.html#3.3.3.4
[1]: https://port70.net/%7Ensz/c/c23/n3220.html#6.5.4.4
So that scaling factor is placed into CHAR_BIT, and would be 64 on that DSP compiler.
> An object shall have its stored value accessed only by an lvalue that has one of the following types:
Yes, my confusion comes down to my confusion of what "byte" means in the C spec.
Thank you for your time in pointing this out.
Bit of a (not so?) fun fact: char* being a universal alias can lead to some potentially unexpected slowdowns [0], especially if the char* bit is behind a typedef.
[0]: https://travisdowns.github.io/blog/2019/08/26/vector-inc.htm...
I just grep'ed my code base and found one counter-example, which I did on Aug 29, 2011.
Where I assumed sizeof(char) was 1. :)