Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Musings on the C Charter (aaronballman.com)
55 points by pjmlp on Jan 2, 2024 | hide | past | favorite | 35 comments


One thing that confuses me about the C committee is their inconsistent attitude towards breaking existing code. For example, in C89-C17, calling realloc with a size of 0 would free the referenced buffer and return a NULL pointer. This helped use cases where realloc was called in a loop on data that changes size. In C23, they changed it so calling realloc with a size of 0 was undefined behavior. This breaks any code that was doing what was previously idiomatic realloc behavior. It's also hard to find in existing codebases because the length passed to realloc will be a variable in most cases.

However, the C committee refused to define NULL as (void *)0 because they were afraid of breaking existing library implementations that define NULL as 0. They instead introduced a new nullptr keyword that's defined as (void *)0.

I just don't understand the inconsistency.


https://sourceware.org/bugzilla/show_bug.cgi?id=12547 contains some info on where this discussion started, basically there is ambiguity in the old interface and rather than trying to rule on a sensible interpretation the standards committee agreed to make the behaviour obscelescent, and then later undefined, as even now at least one BSD seems to refuse to free memory with a size argument of zero.

This was a defect in BSD and the POSIX specification (which literally says it's not intending to contradict the C standard anywhere at the top of every function document), but the committee somehow managed to believe this was cause to deprecate behaviour of realloc() with size zero for 99% of the world. The defect report and associated documents don't seem to be very explicit about the potential impact of the choice to make the behaviour obscelescent in the first place, which might be why this compatibility break was accepted (and eventually completed by making it all undefined).

https://www.open-std.org/jtc1/sc22/wg14/www/docs/summary.htm...

The approach is inconsistent because the committee isn't perfect and every single change to the standard, especially defects, must attempt to best preserve compatibility on a nuanced case-by-case basis, and sometimes when you're in the committee I suspect it's harder to see the wood for the trees. realloc() was broken trying to address a defect report that suggested C's lax definition was potentially a cause of double-frees, a security nightmare, but probably exaggerated in hindsight; and not really made that explicit, although I haven't checked all the minutes and attended the meetings so maybe there was a more conscious decision to deprecate the free-on-zero rule.


There's a little more reasoning in the associated C2x proposal N2464 [0]:

> [DR 400] was resolved by loosening the requirements in the standard to allow for the existing range of implementations and included in C17.

> N2438 Clarification Request requested further clarification [...]

> Discussion at the Ithaca meeting and on the mailing list suggested that a call to realloc with a size of 0 be classified as undefined behavior.

> [snip]

> Classifying a call to realloc with a size of 0 as undefined behavior would allow POSIX to define the otherwise undefined behavior however they please.

[0]: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2464.pdf


Yes but the well was poisoned by marking it for removal / obscelescent in the first place. Making it undefined behaviour is the next logical step after that.


Making realloc with a size 0 be undefined is a way of maintaining existing code, because it means that different implementations kan keep doing what they are doing. Some implementations free the pointer, some create a zero size allocation. Since there are code depending on both behaviours, we cant define a consistent behaviour, without breaking code that depends on another behaviour. This way we acknowledge the issue without breaking anything.

There was a discussion if it should be defined as "platform defined" instead, but the wg14 wanted to indicate that they discourage this use of realloc, and undefined behaviour was a way of doing this.


Making realloc(buff, 0) undefined means that the compiler is permitted to do nothing instead of calling free on buff. Because this both complies with the C23 standard and makes the computer do less work, I won't be surprised if we start seeing compilers applying this optimization. Hopefully we'll at least get a "-frealloc-size-zero-free" compiler flag or something.

BTW, if the "we" implies you're a WG14 member, I wanted to thank you for helping move the language forward. C23 has tons of nice features that makes it compelling to move to (#embed, true/false being keywords, binary literals, built-in endianness macros, etc), which is why I'm concerned about adding more undefined behavior to the standard because some BSD variants didn't comply with the previous standard.


I am in the wg14. But you should not thank me for those features. I'm not very happy with C23. We are working on making UB more understood, and to try to eliminate some UB.

To be clear: BSD was not breaking the standard, the standard didn't spell out what the correct action was. So different people had different interpretations. The standard states that things that aren't explicitly defined are UB, so one interpretation is that it was UB to begin with.


> zero size allocation

Is this useful for anything? I wrote my own memory allocator, is there any reason I might want to support this?


Im not recommending doing this, but you may want to allocate an address to use as a magic number.

NULL is a magic number address that is guaranteed never to be a valid allocation. By allocating an address, you can creat your own magic number, that wont ever be used by any other allocation so you can give it special meaning in your code.


Maybe they care more about breaking library code than user code? Not saying I agree but maybe that explains some of their decisions.

I don't know if changing the realloc() NULL behavior to undefined necessarily breaks code. It definitely leaves the door open to the possibility of breaking code on standard library users but it's still up to the implementation to change the behavior for the code to break.


Oh great. I've got realloc(0) in some code. Here's hoping clang and gcc ignore that.


https://godbolt.org/z/esssnq7fs

Looks like gcc does not take advantage of this, but clang does.


@dang

This post is a duplicate of "Musings on the C Charter" - https://news.ycombinator.com/item?id=38835994

I notice the following:

- Is currently on the first page while the other one with more points and more comments is not, (at least as I write this)

- Did not get marked as a [dupe], something I see marked within seconds for other posts in similar situations.

- Despite being posted 10 hours after the previous one, instead of linking directly to the previous post, become a separate item.

Can you clarify the rules on this? Is it because of human moderation or other rules?


If you check the timestamp on this post, it was submitted before the other one you linked. A mod manually gave this one a second-chance bump to the front page.


What's interesting is that this is still symmetrical, in a sense: When I submitted the other one, it didn't show as a duplicate of this one either.


Thanks. Never noticed the discrepancy. I guess this clarifies it: https://news.ycombinator.com/item?id=37737076


> I think it’s perfectly reasonable to expect “trust me, I know what I’m doing” users to have to step outside of the language to accomplish their goals and use facilities like inline assembly or implementation extensions.

I agree with most of the author's points but this one goes too far. C is the low-level thing you step out into when you're telling the system "trust me" and taking direct control. Asking people to write raw assembly more often instead of using C is a non-starter.

I understand the underlying issue with "trust the programmer" (it's too parallel with "unsafe by default") but a more nuanced solution than "make it so C is 100% locked down" is going to be required. Even Rust has `unsafe` and pointer-fiddling utilities (but I don't see how a similar idea would make sense for C).


Trust the programmer is kind of still with us. Compilers assume the programmer is somehow capable of keeping perfect track of all the many footguns and somehow doesn't trip over them. I don't think that's what it used to mean.

> there should be no invention, without exception

_Generic? The _Atomic mess?

I've been vaguely thinking I need an off ramp from C as it staggers towards a more dangerous and annoying C++ dialect. Currently I think the right play is to write a C99 compiler that doesn't use UB => ruin as an optimising trick that incidentally stomps all over what the programmer thought would happen.


> Trust the programmer is kind of still with us.

Yes, the post isn't saying that it's gone, it is saying that it should be gone, in the future.

> _Generic? The _Atomic mess?

I do not know the story behind these features, and so your point is a bit confusing to me. That said, I think what you're saying is that these are both inventions, and therefore, this principle isn't true. From the post:

> For starters, we’ve shown we’re perfectly comfortable with invention during the C11 and C23 cycles.

If you have any pointers to the story about standardizing these features, I'd love to hear about them though.

> Currently I think the right play is to write a C99 compiler that doesn't use UB => ruin as an optimising trick that incidentally stomps all over what the programmer thought would happen.

I do think that this is a thing that a lot of people want, but I am unsure that it is a thing that is possible, without seriously inhibiting optimizations. And if you're okay with that, you can compile at -O0. But maybe there is some universe in which you could write different optimizations. Only one way to find out! If you did pull that off, I'm sure your compiler would be popular.


Generic and atomic are inventions where the prior art was much better. Function overloading via name mangling is simpler, faster and composes better than generic. Atomic as an operation (the gcc intrinsics) works better than as a type qualifier, especially given the type aliasing -> UB idea. Mostly because atomic is a property of something like "load" or "add", not of a register or an address.

I'm pretty sure whole program optimisation can do the majority of the performance work of the aliasing assumptions. Things like signed integers never overflow are more difficult. My pet theory is that a C compiler that emits machine code that follows a thin layer over the target assembly for semantics will be fast enough for most things C gets used for.


Ah thanks. I have never used _Generic, but I fully agree that "atomics as an operation rather than a type qualifier" is the superior design.


We just need C99 with compiler flags that define the undefined behavior. Basically just turn off strict aliasing and force the compiler to assume 2's complement as the signed integer representation. The -fno-strict-aliasing and -fwrapv flags fix a lot of problems.

There's been some pretty good quality of life features introduced by recent standards. Just backport them to C99 or something.


well c++ adds smart pointer etc and it's still memory unsafe, I don't know how else c can be made memory safe without a GC.

the UB part should be limited to the minimal set, now there are too many cases to cause UB.


Already making it as safe as Modula-2 (1978) would be a great improvement.

- strong typed enumerations

- proper arrays with bounds checking (or fat pointers)

- proper strings with bounds checking (could be a library like SDS)

- memory allocation via types instead of sizeof based math

This alone would already fix quite a few problems.

Apple has such Safe C dialect they use for iBoot firmware.


Annex.K also does safe-c but not adopted by the committee.


Has this site been hacked? I get redirected to: https://blockpg-dock.safezone.mcafee.com/


Try with http: http://blog.aaronballman.com/2023/12/musings-on-the-c-charte...

For me the original https link fails with "SSL received a record that exceeded the maximum permissible length. Error code: SSL_ERROR_RX_RECORD_TOO_LONG"

The http link redirected to safebrowse.io/warn.html where there is a "Proceed Anyway" link to get to the content.


No, but maybe your networking team is giving a hint to stay away from unsafe languages...


> Existing code is important, existing implementations are not.

I am having trouble parsing that sentence.... so now there's a distinction between the code and the implementation? wat!?


HN seems to think this is a dumb question but I disagree, and I think the people replying are misunderstanding what Aaron meant. You're correct, it's not phrased very clearly. Aaron is getting at the idea that most code is effectively documentation about business logic, but a small amount of code is about making a platform-specific library or executable: this code is an implementation detail that's less important.

If you look at the work involved in creating a C89 program that won't compile in modern C, only a tiny fraction of that work involves anything specific to C89. It's almost all about solving some real-world problem in C generally, with the details about C23 compliance being an implementation detail. Aaron seems to view porting a non-compliant C program as loosely equivalent to a security bugfix that changes some low-level details of certain implementations, but doesn't have much impact on the overall code. It might be annoying to fix your K&R declarations / etc, but it won't undo the hard work you did in parsing HTTP headers.

An extreme but illustrative example of this idea: Donald Knuth still maintains TeX in WEB, which is a dialect of Pascal84. He hasn't moved to CWEB or a fancier modern language. I think he's correct: although Pascal84 is fussy and old-fashioned, it is also completely specified, fully stable, and uses a generic ALGOL syntax widely applicable to many modern languages. So the work of translating Pascal84 to modern C can be done with a computer (web2c): TeX's actual WEB source code serves as a complete specification that can easily be translated to a specific implementation. The WEB code is important, but the 1/2/2024 binary output from web2c > gcc is an implementation which is not very important.


I find this interpretation interesting, because I would agree with your sibling commentors, though I also agree with you that this isn't a dumb question. I think that you are answering "this is what Aaron wants to change," whereas others are answering "this is what the current principle means." That is, I think the principle is about compilers and their implementations vs code that users write. The full text from the Charter (covered in the post but not in your parents' question):

