Rendered at 21:39:54 GMT+0000 (Coordinated Universal Time) with Cloudflare Workers.
uecker 2 days ago [-]
Any tutorial for C should explain compiler warnings and other tools that help make code safer. In C, you do not rely on the language specification but on tooling. Especially for Rust programmers, this needs to be explained more explicitly. And of course, you can build abstractions using types in C. So a tutorial should also focus on that, and perhaps not start with a low-level string reversal function.
swinglock 2 days ago [-]
Absolutely. Turn the warnings into errors too, build and test with sanitizers (ASAN, UBSAN, TSAN) when not measuring performance, and use static analysis beyond compiler warnings (Clang Tidy, Clang Static Analyzer).
Though the author doesn't look to be trying to write a great tutorial, rather prioritizing sharing what and how they learned something from their perspective.
tialaramex 2 days ago [-]
> Turn the warnings into errors too,
Please stop smashing all the nuance out of compiler diagnostics this way.
One of the biggest successes of Rust has been its great diagnostics and I can assure you that it would not help to smash my Clippy lint suggesting that what I wrote looks a lot like an implementation of the addition operator† into a fatal error like the one I get for forgetting to initialize a variable.
Rust even has a (begins empty) diagnostic category [named "expect"] for "This warning should be here" which will flag cases where a notable thing not only might happen here and if it does we can ignore that, but if it's no longer detected that is itself suspicious and should be diagnosed.
† Yes it does Clippy, and I considered implementing Add but I had a good reason not to, so here is a suppression annotation.
> This is not a substitute for a proper C tutorial
kingforaday 2 days ago [-]
I'm with you. Since the post already shows Clang catching the array-decay bug, the author could even just add a “always build with -Wall -Wextra -fsanitize=address,undefined” note and some explanation of course.
C got lint in 1979, and is incredible how much advocacy is still required for folks to use the tooling their compilers offer out of the box.
This problem isn't even C specific, other ecosystems suffer from similar adoption issues.
_ph_ 1 days ago [-]
Which, to be provocative, begs the question: why isn't this tooling part of the compiler, that is, why aren't the diagnostics always produced by default, or even considered errors?
Someone 1 days ago [-]
One reason is that there is no such thing as the compiler. C ‘always’ had many different implementations.
Requiring all of them to adopt common linting rules at the same time is infeasible.
“The type-checking rules also require that, in structure references, the left operand of the -> be a pointer to structure”
and mentions warnings on such statements as
int i 1;
i =- 1;
Modern C compilers also warn way more often about unreachable code, pointer incompatibilities, etc. than those from the 1970s
pjmlp 1 days ago [-]
Because of developer culture mostly.
In some circles having the compiler dictate the way, and only true way, is seen as straightjacket programming.
uecker 1 days ago [-]
The most push back we get against making the language or compiler stricter is not for this reason though, it is mostly that the maintenance burden for existing code is considered to high.
But this illustrates how absurd the discussion is nowadays. Incrementally making things stricter and fixing warnings along the way is one of the most cost efficient way to improve quality and safety. Rewriting the software one of the most expensive. So if the former is rejected because the burden is too high, then obviously the overall aim to make things safer can not be too important and the push for rewrites must partially be motivated by other interests.
pjmlp 1 days ago [-]
I saw this talk yesterday,
"The Power of Ten: Rules for Safety Critical Coding by Gerard Holzmann"
At a moment he mentions a trick he played on JPL folks, his static analysis tool, instead of showing all the issues found, would only display the top 10, without telling them there were actually more than 10.
So they felt motivated to fix them, it were only 10 after all.
Naturally eventually they became aware of the trick.
daymanstep 1 days ago [-]
Changing defaults has a tendency to break people's workflows e.g. for scripts that expect a fixed format output.
_ph_ 1 days ago [-]
Sure, but looking at C's age, the language was changed significantly over time. With one of those changes, certain things could have been considered compile errors, not optional tooling.
Or if you turn your argument around: if people depend on workflows and automation (I do of course too), then this is a good reason to standardize the diagnostics and errors of the checking tools to the point where the workflows and automation can depend on them. Otherwise they will remain tools which are completely optional to run, or even worse, are not part of your normal work flow.
pjmlp 1 days ago [-]
ISO languages don't define tooling, so the only way would be to introduce new semantics.
However, like in any ISO languages, that only happens if enough people vote for it, and someone is willing to submit a paper in first place.
uecker 1 days ago [-]
Why "could have"? Compilers have a lot more warnings than the used to, and also defaults got more strict.
gerdesj 2 days ago [-]
A Rust programmer, programming in C is a C programmer!
Tribalism is sooo ... sadly now.
Please don't.
xtajv 2 days ago [-]
If it's any consolation, I figured that the author was using "Rust programmer" as a shorthand way to say "I know Rust best and that tends to be my point of comparison when learning other programming languages. But there's no particular loyalty there, and it's not like Rust is the only language I know. It's just the systems language that I already tend to think in".
uecker 1 days ago [-]
I don't see any tribalism here, the original article addressed Rust programmers looking into C (so not currently C programmers).
pjmlp 1 days ago [-]
It has always been like that in computing, starting on school playground about which 8 bit home computer each kid had.
jminnl 2 days ago [-]
Asan!
deknos 2 days ago [-]
build one. i would verify gladyly
phamilton 2 days ago [-]
Re: arrays are pointers.
One of my favorite things to show just how bare this is in C is to show array access commutativity.
char c = {1,2,3}
c[1] == *(c + 1)
*(c + 1) == *(1 + c)
c[1] == 1[c]
C is wonderfully simple at times.
pwdisswordfishq 2 days ago [-]
I have known about this for quite some time, and the more I think about it, the more useless it seems. Sure, the underlying machine operation is just addition, which is indeed commutative, but at the type system level, the pointer and the offset have distinct roles. It just makes no sense to allow to commute them, just like it makes no sense to allow to commute arguments to, say, strchr, just because the compiler can figure it out by looking at the types. When was the last time you had a practical reason to write "offset + pointer" or "index[array]"?
In most languages, the indexing operator is not commutative. In Rust, pointer offseting is expressed as a function call or a method, also not commutative. I have never seen a single complaint about either. It's not something people want or care about, it's just a tedious detail.
skydhash 2 days ago [-]
> It's not something people want or care about, it's just a tedious detail.
It’s not something idiomatic, but indexing in C is syntactic sugar. Not sure why they allow it in the syntax, but forgetting that arrays are pointers and not special type is just asking for bugs.
im3w1l 2 days ago [-]
Arrays are not pointers in c. Though they are very similar and will implicitly convert, there are differences. The big and obvious difference is that declaring an array will allocate space for it. In a function, on the stack. In a struct, inline. They have different sizeof. I think there are also some other stuff I can't remember.
ynik 2 days ago [-]
In C there's only two operations you can do with arrays that do not decay to a pointer: `sizeof(array_var)` and `&array_var`.
The latter produces a rarely-seen "pointer to array", i.e. a type like `int (*)[10]` (pointer-to-array syntax works like pointer-to-function syntax).
uecker 2 days ago [-]
Luckily I start to see such pointers used more often, because such pointers can be used for bounds checking because the size is preserved in the type.
creata 2 days ago [-]
As fun as it is, 1[c] will be "marked obsolete" in C29 according to Wikipedia.
avadodin 2 days ago [-]
C11 is last C as far as I'm concerned.
I don't mind adding new features in committees but if they are going to remove compatibility, they should rename their language to nuC or CantbelieveitsnotRust(yet).
No offense intended to Rust which is a fine language.
teo_zero 2 days ago [-]
If C was not supposed to be changed to correct its past mistakes, we would still increment with a=+1 instead of a+=1, or we would use something called nunununuC.
> C11 is last C as far as I'm concerned.
Are you sure? I think C23 brought so many enhancements it's a pity to ignore it.
xtajv 2 days ago [-]
> we would use something called nunununuC
"C++", perhaps? :)
xtajv 2 days ago [-]
I'm with you there.
> if they are going to remove compatibility
C as a language has one job, and it is to not do this.
pjmlp 1 days ago [-]
So you still code in K&R C, I reckon.
avadodin 1 days ago [-]
There is such a thing as legacy code. I don't know why they were writing K&R style definitions in what must have been the late nineties but they were. It was standard C after all.
skydhash 2 days ago [-]
> C is wonderfully simple at times.
Yesterday, I watch a quick video[0] where Matthew Butterick was comparing book sizes and their appeal. “The C Programming Language” was my second programming book (after one about JavaScript 1.x) and I still remember it fondly. Easy to start with (with CodeBlocks on Windows and gcc on Linux) and the concepts were nicely explained. The book were also very nice.
All fun and games with arrays being pointers, until you declare an array in a function and return it.
jackling 2 days ago [-]
Isn't this easily catchable with static analysis, -Werror -Wall? I've never had a practical problem with this.
Also arrays aren't pointers in C, they decay into pointers. You can see this since sizeof will work differently in the function that instantiates the array versus one that takes in the pointer as a parameter.
Ygg2 2 days ago [-]
Isn't Lisp even more bare bones?
At end of day minimalism is a neat but not decisive feature. If minimalism was decisive we'd all be writing Brainfuck.
brabel 2 days ago [-]
Lisp minimalism is very different. It assumes a runtime with automatic memory management for example, even if the language concepts themselves are minimal, especially in Scheme, it has almost no syntax and just a few core facilities on top of which everything else is built, which are closely related to the Lambda calculus, kind of ignoring completely what real computers actually look like.
In C the minimalism comes from providing only the minimal set of things that are available in most (maybe all) architectures (the Von Neumann paradigm), like linear memory, a simple function calling convention, close mapping to Assembly operations etc.
But C syntax is not very minimalist compared to Lisp , let alone Forth. The fact that C syntax became prevalent in the programming world seems to be mostly an accident to me, it’s not objectively better than those minimalist languages’ or Pascal’s, Prolog, ML families.
WalterBright 2 days ago [-]
> providing only the minimal set of things that are available in most (maybe all) architectures
The rise of C in the 1980s induced CPU makers to design instructions that cater to C semantics.
xtajv 2 days ago [-]
That is because C already worked everywhere even before CPU makers agreed on things like making pointers and words the same size.
Edit: And by "worked everywhere" I mean C did the heavy lifting of being a programming language that was even a little bit portable across architectures.
pjmlp 1 days ago [-]
PL/I had more knobs for portability, but neither Multics, nor IBM Oses were available for free.
uecker 2 days ago [-]
I don't think this is really true. What instructions would this be?
In contrast, Intel introduced a lot of instruction for PASCAL, e.g. enter / leave etc. which are basically unused.
He says Leave is often used by compilers, but not Enter.
WalterBright 1 days ago [-]
Instructions like AAA (Ascii Adjust for Addition) on the 8088. The 8087's stack architecture. XLAT, STOS, etc. Asymmetries in how the registers work. ENTER/LEAVE. Kludge instruction sets like the Z80.
An instruction set ideally suited for C is the PDP-11.
uecker 6 hours ago [-]
Why would AAA be an instruction made for C? I don't see this. Or 8087's stack architecture? Or xlat? ENTER/LEAVE is also a counter example where CPU vendors added instructions for other languages than C.
WalterBright 4 hours ago [-]
Those instructions preceded the rise of C. CPU makers at the time thought Pascal was going to dominate, hence ENTER/LEAVE.
Ygg2 2 days ago [-]
> In C the minimalism comes from providing only the minimal set of things that are available in most (maybe all) architectures
By that logic Brainfuck is even more minimal. Again. I'm saying minimalism isn't the goal. It's a good quality but not most important one.
brabel 2 days ago [-]
You just misinterpreted what I said. I was referring to a minimum common denominator of cpu architectures. Brainfuck has nothing to do with that.
Ygg2 23 hours ago [-]
Brainfuck has a even more minimal common denominator. It's just increment pointer, jump to address, read/write. It's basically just a minimal turing machine that ANY architecture will run.
brabel 14 hours ago [-]
Again: you're defining minimum wrong here! It's not minimum in the sense of using one instruction (which is possible, see "mov"). A minimum common denominator is NOT that, see https://en.wikipedia.org/wiki/Lowest_common_denominator (I am not a native speaker, apparently it's normally said "Lowest Common Denominator" in English, in my language it's "mínimo múltiplo comum", so sorry for the confusion).
For example, the LCD of 4 and 3 is 12, that's the "minimum" number they have as a common multiplicator.... it's not 0, even though both multiplied by 0 equal 0. In more concrete terms for C: C is more like the maximum number of things that intersect across all different "common architectures". You want to have most things that everyone supports, not the lowest number of things possible! Hope this clarifies what I mean.
Ygg2 12 hours ago [-]
Again, I understand what you meant to say. But I don't understand what part of Brainfuck is unportable while C is?
> In C the minimalism comes from providing only the minimal set of things that are available in most (maybe all) architectures (the Von Neumann paradigm), like linear memory, a simple function calling convention, close mapping to Assembly operations etc.
This is beautifully illustrated in the old classic The C Companion by Allen Holub which overviews a simple abstract demo architecture and code generated for it. This is a small book (in the vein of K&R C) but contains enough illuminating info. for a programmer.
KerrAvon 2 days ago [-]
I would add on top of brabel's post that C is surprisingly difficult to parse correctly (C++ notoriously so).
Zoom out a little: C is a lot like Unix: simple probably isn't the right word; `underengineered` comes to mind. Which leads to complexity, as you need to make things work in the real world. And so Unix syscalls being designed in the early 1970's for a PDP-11 don't really map to modern needs. And so every unanalyzed complex C app has memory leaks.
To be clear, I like C, I like C++, I like Objective-C, I like Swift; I'm comfortable in all of them. But, in 2026, I don't see why for native code everyone shouldn't be programming in Rust / Swift / other modern memory-safe flavor for any new production use. Zig if you want faster compile times, I guess.
uecker 2 days ago [-]
It is "underengineered" perhaps if you believe it is the job of the language to prevent memory leaks (which Rust also doesn't). IMHO it is just fine - and in fact better - if the language is small and analysis is done by a separate tool.
I also do not see why the simplicity of C should lead to complexity. I have build complex systems in C. I did this using C++ in the past, but switched to C because I found out that all the complexity of C++ was a huge distraction and not something that helps me design better systems. C++ also came with a lot of claims that all the features are absolutely needed to build good abstractions and that this is not possible in C, something I found to be completely false. (And then, a lot of complex and still extremely reliable software I use daily is build in C. My practical experience completely contradicts the myth propagated nowadays by some that all C is inherently bad and unreliable . In my experience it is the exact opposite: The C software I use is in fact the most reliable.)
My view is that Linux and free software is still going strong, because it managed to largely keep out the unnecessary complexity out of the core infrastructure. With Rust and AI, I worry that now get a lot of overly complex infrastructure and that it will be hard to ever get this complexity under control again.
skydhash 2 days ago [-]
> But, in 2026, I don't see why for native code everyone shouldn't be programming in Rust / Swift / other modern memory-safe flavor for any new production use. Zig if you want faster compile times, I guess.
Because C is very simple (unlike Rust) and works everywhere. Zig is not yet stable. Go and Swift are under the governance of tech companies.
The true appeal of C for me is the standard and how it applies only to the language. You can easily take a project from 2 decades ago and port it to a current platform. Lot of current ecosystem is way too fussy about tooling to do this.
tialaramex 2 days ago [-]
> Because C is very simple (unlike Rust) and works everywhere
That's definitely true. C is a "Worse is better" language and as such it's everywhere. You can knock together a halfway usable C for some crap hardware in a few weeks and then the hardware is saleable because there's a C implementation.
> You can easily take a project from 2 decades ago and port it to a current platform.
Much of the software I wrote two decades ago in C won't even build today. Good luck figuring out why, periodically I try to figure out which weird 2000s era Mac OS hacks don't like a 2026 Linux system and eventually I give up and write modern software instead.
In theory C written in 2006 definitely "just works" on a 2026 Linux machine but in practice real world C is broken and even though I (co)wrote it I don't know why. Also in practice the first serious Rust project I wrote in 2021 I just found it, checked out the oldest working version ("first rough working code" says the log) from git, cargo run, works as expected.
Maybe it's because the C was four times older, but I think it's because in the real world you don't end up writing that mythical portable C code too often.
fastaguy88 2 days ago [-]
I have 'C' code that was written in 1985 that runs today, mostly after moving function parameters from inside the function to inside the argument list.
'C' seems to be a language that makes it easy to write incomprehensible statements, but it's not that hard (or it was not that hard) to write code that is trivial bring up to date. Perhaps it was easier for me because I started with Fortran.
pjmlp 1 days ago [-]
Only if it targeted UNIX in first place, or was only making use of standard library.
I am quite sure my MS-DOS C code is going to have some hard time being compiled on GCC 16.
msla 2 days ago [-]
I will never understand why some people put C in quotes.
Do you also put Java in quotes? Rust?
fithisux 2 days ago [-]
Very common.
lelanthran 2 days ago [-]
> Maybe it's because the C was four times older, but I think it's because in the real world you don't end up writing that mythical portable C code too often.
It's because C, over time, went from a bunch of almost compatible implementations, to a standard that differed a little from every existing implementation, to a standard that is updated every decade or so, which is then implemented by a bunch of different products, with enough ambiguity in the standard (UB) that there will be differences between compilers and versions of compilers.
Rust is a single implementation. Always has been.
It's a single product, so you code to the product. The product has no external forcing function, like a language standard, that produces changes every decade. When you write Rust, you aren't thinking "Wait, lets make sure this works on the Watcom compiler too".
If Rust code cannot compile 30 years later, that's a failure on the language, because the single implementation is in full control of compatibility. If C code cannot compile 30 years later, that's not a failure of the language, because the implementation in 30 years has had external pressure forcing changes.
People often forget that Rust is a product, C is a specification.
tialaramex 2 days ago [-]
> a standard that is updated every decade or so
In practice the cadence is exactly half that of WG21's breakneck C++ pace. Every six years. C11, C17, C23 so far and C2y is expected to land before the end of this decade.
But anyway, all I can do is report the observable fact. If you want to say it's because GCC 15 is a different product from GCC 4 whereas rustc 1.99 is the same product as rustc 1.49 then I guess you can re-phrase my position as "In C you will keep needing new products and that sucks" if you like.
pjmlp 1 days ago [-]
In theory, in practice many C developers only know "My C Compiler standard", and are usually surprised that some of their expectations aren't even part of ISO C PDF specification.
Then they get irritated that not all C compiler vendors have the same language extensions or implementation defined behaviours they got used to rely on.
jstimpfle 1 days ago [-]
True. In C, it's hard to know what's defined where and what dependencies one is even using unknowingly. The only help is to build on multiple platforms from early on, this will reduce the pain of porting to new platforms later.
jstimpfle 2 days ago [-]
It's _very_ easy to write C that is impossible to port to other platforms or other compilers. The reason is that more than for most other ecosystems, there are many different implementations with their own implementations and quirks.
It's also easy to write C that won't build on the system it was originally developed on, a few years down the line. But that is true for almost every other language ecosystem. I would say, C makes it _possible_ to write code in such a way that it works on many different platforms and compilers, and/or can continue to work in decades to come with relatively little maintenance.
The latter isn't true for most other ecosystems, because to get anything done in them, you need to rely more on that language and its ecosystem -- specific language features, specific libraries. C isn't a glue code language. It's very good at letting you create your own thing.
To do it well though, requires a lot of care and expertise.
creata 2 days ago [-]
> more than for most other ecosystems, there are many different implementations with their own implementations and quirks.
Is that the reason? I thought it was primarily because C stupidly* makes a ton of platform-specific things more convenient than the platform-independent equivalent - the width of integer types, locales, etc., all vary by platform, and it's easy to accidentally depend on them.
* Maybe it wasn't stupid at the time when C was developed, but it's a bad choice now.
jstimpfle 1 days ago [-]
Integer widths are a problem, but hardly the biggest headache (unless it's a decade old preexisting project that was never designed with portability in mind). Fixed width integer types like int32_t have existed for decades. Even Rust has usize which is machine dependent, and pointers as well have to be machine dependent of course.
Much bigger problem is understanding the scope of dependencies. Dependencies can go bad and you need to update. A dependency might not be available on some new platform so you might have to replace with something else on that platform. To make the software portable, good modularity is required. This is to a large extent an aspect of software architecture. Even Rust can't magically make this happen, if you depend on 300 crates that's probably not a great place to be in either if you want to be portable and maintainable.
There is probably a point that C's flat namespace is a major contributor to badly designed software, because people aren't aware of the dependencies they're mixing all the time. Also C encourages transitive includes, leaking implementation details to the user instead of just to the compiler. (But btw. I find C++ and Rust namespace to be unergonomic syntax-wise, and what's needed is not actually namespaces but control over visibility).
tialaramex 1 days ago [-]
Although int32_t exists:
* Programmers are lazy, they're going to type int more often than they should
* All these fancy modern "sized" types are actually just aliases for C's built-in types like int chosen to match the sizes people want...
* ...but C's integer promotion and silent rules about conversions work only on "real" C types you were trying hard not to think about and all the rules for those types, including types you never mentioned but were using because of promotion, apply to your program. You can write code which looks like it's all unsigned, but oops, under the hood a promotion means a signed integer came into existence, then it overflowed, now you have Undefined Behaviour.
I am interested to hear what you don't like about the namespace rules, in Rust in particular.
jstimpfle 24 hours ago [-]
True that integer promotion rules are defined in terms of the machine dependent unsized "real" integer types. That is indeed annoying. I guess I've found ways to cope. Good thing is, those promotions never cast down, they only widen types.
The point of namespaces is exactly to allow less qualified names, to allow spending less energy when creating new names, by having the compiler always add a qualification implicitly. However, that doesn't solve the problem with name collisions when reading or grepping the source code -- it actually makes it much worse. And avoiding those name collisions, i.e. what's in the source literally, what the programmer reads, and what less-than-perfect tools see, is at least as important as avoiding "semantic" name collisions and being able to compile the code.
Namespaces not only encourage many more literal collisions -- now a single name can be available in various shortenings, and even under different aliases, which also makes it hard to tell that two symbols refer to the same thing!
I've found that the only way to get names right is to find a good scheme, to find meaningful good prefixes, suffixes, and abbreviations, and to come up with a system to create new names more mechanically but still keep them as short and readable as possible.
tialaramex 23 hours ago [-]
I think I mostly agree with you about what namespaces should do for us, but somehow I end up liking Rust's choices and you didn't. Interesting.
I like that core::cmp::Ordering and core::sync::atomic::Ordering exist because in their contexts I think these types should both be named Ordering and so it would be annoying if they were named Ordering and AtomicOrdering or something. If I'm writing code which does tricky atomic operations I can bring in core::sync::atomic::Ordering and now Ordering means that one, whereas if I'm writing code about whether this Goose is before or after that Goose, I ask for core::cmp::Ordering to get the relevant Ordering.
And if it's unclear in context, I can write core::sync::atomic::Ordering and everybody knows which one I meant.
I don't usually use an IDE but I believe it's usual in an IDE to tell you the full names of such things and even to help you import relevant stuff based on completions.
jstimpfle 22 hours ago [-]
I find both of these names too long and the colons are noisy. The fact that I can refer to them using different qualifications makes me uneasy. For both we could find better names, e.g. MemoryOrder is already an established term.
I've realized that naming is an art that is difficult to master but makes a huge difference when gotten right. It consists of not just combining names into compounds but also finding new shorter names that are ergonomic and distinguishable. For example, I've replaced the compound "NodeIntersectTest" in my current project with simply "Probe", which is much easier to read and write, easy to distinguish from other terms after learning its meaning once (it is a central concept to my app), and also perfect for log messages and stats where brevity matters a lot.
A two letter prefix for all identifiers in a project or company codebase is a good idea, but I try to avoid
hierarchies and finer granularity, and generally to be overspecific, in order to keep things easier to change later.
1 days ago [-]
jhgb 2 days ago [-]
> But, in 2026, I don't see why for native code everyone shouldn't be programming in Rust / Swift / other modern memory-safe flavor for any new production use.
The lack of OpenMP for Rust etc. seems like the most obvious reason for "why use C instead of Rust in 2026" to me.
I don't know what you're implying - I asked a genuine question: what is impressive about that syntax.
cephi 2 days ago [-]
It's not impressive, it's just a fun thing to show people who learned traditionally and never thought to question the commutativity of the syntax (or whether it is commutative at all)
1718627440 2 days ago [-]
It's not the syntax that's impressive. It's that most people who don't know C, likely wouldn't consider array access to be commutative.
junon 2 days ago [-]
They never said it was impressive. Just that they like to demonstrate it, presumably to people who don't know about it.
bennettnate5 2 days ago [-]
Some fun quirks I never learned until years after first getting into C:
- zero-initialization via `= {0};` will still leave padding and any data not covered by the smallest elements of unions uninitialized, so serializing data structures zeroed via that method is unsafe
- A pointer that increments any more than 1 past the end of a valid memory region (e.g. the end of a buffer) is instant undefined behavior even if the pointer is never dereferenced
- strict aliasing is on by default for pointers of differing types, but not for void/char/uchar. This means that having `struct sockaddr` and `struct sockaddr_in` pointers pointing to the same struct is UB.
The more I learn, the more I run from C.
teo_zero 2 days ago [-]
> zero-initialization via `= {0};` will still leave padding and any data not covered by the smallest elements of unions uninitialized
You really should move to C23 and use ={}.
2 days ago [-]
2 days ago [-]
delifue 2 days ago [-]
There is a nuanced mistake in
> If your machine runs out of memory, reversed[i] will cause a segfault upon being accessed. In order to avoid unhelpful segfaults, the program should check for a null pointer and gracefully exit if one is found
If overcommit is enabled (often by default), malloc will not return null when machine runs out of memory. malloc will still return null if input argument is invalid (e.g. too large).
pornel 1 days ago [-]
It's a mistake to assume that Linux with overcommit is the only platform that C runs on.
delifue 1 days ago [-]
Overcommit is very common now, including desktop, mobile and server, even in contianers. Checking malloc returning null only prevents OOM in places like embedded.
tgv 2 days ago [-]
Isn't that more of an environmental factor than a language feature? I would expect Rust applications to suffer the same way.
delifue 1 days ago [-]
Yes, same applies to Rust
_dain_ 2 days ago [-]
>The very first systems programming language I ever learned was Rust. This is uncommon compared to many other programmers; you're more likely to find someone who learned C or C++ first before coming to Rust.
It's becoming increasingly common. Rust is my first systems programming language too. I tried to learn C a few years ago but I found it too austere and prickly, which put me off.
assimpleaspossi 2 days ago [-]
I was going to write that I found this odd. The kid that cuts my grass is going for a degree in CS and told me just yesterday that, in his second year, Java is the language he uses with a little Python. That C was such a struggle and he won't touch it.
For someone getting a CS degree, that just seems so, so odd. Along with that, he's been studying edge detection in images. In his second year. Not to run off topic but, again, I find that so, so odd.
2 days ago [-]
le-mark 2 days ago [-]
When I was in college at a large Midwest university in 2002 intro cs classes were taught in C. Business majors were required to take cs 101 taught in C. We had a project that required implementing linked lists, oh how the business majors suffered! We all did actually but at least the cs majors could use the knowledge!
randomNumber7 2 days ago [-]
Stuff like this was often done to enforce a minimum IQ.
Very reasonable imo as universitys educated people that get positions with responsibilitys.
creata 2 days ago [-]
That's interesting - to me C is delightfully austere. Most other languages, even much younger languages like Rust, spiral in complexity as they mature. But C hasn't grown much more complex since C99. How many other (mainstream) languages can say that?
ReDress 2 days ago [-]
Probably because C and C++ are mostly used in system and desktop development.
Rust is taking steps or has been taking steps in this direction too.
It's not surprising that a lot of experienced systems and desktop development engineers looking to getting their hands on the newest tool already have experience or at least familiarity with C and C++.
rini17 2 days ago [-]
My biggest bewilderment with C is that malloc has to store the buffer size otherwise free could not work...but nobody in decades thought to make this information accessible to the programmer! If you want to keep track of buffer bounds in C, you have to do it artisanally. Despite it's, in most implementations, stored right there next to the data and thus in L1 cache already.
charleslmunger 2 days ago [-]
Not nobody! This proposal discusses some of the tradeoffs with exposing the size given a pointer, while proposing a different way to give the true buffer size:
Many fast allocators do not store the size next to the allocation, and may not store the requested size at all. This has the advantage that freeing a large allocation does not fault in cold pages with a write, as well as reducing allocator memory overhead.
rini17 1 days ago [-]
Reading the replies...basically all kinds of learned helplessness :(
charleslmunger 1 days ago [-]
If you meant standards committee replies, this proposal did pass an along-the-lines vote and is now in wording committee.
My experience with the process as a first time submitter has been pretty pleasant, all things considered.
nofriend 2 days ago [-]
> nobody in decades thought to make this information accessible to the programmer!
Obviously this is untrue. Under GNU we have malloc_usable_size(3). What's true is that it never became standardized, but standards for just about anything in C are very bare, so it's not too surprising.
xtajv 2 days ago [-]
Respectfully, I'm sure that there's some exotic usecase that is motivating the question, but
> nobody in decades thought to make this information accessible to the programmer
> otherwise free could not work
I'm inclined to say that you just answered your own question as to why.
I was always taught that "There are parts of a binary that are there for you, the lucky high-level language programmer. And then there are other parts of the binary which are there for the compiler to clean up after the spoiled little high-level language programmer who doesn't have to write their own assembly :)"
and
"If you want to argue with the compiler, that's what `gcc -S -fverbose-asm` and $YOUR_FAVORITE_TEXT_EDITOR` is for".
But I'm curious if this advice is unrealistic for said exotic usecase.
Do tell?
rini17 1 days ago [-]
I see, checking buffer boundaries for heap allocated pointers is very exotic usecase in C. /s
torginus 1 days ago [-]
I mean, all allocators in all languages do this, Rust C++ etc.
Also this information is of limited use to the programmer. There's no guarantee the buffer you get is going to be the size you asked for. Lots of allocators use nearest power of 2 sizes for small allocations.
For example mimalloc has separate regions by allocation size, so there's nowhere to put this info.
skydhash 2 days ago [-]
> but nobody in decades thought to make this information accessible to the programmer
Isn’t this implementation details, and an information already available caller side. It would be like returning the filename for a file handle. The more extraneous details in an API, the less flexible it is.
rini17 1 days ago [-]
I respectfully but firmly disagree that having buffer length information available for a pointer is "extraneous".
layer8 2 days ago [-]
> Woah, iterating over pointers instead of indexes! […] However, I'm not sure how good an idea that is.
I’d recommend anyone (including the author) wanting to understand C to read K&R’s “The C Programming Language”, which among other things will illustrate how iterating over pointers is idiomatic in C (though not quite in the way the author’s example does it).
In general, no one should be custom building most basic structures in C these days. =3
salawat 1 days ago [-]
Disagree. If you stop building it, you lose the knack. Secondly, more dependency centralization confers a degree of "elevation" to early comers that just really chafes my "programming is math" chops.
Then there's the Trust issue. If something goes wrong, it's my fault if I wrote it. I'm not going to argue with people who reach for "You're holding it wrong" reflexively.
t. Has spent time reimplementing basic data structures just because.
Joel_Mckay 1 days ago [-]
In general, there are a lot of platform specific issues that most people will just ignore... rather than address the odd edge-case failure modes or thread safety. =3
pwdisswordfishq 2 days ago [-]
Yes. And this is what C++ iterators evolved from.
ReDress 2 days ago [-]
I'm going to go out on a limb here and make a wild guess.
That boolean is actually mostly an extension of the integer system whereby we now have an integer type that stores only one bit.
Whereby the bit stored either results in a 'true' or 'false' value.
Anyways, I know booleans are useful in systems development when you have strict memory / storage constrains / bandwidth (networks).
Yeah, that works for me.
cogman10 2 days ago [-]
The way C handles (prior to C23) booleans is pretty close to how you'd handle booleans in assembly.
CPUs don't have types, everything is integers or floats. You do your work on registers which have fixed sizes. CPUs have built in instructions for "is this register not zero" which leaks into C. 1 is true in C, but so is 2.
Also, single bits are rarely used for booleans because it requires more CPU power to extract a single bit. Everything is byte aligned at a minimum.
When doing something like network code, if you want to store a bunch of booleans you are typically going to either pack them into a byte, or you'll burn the extra bits and send a single byte for the boolean value. Typically this was flags and masks.
quikoa 2 days ago [-]
> Also, single bits are rarely used for booleans because it requires more CPU power to extract a single bit.
Not necessarily, on modern CPUs memory access is often the bottleneck. If you have many bits storing these in a bitarray can be quite beneficial for performance.
cogman10 1 hours ago [-]
Only if you have an application which can somehow utilize an array of bits. It's been quite rare in my career to see that in practice. I know of only a few corner cases.
Regardless, due to the way most CPUs work, you almost certainly aren't saving anything by using a bit instead of a byte. When a CPU goes to load a memory address it'll not just get that address, but also the addresses around that address and shove them into cache. With the thought that "If you are loading 1 byte, you are likely loading 128 bytes". The cost savings really only comes into effect when you have more boolean values than bytes that fit in a cache line.
torginus 1 days ago [-]
This was the idea behind std::vector<bool> which is a bit array with template specialization, and has been tha bane of C++ devs for decades.
streganil 1 days ago [-]
I feel this comment misses some important context; in assembler, you might return a boolean value from a function in a register, but you also might just set a flag (which is much more similar to the "one bit value"). A flag register is very much a set of booleans, not an integer.
> single bits are rarely used for booleans because it requires more CPU power to extract a single bit
I don't believe this to be true; (on x86) `cmp X, 0` is exactly the same amount of operations as `test X, pow2`. One of them is a `sub` and one of them is an `and`. I believe this is how it works on all important CPU architectures of today back to the early microcomputers (although I don't know how the minicomputers handle this, so it's possible that booleans being integers makes more sense on a PDP-11 or a honeywell).
xhroot 2 days ago [-]
> single bits are rarely used for booleans
SQL Server still has no boolean type and groups bits in the same row into a byte if possible.
bjackman 2 days ago [-]
'bool' is still at least 1 byte in C. If you want to store one Boolean per bit you have to manually implement a bitmap.
tyromaniac 2 days ago [-]
Or pull out the C++ and use std::vector<bool>,... But you should probably never do that
randomNumber7 2 days ago [-]
I heard the C++ guys regret defining vector<bool> using 1 bit per value.
Also for most code it will be premature optimization to worry about that.
KerrAvon 2 days ago [-]
From what I can tell in a cursory web search, that regret is more about issues with C++ being unable to abstract that field vs bit divergence properly, not about the concept of having the specialization be bit-based, which is sound.
tialaramex 2 days ago [-]
I think your description is technically accurate, but maybe this phrases it better:
std::vector<bool> is a perfectly nice growable bit array type, and if the exact same code were in the C++ standard library named std::growable_bit_array nobody would be annoyed about this type, some people would use it, others would ignore it, nobody would write epic rants about it or name it the singe worst thing about C++.
The problem is that C++ popularized generics, and std::vector<T> is a generic growable array type, you ask for a std::vector<Goose> you get a growable array of your custom Goose type, great idea, very popular these days -- yet std::vector<bool> is not a generic growable array of bool, it's this other thing instead that's similar but not quite similar enough to be a drop-in replacement.
In C++ there is no way for the specialization to be bit-based without it being apparent that you are not in fact getting a growable array of the bool type.
codys 2 days ago [-]
No, that's not really an accurate rephrasing. Your comment indicates the problem is that it was exposed as a specialization, KerrAvon is saying the problem was instead that the specialization wasn't effectively abstracted (hidden) behind the API of vector (ie: implication is that ideally we'd have a better API for vector that might require C++ language changes due to the nature of the problems with vector<bool>. not that we shouldn't have the specialization)
tialaramex 2 days ago [-]
> Your comment indicates the problem is that it was exposed as a specialization
Because that is the problem? That's what C++ programmmers are unhappy about.
> ideally we'd have a better API for vector that might require C++ language changes
Ideally after almost thirty years "maybe we could re-design the whole programming language to paper over this bug?" would be such an obviously bad idea that I wouldn't see it in a response.
codys 1 days ago [-]
> but maybe this phrases it better
I recommend not indicating you're rewording another commenter when you actually disagree with them if you don't want that difference in content pointed out. I note that you've trimmed out the parts of my comment that made it clear I was drawing that distinction. Maybe you missed those?
Separately, though, it's not accurate to call this "papering over [a] bug". Wanting to have a thing that behaves like a reference that can identify things that aren't addressable directly (ie: having custom reference representations) shows up in a surprising number of places as desirable because we use references (and things like references) as an abstraction in many programming languages to enable nicer, more usable syntax (ie: having assignments behave in a particular way).
Edit: and changing the language would have been at the time that std::vector was designed (ie: c++98-ish c++ has a real problem with abstracting over not-really-references). With current c++, we can get a reference-like-object much closer to being fully transparent (`auto&&`, ranges iterator model, etc). But std::vector regrets reflect the constraints of the time when it was initially designed/standardized. C++ doesn't have a good story for making breaking changes, so if folks want a good vector for bits, today they'll go elsewhere.
tialaramex 1 days ago [-]
> I recommend not indicating you're rewording another commenter when you actually disagree with them if you don't want that difference in content pointed out.
I can see why you're upset but KerrAvon describes their statement as the result of "a cursory web search" and it isn't false so I wanted to be clear that what they wrote was not wrong per se. I don't think I "disagree" with a person who used Google, read some stuff and came away with an emphasis on the technical reasons this is a problem rather than how it's actually a problem for real programmers.
Maybe I do disagree with you, that's very possible.
With current C++ you could design a specialization that's somwhat less crap. But what's actually important - and what would actually happen - is that you would not standardize such a specialization at all because everybody knows it's a bad idea to do this - it would still be crap if you tried today, just less crap than in 1998.
C++ chooses to have a bad story for breaking changes. That's a deliberate choice and should be understood that way. WG21 are not hapless victims.
codys 23 hours ago [-]
I definitely agree that the state of the committee's management of C++'s ability to have breaking changes is not good. Languages should really learn from how rust is handling that pattern, as it seems like it's working fairly well there. Though rust so far has a much shorter lifetime, and hasn't needed to make as large of changes as C++ has needed, the editions model (or something like it) with isolation between translation units seems like it's very workable as a general pattern.
viega 2 days ago [-]
A bool does automatically cast to an unsized bit slice.
Meaning, if you have a struct and you want to bit-pack your booleans, you can declare each one as, say, uint8_t some_bool : 1;
You may then do `x.some_bool = true;` etc.
It's a small nicety to avoid bitwise operators, anyway.
bjackman 2 days ago [-]
I do tend to think people under use bitfields.
I suspect this is because they are usually introduced like "here are bitfields, but DO NOT USE THEM for anything that might become ABI because their order is implementation defined". I think that causes people to mentally file it under <sketchy language features to avoid> when actually there are many contexts where the ABI risk is not real.
ReDress 2 days ago [-]
[dead]
teo_zero 2 days ago [-]
If the author is taken aback by '->' vs. '.' and by true and false being 1 and 0, I'm surprised they haven't mentioned digraphs and trigraphs, or the alien <name> syntax for #include!
procaryote 2 days ago [-]
The only place I've ever seen a trigraph is in malicious c code style content. It's a terrible idea for sure, but not a problem in pratice... to the point that gcc at least used to require an extra flag to turn them on
kvemkon 2 days ago [-]
> the program should check for a null pointer and gracefully exit if one is found: ...
If malloc() fails, there is no need to exit the program completely with exit(), only return from the current function with an error.
randomNumber7 2 days ago [-]
It is impractical to try to recover/continue your program in an out of memory situation for most programs.
So it is a good advice for beginners.
smj-edison 2 days ago [-]
I think this is fair the majority of time, but I'd like to mention Zig, since Zig has a convention of all allocations being fallible and handled. It's a pain at first to handle error.OutOfMemory at each allocating site, but I feel like I'm much more conscious of where allocation can fail and how to gracefully handle it. I've also gotten a lot better at transactions since pretty much every operation has failure points now.
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).
steveklabnik 2 days ago [-]
The Rust stdlib has added those functions that return errors, and the types are parameterized by the allocator trait. The trait is coming to stable in the next release!
smj-edison 2 days ago [-]
Oh that's exciting! I knew about the allocator work, but I hadn't heard about error reporting for failed allocations, or that it was about to be stabilized.
steveklabnik 2 days ago [-]
To be specific, I’m talking about try_ variants of functions that allocate and return Result.
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.
mappu 2 days ago [-]
In userspace/application code this isn't giving you all the value you hope for - because of overcommit and fork/exec, you can OOM on simply modifying a variable (in a non-file-backed page).
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.
smj-edison 2 days ago [-]
Unless you use cgroups or turn off overcommit :) I'm also hoping to use the core interpreter on memory-limited devices like an ESP-32.
layer8 2 days ago [-]
If the code is in a library (and I’d treat code as if being part of a library by default), then the library shouldn’t be deciding that.
1718627440 2 days ago [-]
But in order to able to show proper diagnostics and error messages, you should still return all the way to the top.
im3w1l 2 days ago [-]
No one does this. The best you can hope for is that a rare few especially error prone allocations (perhaps they are huge, or user controlled) are handled. Someone might I suppose also wrap all malloc calls in a function that shows a dialog or error message and only then exits. But returning all the way to the top, yeah.. no..
eesmith 2 days ago [-]
Agreed.
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:
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?
steveklabnik 2 days ago [-]
> I think it's because
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.
tcfhgj 2 days ago [-]
recently noticed that iced (rust ui framework) crashes on oom - from the logs it seems to be aware and crash explicitly.
I wish it would just reduce the fps or hang a bit instead of crashing, perhaps use exponential backoff
eesmith 2 days ago [-]
How is it then that my stock KUbuntu system won't let me allocate 50,000,000,000 bytes?
$ 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." :)
steveklabnik 1 days ago [-]
> How is it then that my stock KUbuntu system won't let me allocate 50,000,000,000 bytes?
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.
eesmith 1 days ago [-]
My software does work with large blocks of contiguous data, where malloc can fail in exactly this normal way.
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()".
steveklabnik 7 hours ago [-]
> I don't think my software is all that special.
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.
dwattttt 1 days ago [-]
> How is it then that my stock KUbuntu system won't let me allocate 50,000,000,000 bytes?
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.
eesmith 1 days ago [-]
Sure.
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.
dwattttt 1 days ago [-]
I learned programming from a tradition without overcommit; even though one failure mode is reported synchronously, I can't trust a success to mean I won't fail later.
"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.
aw1621107 2 days ago [-]
> I've never been on a system where sizeof(char) != 1
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)
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.
I .... okay, my mind is blown. It absolutely says that, and https://en.cppreference.com/cpp/language/types adds "this allows the extreme case in which bytes are sized 64 bits, all types (including char) are 64 bits wide, and sizeof returns 1 for every type."
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.
aw1621107 2 days ago [-]
Yeah, it's one of those things that is not exactly intuitive if you didn't already know about it. The menagerie of other byte-ish-sized types added in recent versions of C and C++ probably doesn't help either.
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.
(And CHAR_BIT is required to be 8 by POSIX, so even though the language and some exotic hardware will have it at non-8, it’s exceedingly rare at this point.)
wahern 2 days ago [-]
Not sure I agree with this pattern for strings, but if you're gonna multiply for an allocation like that and are concerned about consistent idioms, use calloc or [non-standard] mallocarray, which will check the multiplication for overflow.
eesmith 1 days ago [-]
After being bitten by mistakenly using int for the result of a sizeof, then overflowing the int multiplication with the count, my personal idiom is only use malloc if it's a count multiplied by a sizeof.
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. :)
rramadass 2 days ago [-]
Read the following if you want to be a C Programmer;
Nice, but I am also looking for something like "Rust for C programmers" - more suitable for the current era.
redrobein 1 days ago [-]
Isn't this just the rust book? There's some early chapters that can be skimmed through, but there's also enough new features and behavior that if you've only ever done C, it'd be hard to put everything in a cheatsheet.
When I was building my kernel in my late teens, I first wrote the bootloader by hand on paper in assembly language. I then referenced the x86 manual for the instruction set and converted my assembly code into the equivalent hexadecimal machine code values of the x86 machine instructions, which I also wrote by hand on paper. Then I used a hex editor on the desktop to manually write the hex values into a file and used it as the bootloader i.e as the first 512 bytes.
The whole exercise gave me a sense of hard grounded zero magic, raw, unfiltered experience. This is an experience that is hard to replicate in any other way. I deliberately did that so as to peel away as much magic/abstraction layers as I possibly can.
Later on when I started using C, I never had to learn C but merely just had to reference the equivalents of the assembly language. Like how the primitive "if" doesn't exist in the hardware but is a composition of cmp and jmp instructions. Seen this way, C becomes a glorified portable syntactic sugar over assembly language.
Then higher up the ladder to C++ for object oriented problem solving while retaining the spirit of functional programming. Rust was a breath of fresh air, where correctness across a myriad of use cases was a first class primitive.
Each abstraction layer can thus be evaluated for its utility in problem solving while its underlying mechanics remain understandable down to the hardware level.
More recently, our own arcc compiler extends correctness to our architecture and not just the types.
So if you are young and have time to spare, I suggest a little bit of Assembly => C => C++ => Rust.
This essentially makes you immune to hype train bullshit.
jan_m_savage 2 days ago [-]
real programmers don't build identities around languages (i.e. don't call themselves a ___insert__language programmer).
weinzierl 2 days ago [-]
real musicians don't build identities around instruments (i.e. don't call themselves pianist, violinist or singer).
(I say this as an aspiring C major pianist.)
jan_m_savage 2 days ago [-]
yes, just look at AR Rahman, arguably imo the most prolific (living) musician in the world, let alone Asia.
zeroq 2 days ago [-]
in an ideal world real programmers wouldn't be building their whole identities around publishing libraries like "isEven" or claiming fame for vibe porting perfectly fine libraries into whatever is the newest and hottest platform...
... but that ship has already sailed
gruntled-worker 2 days ago [-]
But... how would the powers-that-be induce language fragmentation, then? /s
xtajv 2 days ago [-]
> As such, there are plenty of "Rust for C Programmers" articles on the internet, but little to no "C for Rust Programmers" articles out there.
[obligatory] The original "C for ... programmers" article that convinced the world to make C as pervasive as it is today is K&R's "The C Programming Language".
I know it's in book form (egad!) and if you download a PDF, it's like 80 pages (double egad!). But it makes for surprisingly light reading.
physicsguy 1 days ago [-]
This misses the point entirely of why C is as it is.
It’s quirky because when you had kilobytes of RAM, storing the length of the string was memory you couldn’t afford. Arrays decaying into pointers similarly meant carrying a single pointer around rather than a fat struct. Magic integers for return codes were used because they’d fit into a single register. Compilers didn’t have the processing power or memory to track complex borrow rules or static analysis. They were slow even as they were! Old processor architectures were way more diverse and fragmented than now - and even 30 years ago endianness wasn’t a settled thing.
Things weren’t quirky because people “wanted” them to be, they were quirky because everything was way more constrained. Rust came into a way more rationalised environment and it’s a great language of course but the operating environment is so totally different it’s hard to compare.
blourvim 2 days ago [-]
It reads really well
fithisux 2 days ago [-]
What kills the C experience in my opinion is that articles like that do not specify C standard and compiler extensions.
pwdisswordfishq 2 days ago [-]
isize and usize seem to be more like intptr_t and uintptr_t. To be fair, for a long time it was not very well-defined to which C type they correspond to; I think that was cleared up only recently.
It's also a shame the article uses the self-delusional C++ style of pointer declarators.
Otherwise pretty okay.
tialaramex 2 days ago [-]
They're not great analogs to any of the C types because C and C++ have a different relationship to pointers than Rust does but particularly they are not intptr_t / uintptr_t because those claim that we can intra-convert between these types and pointers.
On a typical PC that doesn't seem like a problem, and it will (at least kinda) work which might give you the false impression it's required to work, which it very much is not in Rust. On CHERI it's obvious why this can't work. CHERI's pointers are 128-bit. Rust does have 128-bit integers, but Rust's isize and usize on CHERI will be 64 bits. Because only half of CHERI's pointer bits are address bits, and Rust told you that isize and usize were big enough for the address not the whole pointer.
Many clever pointer tricks only want to fiddle with the address. For example hiding bit flags in an aligned pointer works, as does hiding the entire value inline in today's enormous pointers (64 bits! Luxury) and using a single bit to mark "not a real pointer". In Rust we do these with the actual raw pointer types, they have methods like any other type, but in C or C++ you need to convert to a pointer-sized integer and then do tricks with the integer or you will write UB.
afdbcreid 2 days ago [-]
That's not true. `usize` and `isize` are explicitly specified to roundtrip a pointer address. You are correct that this would imply they need to be 128-bit in CHERI which will be a major perf hit, and this is actually blocking Rust support on CHERI, and the solution might be to change the guarantees (but it is hard because code in the wild relies on it), but it is the current guarantee.
tialaramex 2 days ago [-]
I think what's going on is that you've half understood the situation with CHERI as of a few years ago and now you're trying to explain your half-understanding to me confidently as if you're correcting me.
As I said, these types are the same width as an address on the target but a CHERI pointer isn't just an address, that's why they are so wide.
No, I usually follow language and opsem discussions. While CHERI discussions are not something I follow closely, unless you have an explicit link I'm pretty sure I didn't miss something that important.
The strict provenance APIs or the "Rust has provenance" RFC are not relevant here (more precisely, they help clarify the options but do not solve the problem).
> Pointers to sized types have the same size and alignment as `usize`.
Which just cannot be guaranteed on CHERI with 64-bit `usize`.
There are discussions like there were before, and it's pretty clear that we'll have to give up either performance or possibly quite a lot of compatibility, but no decision.
tialaramex 1 days ago [-]
I was not aware of that line in The Reference so that's interesting.
I suppose one of the things you could do, at least on Morello, is treat it entirely as Arm64 in all your Rust but offer some CHERI flavouring in FFI for people who had to do their memory safety in hardware. In such a world that statement is just true.
afdbcreid 1 days ago [-]
Can you roundtrip a pointer as `MaybeUninit<usize>`? This is implied by this statement (and other rules of the language). And people rely on it.
You can make CHERI a non-conforming target. In fact we already have some (did you know x86-32 is a non-conforming target?). This is one of the leading options. The question is whether we don't give up too much existing code by that. No existing target is that incompatible.
tialaramex 23 hours ago [-]
> Can you roundtrip a pointer as `MaybeUninit<usize>`?
AIUI Strict Provenance rules imply that you can't expect any such tricks to work unless the target promised they would work.
You say "People rely on it" and I must ask: How many people and how sure are you that they actually can't live with Strict Provenance ?
afdbcreid 6 hours ago [-]
That's not true. Strict provenance says that you can not roundtrip a pointer via `usize` (with transmutes and not casts). You can via `MaybeUninit<usize>`. One of the fundamental properties of the language that is even specified explicitly (https://doc.rust-lang.org/stable/std/mem/union.MaybeUninit.h...) is that `MaybeUninit` preserves the exact Abstract Machine bytes, including provenance.
2 days ago [-]
jmclnx 2 days ago [-]
Really just another use Rust instead of c article.
BitProgram 4 days ago [-]
This article is written for Rust programmers learning C. As someone
in the opposite position I know C and I've been curious about Rust I'd love to read the reverse.
What's the hardest thing for a C programmer to unlearn when moving
to Rust? Is it the borrow checker, or is that just the thing people
talk about because it's the most visible?
coldpie 1 days ago [-]
> What's the hardest thing for a C programmer to unlearn when moving to Rust?
One thing that stood out to me going this direction is it's really hard to write hackish or intentionally-broken code in order to try to explore implementation options or debug an issue or test an algorithm. Rust requires you to handle error cases, and all possible values in cases, and is strict about memory access and stuff. But sometimes I just want to shove these bytes into this thing and see what happens, or hack in an early return or something, or half-implement this API to see if it "feels right" before committing to a full implementation, and Rust's enforced best practices stop me doing that.
(To be clear, I like Rust. This is just one thing that trips me up coming from C.)
dwattttt 1 days ago [-]
I'll offer a thought, which amounts to rephrasing "front loaded with a myriad of compiler gymnastics you need to think through." from sibling.
There's a _lot_ of utility in things that aren't represented by a literal difference in the compiled code. It's not unusual to make a type in Rust that's just a single field; you're not making a struct to gather related data together, you're giving it a different name because the same data means something different.
It's a little like the difference between a uintptr_t and a uint16_t*. They're represented the same in the machine, the difference only exists in the source language.
Rust derives a lot of value from things that only mean something at compile time. I think you'd "unlearn" what C (commonly) teaches when you don't find a C struct with one member "weird".
elendilm 2 days ago [-]
In C, mostly you think about your code running on the machine with some thought dedicated to the compiler gymnastics involved during the compilation phase.
With Rust, you are front loaded with a myriad of compiler gymnastics you need to think through.
But once you get comfortable enough, it becomes natural. You also have to get accustomed to writing code that is more verbose than C which might look ugly at first but later you start to accommodate it as the necessary cost for the utility you are handed in return by the compiler.
For example, multiple variables in Rust doesn't necessarily mean multiple memory allocated variables at runtime like in C. The rust compiler will usually keep track of and ensure multiple variables (non Copy types such as String with Move semantics) map to one memory allocated variable at runtime (in normal single threaded use cases under normal circumstances without using RC, ARC, etc.). Eg: let a = String::from("hello"); let b = a; ... Note: The example is for illustration purposes only and not always true. In summary source code variables are abstractions and may not belong to distinct runtime memory location. Yes it is true even for C. But Rust's ownership model makes that distinction aggressively visible.
bjackman 2 days ago [-]
It's beautiful to see this perspective! The fact that we are at "whoa, C is fucked up compared to my expectations" instead of "look at Rust's fancy safety stuff" shows that as a field we've started lifting the baseline.
Nowadays I actually believe I'll likely see a world without memory corruption within my lifetime.
kingforaday 2 days ago [-]
It’s definitely great that memory safety is becoming more pervasive. Though I’ll believe “a world without memory corruption” right up until another <insert your relevant hacker hero name> comes along and finds a way through an unsafe block, an FFI boundary, or the hardware itself (Rowhammer says hi).
bjackman 2 days ago [-]
> Rowhammer says hi
Yeah I was thinking of prefixing my "memory corruption" with "software-bug induced"! I don't see a credible solution to Rowhammer. ("DDR[n+1] fixes it" - lol)
> an unsafe block, an FFI boundary
Honestly these feel solvable to me at this point! I think we'll see:
- unsafe code shrink as languages get more powerful
- amount of analysis we can apply to each unsafe line shoot up exponentially as AI gets cheaper
- amount of FFI we actually need shrink as it gets easier to just click "rewrite it in $lang" on the decision card when your coding agent says "I found a library for that but it's in a different language"
(Having said all of that, people seem to be adopting Zig for some bizarre reason... So maybe I'm naive to expect unsafe lines to shrink)
(But also, maybe AI gets so good and so cheap that we can just type "go fidn all the bugs andfi xthenm" into an LLM, between sips of a Piña Colada)
smj-edison 2 days ago [-]
I'm one of those people who's adopting Zig :) But I don't think it's best for every project. For context, I've used Rust before for probably two years writing a realtime audio synthesis engine, so I'm fairly familiar with Rust vs Zig for handling low level details.
The biggest reason I use Zig is it's a very explicit language. The creators made a very intentional decision to avoid too many "high level" designs. This doesn't mean there's no capabilities for abstraction (comptime is great for that), but when you see array indexing, you can think "ptr + index * size with bounds check". There's lots of other things like that where the language does exactly one thing, and that thing is a low level operation.
This is terrible when you want to create high level abstractions that hide details from the programmer, but it's what I need when doing realtime audio synthesis or what I'm doing now which is writing an interpreter. I know exactly what allocates, I know what calls IO and can block (both operations explicitly take in an allocator or IO parameter), no data structures have private fields so I can always poke around at the insides. I know what types of errors a function returns, and creating errors is cheap with Zig's error union design.
So I don't use Zig because I think it's the safer language, I know it has sharp edges because of the number of times I've caused a panic on a poisoned pointer or use-after-free. But because it gives me such a transparent view into what is happening I find it liberating.
uecker 2 days ago [-]
I would say there are two big misconceptions here, which this article (and your comment) unfortunately reinforces.
The first is that it is "C is fucked up" instead of the poor defaults of the C implementation your are using.
The second one is the idea that C all has to be fragile low-level pointer fiddling instead of much safer high-level code build around abstractions, which good C would usually have.
bjackman 2 days ago [-]
Sorry but no, this is totally wrong. There is no set of compiler flags or design practices that can fix the lack of lifetime analysis in C.
I do actually think that with modern C++ it's possible to get to a style of programming where memory unsafety is mostly a theoretical concern but not with C.
uecker 2 days ago [-]
It is true that you will not get full lifetime analysis with a C compiler, I do not think this defeats my point. (and you can get lifetime analysis with other tools, but we lack good tooling for this)
For me, memory safety is also mostly a theoretical concern in C. This is achieved by having proper abstractions instead of low-level pointer fiddling and by having a clear strategy for managing lifetimes.
the__alchemist 2 days ago [-]
My take too. (Aside from this being a well-written, clear article, that I think highlighted a fair selection of relevant points; especially the "always use uint8_t instead if int" part).
I don't see rust a "memory safe" language, or a niche one. I have complaints about it etc, but it's overall a fair baseline of reasonable decisions. When I look at C or other languages rust has learned from, I have more "Yikes, that's rough" takes. So... rust as the language of least "fucked up", to use your phrase? Ownership/safety are one part of the picture, but not what defines it for me.
cohani 2 days ago [-]
[flagged]
the__alchemist 2 days ago [-]
Is this the sort of thing you only need to know about if building/maintaining compilers? I ask because I don't know what that means from a practical perspective. I'm familiar with Ferrous systems from their work on probe-rs, defmt, and flip-link, which are exquisite libraries / tools.
edit: I think this is a bot or troll account.
Ygg2 2 days ago [-]
Rust never had a goal of having well defined spec. It all depends on what ecosystem wants.
In lieu of that it didn't fuck up.
cohani 2 days ago [-]
Focusing narrowly on memory safety is frequently a distraction that incompetent developers use to excuse their buggy code. "Sure, my Rust code might cause deaths and cause millions of dollars lost; and my code might be unsafe, insecure and incorrect; but at least my code is memory safe and easy to debug when I fuck it up!". And then it often turns out that Rust is not memory safe in practice. https://news.ycombinator.com/item?id=49697392
The real value of Rust is likely pattern matching and tagged unions. Apart from modules/packages that are not a disaster, like Gabriel Dos Reis the saboteur's disaster with modules in C++.
nilslindemann 2 days ago [-]
Were you intentionally rude with your first sentence or just unaware?
For what it's worth, in that specific instance there's no memory safety issue since Rust is guaranteed to crash on stack overflow on Ubuntu (and probably other supported Linux distros)
afdbcreid 2 days ago [-]
For what it's worth, this is not only that specific instance. Rust might not be memory safe in theory, but when talking about "in practice" - there is profound evidence that Rust's memory safety works.
wannabe44 2 days ago [-]
Indeed! They shrug it off when you ask them about supply chain safety or compiler complexity. I opine that post 1.0 they wasted precious engineering time trying to placate nodejs webshits over building the best systems language.
There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies and the other way is to make it so complicated that there are no obvious deficiencies.
— C.A.R. Hoare, The 1980 ACM Turing Award Lecture
aw1621107 2 days ago [-]
What would you have preferred the devs work on instead?
slopinthebag 2 days ago [-]
i suppose a supply chain that doesn't exist is pretty secure.
slopinthebag 2 days ago [-]
this argument is so stupid, yes rust doesn't prevent all bugs but eliminating a large proportion of them is very obviously a good thing.
caaqil 2 days ago [-]
> there are plenty of "Rust for C Programmers" articles on the internet, but little to no "C for Rust Programmers" articles out there.
Why would a Rust programmer learn C? Isn't that basically a regression?
glum64 2 days ago [-]
The first language is formative. It gets the programmer to think at a particular level of abstraction. If that level is wrong for the problem at hand, the programmer needs another language.
Write, say, a boot loader, for enlightement.
cohani 2 days ago [-]
It is easier to write a C compiler from scratch than a Rust compiler from scratch. No usage of LLVM or anything like it.
That also reflects in that many embedded systems offer C support and do not offer Rust support.
orbitaldesk 2 days ago [-]
Not a regression. Most of the systems code you'll ever touch is still C, and knowing C makes Rust's unsafe blocks and FFI story way less mysterious. I learned C after Rust and it changed how I read Rust too: the ownership rules click differently once you've managed memory by hand and felt the bugs they prevent.
Though the author doesn't look to be trying to write a great tutorial, rather prioritizing sharing what and how they learned something from their perspective.
Please stop smashing all the nuance out of compiler diagnostics this way.
One of the biggest successes of Rust has been its great diagnostics and I can assure you that it would not help to smash my Clippy lint suggesting that what I wrote looks a lot like an implementation of the addition operator† into a fatal error like the one I get for forgetting to initialize a variable.
Rust even has a (begins empty) diagnostic category [named "expect"] for "This warning should be here" which will flag cases where a notable thing not only might happen here and if it does we can ignore that, but if it's no longer detected that is itself suspicious and should be diagnosed.
† Yes it does Clippy, and I considered implementing Add but I had a good reason not to, so here is a suppression annotation.
C got lint in 1979, and is incredible how much advocacy is still required for folks to use the tooling their compilers offer out of the box.
This problem isn't even C specific, other ecosystems suffer from similar adoption issues.
Requiring all of them to adopt common linting rules at the same time is infeasible.
Also, it’s not true that linting rules never become part of the compiler. For example, the original lint (https://wolfram.schneider.org/bsd/7thEdManVol2/lint/lint.pdf) states
“The type-checking rules also require that, in structure references, the left operand of the -> be a pointer to structure”
and mentions warnings on such statements as
Modern C compilers also warn way more often about unreachable code, pointer incompatibilities, etc. than those from the 1970sIn some circles having the compiler dictate the way, and only true way, is seen as straightjacket programming.
But this illustrates how absurd the discussion is nowadays. Incrementally making things stricter and fixing warnings along the way is one of the most cost efficient way to improve quality and safety. Rewriting the software one of the most expensive. So if the former is rejected because the burden is too high, then obviously the overall aim to make things safer can not be too important and the push for rewrites must partially be motivated by other interests.
"The Power of Ten: Rules for Safety Critical Coding by Gerard Holzmann"
https://youtu.be/GRJtYwneG2Q?is=M0z7hnpQ78C8fLLP
At a moment he mentions a trick he played on JPL folks, his static analysis tool, instead of showing all the issues found, would only display the top 10, without telling them there were actually more than 10.
So they felt motivated to fix them, it were only 10 after all.
Naturally eventually they became aware of the trick.
Or if you turn your argument around: if people depend on workflows and automation (I do of course too), then this is a good reason to standardize the diagnostics and errors of the checking tools to the point where the workflows and automation can depend on them. Otherwise they will remain tools which are completely optional to run, or even worse, are not part of your normal work flow.
However, like in any ISO languages, that only happens if enough people vote for it, and someone is willing to submit a paper in first place.
Tribalism is sooo ... sadly now.
Please don't.
One of my favorite things to show just how bare this is in C is to show array access commutativity.
C is wonderfully simple at times.In most languages, the indexing operator is not commutative. In Rust, pointer offseting is expressed as a function call or a method, also not commutative. I have never seen a single complaint about either. It's not something people want or care about, it's just a tedious detail.
It’s not something idiomatic, but indexing in C is syntactic sugar. Not sure why they allow it in the syntax, but forgetting that arrays are pointers and not special type is just asking for bugs.
I don't mind adding new features in committees but if they are going to remove compatibility, they should rename their language to nuC or CantbelieveitsnotRust(yet).
No offense intended to Rust which is a fine language.
> C11 is last C as far as I'm concerned.
Are you sure? I think C23 brought so many enhancements it's a pity to ignore it.
"C++", perhaps? :)
> if they are going to remove compatibility
C as a language has one job, and it is to not do this.
Yesterday, I watch a quick video[0] where Matthew Butterick was comparing book sizes and their appeal. “The C Programming Language” was my second programming book (after one about JavaScript 1.x) and I still remember it fondly. Easy to start with (with CodeBlocks on Windows and gcc on Linux) and the concepts were nicely explained. The book were also very nice.
[0] https://www.youtube.com/watch?v=W-ryv6TwQvM
Also arrays aren't pointers in C, they decay into pointers. You can see this since sizeof will work differently in the function that instantiates the array versus one that takes in the pointer as a parameter.
At end of day minimalism is a neat but not decisive feature. If minimalism was decisive we'd all be writing Brainfuck.
In C the minimalism comes from providing only the minimal set of things that are available in most (maybe all) architectures (the Von Neumann paradigm), like linear memory, a simple function calling convention, close mapping to Assembly operations etc. But C syntax is not very minimalist compared to Lisp , let alone Forth. The fact that C syntax became prevalent in the programming world seems to be mostly an accident to me, it’s not objectively better than those minimalist languages’ or Pascal’s, Prolog, ML families.
The rise of C in the 1980s induced CPU makers to design instructions that cater to C semantics.
Edit: And by "worked everywhere" I mean C did the heavy lifting of being a programming language that was even a little bit portable across architectures.
In contrast, Intel introduced a lot of instruction for PASCAL, e.g. enter / leave etc. which are basically unused.
He says Leave is often used by compilers, but not Enter.
An instruction set ideally suited for C is the PDP-11.
By that logic Brainfuck is even more minimal. Again. I'm saying minimalism isn't the goal. It's a good quality but not most important one.
For example, the LCD of 4 and 3 is 12, that's the "minimum" number they have as a common multiplicator.... it's not 0, even though both multiplied by 0 equal 0. In more concrete terms for C: C is more like the maximum number of things that intersect across all different "common architectures". You want to have most things that everyone supports, not the lowest number of things possible! Hope this clarifies what I mean.
Brainfuck is quite literally minimal Turing machine. It is as portable as C. Or P'' ( https://en.wikipedia.org/wiki/P%E2%80%B2%E2%80%B2)
This is beautifully illustrated in the old classic The C Companion by Allen Holub which overviews a simple abstract demo architecture and code generated for it. This is a small book (in the vein of K&R C) but contains enough illuminating info. for a programmer.
Zoom out a little: C is a lot like Unix: simple probably isn't the right word; `underengineered` comes to mind. Which leads to complexity, as you need to make things work in the real world. And so Unix syscalls being designed in the early 1970's for a PDP-11 don't really map to modern needs. And so every unanalyzed complex C app has memory leaks.
To be clear, I like C, I like C++, I like Objective-C, I like Swift; I'm comfortable in all of them. But, in 2026, I don't see why for native code everyone shouldn't be programming in Rust / Swift / other modern memory-safe flavor for any new production use. Zig if you want faster compile times, I guess.
I also do not see why the simplicity of C should lead to complexity. I have build complex systems in C. I did this using C++ in the past, but switched to C because I found out that all the complexity of C++ was a huge distraction and not something that helps me design better systems. C++ also came with a lot of claims that all the features are absolutely needed to build good abstractions and that this is not possible in C, something I found to be completely false. (And then, a lot of complex and still extremely reliable software I use daily is build in C. My practical experience completely contradicts the myth propagated nowadays by some that all C is inherently bad and unreliable . In my experience it is the exact opposite: The C software I use is in fact the most reliable.)
My view is that Linux and free software is still going strong, because it managed to largely keep out the unnecessary complexity out of the core infrastructure. With Rust and AI, I worry that now get a lot of overly complex infrastructure and that it will be hard to ever get this complexity under control again.
Because C is very simple (unlike Rust) and works everywhere. Zig is not yet stable. Go and Swift are under the governance of tech companies.
The true appeal of C for me is the standard and how it applies only to the language. You can easily take a project from 2 decades ago and port it to a current platform. Lot of current ecosystem is way too fussy about tooling to do this.
That's definitely true. C is a "Worse is better" language and as such it's everywhere. You can knock together a halfway usable C for some crap hardware in a few weeks and then the hardware is saleable because there's a C implementation.
> You can easily take a project from 2 decades ago and port it to a current platform.
Much of the software I wrote two decades ago in C won't even build today. Good luck figuring out why, periodically I try to figure out which weird 2000s era Mac OS hacks don't like a 2026 Linux system and eventually I give up and write modern software instead.
In theory C written in 2006 definitely "just works" on a 2026 Linux machine but in practice real world C is broken and even though I (co)wrote it I don't know why. Also in practice the first serious Rust project I wrote in 2021 I just found it, checked out the oldest working version ("first rough working code" says the log) from git, cargo run, works as expected.
Maybe it's because the C was four times older, but I think it's because in the real world you don't end up writing that mythical portable C code too often.
'C' seems to be a language that makes it easy to write incomprehensible statements, but it's not that hard (or it was not that hard) to write code that is trivial bring up to date. Perhaps it was easier for me because I started with Fortran.
I am quite sure my MS-DOS C code is going to have some hard time being compiled on GCC 16.
Do you also put Java in quotes? Rust?
It's because C, over time, went from a bunch of almost compatible implementations, to a standard that differed a little from every existing implementation, to a standard that is updated every decade or so, which is then implemented by a bunch of different products, with enough ambiguity in the standard (UB) that there will be differences between compilers and versions of compilers.
Rust is a single implementation. Always has been.
It's a single product, so you code to the product. The product has no external forcing function, like a language standard, that produces changes every decade. When you write Rust, you aren't thinking "Wait, lets make sure this works on the Watcom compiler too".
If Rust code cannot compile 30 years later, that's a failure on the language, because the single implementation is in full control of compatibility. If C code cannot compile 30 years later, that's not a failure of the language, because the implementation in 30 years has had external pressure forcing changes.
People often forget that Rust is a product, C is a specification.
In practice the cadence is exactly half that of WG21's breakneck C++ pace. Every six years. C11, C17, C23 so far and C2y is expected to land before the end of this decade.
But anyway, all I can do is report the observable fact. If you want to say it's because GCC 15 is a different product from GCC 4 whereas rustc 1.99 is the same product as rustc 1.49 then I guess you can re-phrase my position as "In C you will keep needing new products and that sucks" if you like.
Then they get irritated that not all C compiler vendors have the same language extensions or implementation defined behaviours they got used to rely on.
It's also easy to write C that won't build on the system it was originally developed on, a few years down the line. But that is true for almost every other language ecosystem. I would say, C makes it _possible_ to write code in such a way that it works on many different platforms and compilers, and/or can continue to work in decades to come with relatively little maintenance.
The latter isn't true for most other ecosystems, because to get anything done in them, you need to rely more on that language and its ecosystem -- specific language features, specific libraries. C isn't a glue code language. It's very good at letting you create your own thing.
To do it well though, requires a lot of care and expertise.
Is that the reason? I thought it was primarily because C stupidly* makes a ton of platform-specific things more convenient than the platform-independent equivalent - the width of integer types, locales, etc., all vary by platform, and it's easy to accidentally depend on them.
* Maybe it wasn't stupid at the time when C was developed, but it's a bad choice now.
Much bigger problem is understanding the scope of dependencies. Dependencies can go bad and you need to update. A dependency might not be available on some new platform so you might have to replace with something else on that platform. To make the software portable, good modularity is required. This is to a large extent an aspect of software architecture. Even Rust can't magically make this happen, if you depend on 300 crates that's probably not a great place to be in either if you want to be portable and maintainable.
There is probably a point that C's flat namespace is a major contributor to badly designed software, because people aren't aware of the dependencies they're mixing all the time. Also C encourages transitive includes, leaking implementation details to the user instead of just to the compiler. (But btw. I find C++ and Rust namespace to be unergonomic syntax-wise, and what's needed is not actually namespaces but control over visibility).
* Programmers are lazy, they're going to type int more often than they should
* All these fancy modern "sized" types are actually just aliases for C's built-in types like int chosen to match the sizes people want...
* ...but C's integer promotion and silent rules about conversions work only on "real" C types you were trying hard not to think about and all the rules for those types, including types you never mentioned but were using because of promotion, apply to your program. You can write code which looks like it's all unsigned, but oops, under the hood a promotion means a signed integer came into existence, then it overflowed, now you have Undefined Behaviour.
I am interested to hear what you don't like about the namespace rules, in Rust in particular.
The point of namespaces is exactly to allow less qualified names, to allow spending less energy when creating new names, by having the compiler always add a qualification implicitly. However, that doesn't solve the problem with name collisions when reading or grepping the source code -- it actually makes it much worse. And avoiding those name collisions, i.e. what's in the source literally, what the programmer reads, and what less-than-perfect tools see, is at least as important as avoiding "semantic" name collisions and being able to compile the code.
Namespaces not only encourage many more literal collisions -- now a single name can be available in various shortenings, and even under different aliases, which also makes it hard to tell that two symbols refer to the same thing!
I've found that the only way to get names right is to find a good scheme, to find meaningful good prefixes, suffixes, and abbreviations, and to come up with a system to create new names more mechanically but still keep them as short and readable as possible.
I like that core::cmp::Ordering and core::sync::atomic::Ordering exist because in their contexts I think these types should both be named Ordering and so it would be annoying if they were named Ordering and AtomicOrdering or something. If I'm writing code which does tricky atomic operations I can bring in core::sync::atomic::Ordering and now Ordering means that one, whereas if I'm writing code about whether this Goose is before or after that Goose, I ask for core::cmp::Ordering to get the relevant Ordering.
And if it's unclear in context, I can write core::sync::atomic::Ordering and everybody knows which one I meant.
I don't usually use an IDE but I believe it's usual in an IDE to tell you the full names of such things and even to help you import relevant stuff based on completions.
I've realized that naming is an art that is difficult to master but makes a huge difference when gotten right. It consists of not just combining names into compounds but also finding new shorter names that are ergonomic and distinguishable. For example, I've replaced the compound "NodeIntersectTest" in my current project with simply "Probe", which is much easier to read and write, easy to distinguish from other terms after learning its meaning once (it is a central concept to my app), and also perfect for log messages and stats where brevity matters a lot.
A two letter prefix for all identifiers in a project or company codebase is a good idea, but I try to avoid hierarchies and finer granularity, and generally to be overspecific, in order to keep things easier to change later.
The lack of OpenMP for Rust etc. seems like the most obvious reason for "why use C instead of Rust in 2026" to me.
Anything else?
- zero-initialization via `= {0};` will still leave padding and any data not covered by the smallest elements of unions uninitialized, so serializing data structures zeroed via that method is unsafe
- A pointer that increments any more than 1 past the end of a valid memory region (e.g. the end of a buffer) is instant undefined behavior even if the pointer is never dereferenced
- strict aliasing is on by default for pointers of differing types, but not for void/char/uchar. This means that having `struct sockaddr` and `struct sockaddr_in` pointers pointing to the same struct is UB.
The more I learn, the more I run from C.
You really should move to C23 and use ={}.
> If your machine runs out of memory, reversed[i] will cause a segfault upon being accessed. In order to avoid unhelpful segfaults, the program should check for a null pointer and gracefully exit if one is found
If overcommit is enabled (often by default), malloc will not return null when machine runs out of memory. malloc will still return null if input argument is invalid (e.g. too large).
It's becoming increasingly common. Rust is my first systems programming language too. I tried to learn C a few years ago but I found it too austere and prickly, which put me off.
For someone getting a CS degree, that just seems so, so odd. Along with that, he's been studying edge detection in images. In his second year. Not to run off topic but, again, I find that so, so odd.
Very reasonable imo as universitys educated people that get positions with responsibilitys.
Rust is taking steps or has been taking steps in this direction too.
It's not surprising that a lot of experienced systems and desktop development engineers looking to getting their hands on the newest tool already have experience or at least familiarity with C and C++.
https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3899.pdf
Many fast allocators do not store the size next to the allocation, and may not store the requested size at all. This has the advantage that freeing a large allocation does not fault in cold pages with a write, as well as reducing allocator memory overhead.
My experience with the process as a first time submitter has been pretty pleasant, all things considered.
Obviously this is untrue. Under GNU we have malloc_usable_size(3). What's true is that it never became standardized, but standards for just about anything in C are very bare, so it's not too surprising.
> nobody in decades thought to make this information accessible to the programmer
> otherwise free could not work
I'm inclined to say that you just answered your own question as to why.
I was always taught that "There are parts of a binary that are there for you, the lucky high-level language programmer. And then there are other parts of the binary which are there for the compiler to clean up after the spoiled little high-level language programmer who doesn't have to write their own assembly :)"
and
"If you want to argue with the compiler, that's what `gcc -S -fverbose-asm` and $YOUR_FAVORITE_TEXT_EDITOR` is for".
But I'm curious if this advice is unrealistic for said exotic usecase.
Do tell?
Also this information is of limited use to the programmer. There's no guarantee the buffer you get is going to be the size you asked for. Lots of allocators use nearest power of 2 sizes for small allocations.
For example mimalloc has separate regions by allocation size, so there's nowhere to put this info.
Isn’t this implementation details, and an information already available caller side. It would be like returning the filename for a file handle. The more extraneous details in an API, the less flexible it is.
I’d recommend anyone (including the author) wanting to understand C to read K&R’s “The C Programming Language”, which among other things will illustrate how iterating over pointers is idiomatic in C (though not quite in the way the author’s example does it).
https://docs.gtk.org/glib/data-structures.html#doubly-linked...
GSL GNU Scientific Library for C:
https://www.gnu.org/software/gsl/doc/html/intro.html
In general, no one should be custom building most basic structures in C these days. =3
Then there's the Trust issue. If something goes wrong, it's my fault if I wrote it. I'm not going to argue with people who reach for "You're holding it wrong" reflexively.
t. Has spent time reimplementing basic data structures just because.
That boolean is actually mostly an extension of the integer system whereby we now have an integer type that stores only one bit.
Whereby the bit stored either results in a 'true' or 'false' value.
Anyways, I know booleans are useful in systems development when you have strict memory / storage constrains / bandwidth (networks).
Yeah, that works for me.
CPUs don't have types, everything is integers or floats. You do your work on registers which have fixed sizes. CPUs have built in instructions for "is this register not zero" which leaks into C. 1 is true in C, but so is 2.
Also, single bits are rarely used for booleans because it requires more CPU power to extract a single bit. Everything is byte aligned at a minimum.
When doing something like network code, if you want to store a bunch of booleans you are typically going to either pack them into a byte, or you'll burn the extra bits and send a single byte for the boolean value. Typically this was flags and masks.
Not necessarily, on modern CPUs memory access is often the bottleneck. If you have many bits storing these in a bitarray can be quite beneficial for performance.
Regardless, due to the way most CPUs work, you almost certainly aren't saving anything by using a bit instead of a byte. When a CPU goes to load a memory address it'll not just get that address, but also the addresses around that address and shove them into cache. With the thought that "If you are loading 1 byte, you are likely loading 128 bytes". The cost savings really only comes into effect when you have more boolean values than bytes that fit in a cache line.
> single bits are rarely used for booleans because it requires more CPU power to extract a single bit
I don't believe this to be true; (on x86) `cmp X, 0` is exactly the same amount of operations as `test X, pow2`. One of them is a `sub` and one of them is an `and`. I believe this is how it works on all important CPU architectures of today back to the early microcomputers (although I don't know how the minicomputers handle this, so it's possible that booleans being integers makes more sense on a PDP-11 or a honeywell).
SQL Server still has no boolean type and groups bits in the same row into a byte if possible.
Also for most code it will be premature optimization to worry about that.
std::vector<bool> is a perfectly nice growable bit array type, and if the exact same code were in the C++ standard library named std::growable_bit_array nobody would be annoyed about this type, some people would use it, others would ignore it, nobody would write epic rants about it or name it the singe worst thing about C++.
The problem is that C++ popularized generics, and std::vector<T> is a generic growable array type, you ask for a std::vector<Goose> you get a growable array of your custom Goose type, great idea, very popular these days -- yet std::vector<bool> is not a generic growable array of bool, it's this other thing instead that's similar but not quite similar enough to be a drop-in replacement.
In C++ there is no way for the specialization to be bit-based without it being apparent that you are not in fact getting a growable array of the bool type.
Because that is the problem? That's what C++ programmmers are unhappy about.
> ideally we'd have a better API for vector that might require C++ language changes
Ideally after almost thirty years "maybe we could re-design the whole programming language to paper over this bug?" would be such an obviously bad idea that I wouldn't see it in a response.
I recommend not indicating you're rewording another commenter when you actually disagree with them if you don't want that difference in content pointed out. I note that you've trimmed out the parts of my comment that made it clear I was drawing that distinction. Maybe you missed those?
Separately, though, it's not accurate to call this "papering over [a] bug". Wanting to have a thing that behaves like a reference that can identify things that aren't addressable directly (ie: having custom reference representations) shows up in a surprising number of places as desirable because we use references (and things like references) as an abstraction in many programming languages to enable nicer, more usable syntax (ie: having assignments behave in a particular way).
Edit: and changing the language would have been at the time that std::vector was designed (ie: c++98-ish c++ has a real problem with abstracting over not-really-references). With current c++, we can get a reference-like-object much closer to being fully transparent (`auto&&`, ranges iterator model, etc). But std::vector regrets reflect the constraints of the time when it was initially designed/standardized. C++ doesn't have a good story for making breaking changes, so if folks want a good vector for bits, today they'll go elsewhere.
I can see why you're upset but KerrAvon describes their statement as the result of "a cursory web search" and it isn't false so I wanted to be clear that what they wrote was not wrong per se. I don't think I "disagree" with a person who used Google, read some stuff and came away with an emphasis on the technical reasons this is a problem rather than how it's actually a problem for real programmers.
Maybe I do disagree with you, that's very possible.
With current C++ you could design a specialization that's somwhat less crap. But what's actually important - and what would actually happen - is that you would not standardize such a specialization at all because everybody knows it's a bad idea to do this - it would still be crap if you tried today, just less crap than in 1998.
C++ chooses to have a bad story for breaking changes. That's a deliberate choice and should be understood that way. WG21 are not hapless victims.
Meaning, if you have a struct and you want to bit-pack your booleans, you can declare each one as, say, uint8_t some_bool : 1;
You may then do `x.some_bool = true;` etc.
It's a small nicety to avoid bitwise operators, anyway.
I suspect this is because they are usually introduced like "here are bitfields, but DO NOT USE THEM for anything that might become ABI because their order is implementation defined". I think that causes people to mentally file it under <sketchy language features to avoid> when actually there are many contexts where the ABI risk is not real.
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
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: 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.
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.
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
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:
I believe the following is the correct (or less incorrect) way to do it: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
[2]: https://port70.net/%7Ensz/c/c89/c89-draft.html#3.3
[3]: https://port70.net/%7Ensz/c/c23/n3220.html#6.5.1
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. :)
Fluent C: Principles, Practices and Patterns by Christopher Preschern - https://www.oreilly.com/library/view/fluent-c/9781492097273/
BUT, I guess this comes close https://fasterthanli.me/articles/a-half-hour-to-learn-rust
When I was building my kernel in my late teens, I first wrote the bootloader by hand on paper in assembly language. I then referenced the x86 manual for the instruction set and converted my assembly code into the equivalent hexadecimal machine code values of the x86 machine instructions, which I also wrote by hand on paper. Then I used a hex editor on the desktop to manually write the hex values into a file and used it as the bootloader i.e as the first 512 bytes.
The whole exercise gave me a sense of hard grounded zero magic, raw, unfiltered experience. This is an experience that is hard to replicate in any other way. I deliberately did that so as to peel away as much magic/abstraction layers as I possibly can.
Later on when I started using C, I never had to learn C but merely just had to reference the equivalents of the assembly language. Like how the primitive "if" doesn't exist in the hardware but is a composition of cmp and jmp instructions. Seen this way, C becomes a glorified portable syntactic sugar over assembly language.
Then higher up the ladder to C++ for object oriented problem solving while retaining the spirit of functional programming. Rust was a breath of fresh air, where correctness across a myriad of use cases was a first class primitive.
Each abstraction layer can thus be evaluated for its utility in problem solving while its underlying mechanics remain understandable down to the hardware level.
More recently, our own arcc compiler extends correctness to our architecture and not just the types.
So if you are young and have time to spare, I suggest a little bit of Assembly => C => C++ => Rust.
This essentially makes you immune to hype train bullshit.
(I say this as an aspiring C major pianist.)
... but that ship has already sailed
[obligatory] The original "C for ... programmers" article that convinced the world to make C as pervasive as it is today is K&R's "The C Programming Language".
I know it's in book form (egad!) and if you download a PDF, it's like 80 pages (double egad!). But it makes for surprisingly light reading.
It’s quirky because when you had kilobytes of RAM, storing the length of the string was memory you couldn’t afford. Arrays decaying into pointers similarly meant carrying a single pointer around rather than a fat struct. Magic integers for return codes were used because they’d fit into a single register. Compilers didn’t have the processing power or memory to track complex borrow rules or static analysis. They were slow even as they were! Old processor architectures were way more diverse and fragmented than now - and even 30 years ago endianness wasn’t a settled thing.
Things weren’t quirky because people “wanted” them to be, they were quirky because everything was way more constrained. Rust came into a way more rationalised environment and it’s a great language of course but the operating environment is so totally different it’s hard to compare.
It's also a shame the article uses the self-delusional C++ style of pointer declarators.
Otherwise pretty okay.
On a typical PC that doesn't seem like a problem, and it will (at least kinda) work which might give you the false impression it's required to work, which it very much is not in Rust. On CHERI it's obvious why this can't work. CHERI's pointers are 128-bit. Rust does have 128-bit integers, but Rust's isize and usize on CHERI will be 64 bits. Because only half of CHERI's pointer bits are address bits, and Rust told you that isize and usize were big enough for the address not the whole pointer.
Many clever pointer tricks only want to fiddle with the address. For example hiding bit flags in an aligned pointer works, as does hiding the entire value inline in today's enormous pointers (64 bits! Luxury) and using a single bit to mark "not a real pointer". In Rust we do these with the actual raw pointer types, they have methods like any other type, but in C or C++ you need to convert to a pointer-sized integer and then do tricks with the integer or you will write UB.
As I said, these types are the same width as an address on the target but a CHERI pointer isn't just an address, that's why they are so wide.
Maybe start here: https://doc.rust-lang.org/std/ptr/index.html#strict-provenan...
The strict provenance APIs or the "Rust has provenance" RFC are not relevant here (more precisely, they help clarify the options but do not solve the problem).
The problematic statement is still in The Reference (https://doc.rust-lang.org/reference/type-layout.html):
> Pointers to sized types have the same size and alignment as `usize`.
Which just cannot be guaranteed on CHERI with 64-bit `usize`.
There are discussions like there were before, and it's pretty clear that we'll have to give up either performance or possibly quite a lot of compatibility, but no decision.
I suppose one of the things you could do, at least on Morello, is treat it entirely as Arm64 in all your Rust but offer some CHERI flavouring in FFI for people who had to do their memory safety in hardware. In such a world that statement is just true.
You can make CHERI a non-conforming target. In fact we already have some (did you know x86-32 is a non-conforming target?). This is one of the leading options. The question is whether we don't give up too much existing code by that. No existing target is that incompatible.
AIUI Strict Provenance rules imply that you can't expect any such tricks to work unless the target promised they would work.
You say "People rely on it" and I must ask: How many people and how sure are you that they actually can't live with Strict Provenance ?
One thing that stood out to me going this direction is it's really hard to write hackish or intentionally-broken code in order to try to explore implementation options or debug an issue or test an algorithm. Rust requires you to handle error cases, and all possible values in cases, and is strict about memory access and stuff. But sometimes I just want to shove these bytes into this thing and see what happens, or hack in an early return or something, or half-implement this API to see if it "feels right" before committing to a full implementation, and Rust's enforced best practices stop me doing that.
(To be clear, I like Rust. This is just one thing that trips me up coming from C.)
There's a _lot_ of utility in things that aren't represented by a literal difference in the compiled code. It's not unusual to make a type in Rust that's just a single field; you're not making a struct to gather related data together, you're giving it a different name because the same data means something different.
It's a little like the difference between a uintptr_t and a uint16_t*. They're represented the same in the machine, the difference only exists in the source language.
Rust derives a lot of value from things that only mean something at compile time. I think you'd "unlearn" what C (commonly) teaches when you don't find a C struct with one member "weird".
With Rust, you are front loaded with a myriad of compiler gymnastics you need to think through.
But once you get comfortable enough, it becomes natural. You also have to get accustomed to writing code that is more verbose than C which might look ugly at first but later you start to accommodate it as the necessary cost for the utility you are handed in return by the compiler.
For example, multiple variables in Rust doesn't necessarily mean multiple memory allocated variables at runtime like in C. The rust compiler will usually keep track of and ensure multiple variables (non Copy types such as String with Move semantics) map to one memory allocated variable at runtime (in normal single threaded use cases under normal circumstances without using RC, ARC, etc.). Eg: let a = String::from("hello"); let b = a; ... Note: The example is for illustration purposes only and not always true. In summary source code variables are abstractions and may not belong to distinct runtime memory location. Yes it is true even for C. But Rust's ownership model makes that distinction aggressively visible.
Nowadays I actually believe I'll likely see a world without memory corruption within my lifetime.
Yeah I was thinking of prefixing my "memory corruption" with "software-bug induced"! I don't see a credible solution to Rowhammer. ("DDR[n+1] fixes it" - lol)
> an unsafe block, an FFI boundary
Honestly these feel solvable to me at this point! I think we'll see:
- unsafe code shrink as languages get more powerful
- amount of analysis we can apply to each unsafe line shoot up exponentially as AI gets cheaper
- amount of FFI we actually need shrink as it gets easier to just click "rewrite it in $lang" on the decision card when your coding agent says "I found a library for that but it's in a different language"
(Having said all of that, people seem to be adopting Zig for some bizarre reason... So maybe I'm naive to expect unsafe lines to shrink)
(But also, maybe AI gets so good and so cheap that we can just type "go fidn all the bugs andfi xthenm" into an LLM, between sips of a Piña Colada)
The biggest reason I use Zig is it's a very explicit language. The creators made a very intentional decision to avoid too many "high level" designs. This doesn't mean there's no capabilities for abstraction (comptime is great for that), but when you see array indexing, you can think "ptr + index * size with bounds check". There's lots of other things like that where the language does exactly one thing, and that thing is a low level operation.
This is terrible when you want to create high level abstractions that hide details from the programmer, but it's what I need when doing realtime audio synthesis or what I'm doing now which is writing an interpreter. I know exactly what allocates, I know what calls IO and can block (both operations explicitly take in an allocator or IO parameter), no data structures have private fields so I can always poke around at the insides. I know what types of errors a function returns, and creating errors is cheap with Zig's error union design.
So I don't use Zig because I think it's the safer language, I know it has sharp edges because of the number of times I've caused a panic on a poisoned pointer or use-after-free. But because it gives me such a transparent view into what is happening I find it liberating.
The first is that it is "C is fucked up" instead of the poor defaults of the C implementation your are using.
The second one is the idea that C all has to be fragile low-level pointer fiddling instead of much safer high-level code build around abstractions, which good C would usually have.
I do actually think that with modern C++ it's possible to get to a style of programming where memory unsafety is mostly a theoretical concern but not with C.
For me, memory safety is also mostly a theoretical concern in C. This is achieved by having proper abstractions instead of low-level pointer fiddling and by having a clear strategy for managing lifetimes.
I don't see rust a "memory safe" language, or a niche one. I have complaints about it etc, but it's overall a fair baseline of reasonable decisions. When I look at C or other languages rust has learned from, I have more "Yikes, that's rough" takes. So... rust as the language of least "fucked up", to use your phrase? Ownership/safety are one part of the picture, but not what defines it for me.
edit: I think this is a bot or troll account.
In lieu of that it didn't fuck up.
The real value of Rust is likely pattern matching and tagged unions. Apart from modules/packages that are not a disaster, like Gabriel Dos Reis the saboteur's disaster with modules in C++.
For what it's worth, in that specific instance there's no memory safety issue since Rust is guaranteed to crash on stack overflow on Ubuntu (and probably other supported Linux distros)
Why would a Rust programmer learn C? Isn't that basically a regression?
Write, say, a boot loader, for enlightement.
That also reflects in that many embedded systems offer C support and do not offer Rust support.