> 1. Existing code is important, existing implementations are not. A large body of C code exists of considerable commercial value. Every attempt has been made to ensure that the bulk of this code will be acceptable to any implementation conforming to the Standard. The Committee did not want to force most programmers to modify their C programs just to have them accepted by a conforming translator.

>

> On the other hand, no one implementation was held up as the exemplar by which to define C: It is assumed that all existing implementations must change somewhat to conform to the Standard.

This reads very clearly to me. However,

> Aaron seems to view porting a non-compliant C program as loosely equivalent to a security bugfix that changes some low-level details of certain implementations, but doesn't have much impact on the overall code. It might be annoying to fix your K&R declarations / etc, but it won't undo the hard work you did in parsing HTTP headers.

I do think you're getting at something here, which is the change Aaron is advocating for. I think it's a bit more subtle than that though: most people think of C as being 100% backwards compatible. But that's not really true. Famously, for example, C++ removed the gets function outright. I think Aaron isn't arguing that backwards compatibility should be thrown out entirely, but that the committee should build in a formal process for deprecation and eventual removal, and that that is better than the status quo. And that this power should be used wisely, not making changes that would be a burden on users for little gain. Just that it is okay to give users a little burden for big gains, rather than the current status of "basically no burden at all, but also improvements are difficult."


Quick comment, I just made a dumb mistake - I had read the post last night when it was posted and came back to the discussion in the comments. I forgot which parts where the charter vs. which parts were Aaron's comments!

So I was summarizing Aaron's gist, and in particular his comments about wanting to avoid a Python 2/3 schism. This was a case of "big burden for big gains," the burden being so large that developers didn't want to harm their hard-earned business logic implementation because 2to3 missed an edge case in a dict comparison.


Ah ha! It's all good, happens to the best of us.


"implementation" here means an implementation of the C standard (ergo a compiler toolchain). The full text is this:

> 1. Existing code is important, existing implementations are not. A large body of C code exists of considerable commercial value. Every attempt has been made to ensure that the bulk of this code will be acceptable to any implementation conforming to the Standard. The Committee did not want to force most programmers to modify their C programs just to have them accepted by a conforming translator.


"Code" refers to whatever software written in C. "Implementations" refers to implementations of the language - C compilers and runtimes.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: