Rendered at 20:07:10 GMT+0000 (Coordinated Universal Time) with Cloudflare Workers.
uecker 8 hours 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 7 hours 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.
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.
bjackman 7 hours ago [-]
> This is not a substitute for a proper C tutorial
kingforaday 7 hours 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.
jminnl 6 hours ago [-]
Asan!
phamilton 6 hours 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 5 hours 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 5 hours 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 4 hours 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 hours 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).
creata 5 hours ago [-]
As fun as it is, 1[c] will be "marked obsolete" in C29 according to Wikipedia.
mr_00ff00 6 hours ago [-]
All fun and games with arrays being pointers, until you declare an array in a function and return it.
jackling 6 hours 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.
skydhash 5 hours 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.
At end of day minimalism is a neat but not decisive feature. If minimalism was decisive we'd all be writing Brainfuck.
brabel 5 hours 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.
Ygg2 2 hours 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.
KerrAvon 5 hours 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.
skydhash 5 hours 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.
mathisfun123 5 hours ago [-]
> One of my favorite things to show
Who have you shown this to? What kind of person is impressed by this? Anyone that programs in any other language already knows that syntax is fungible so who cares if C chooses to use addition and brackets this way. So the only people that might be impressed by this are people who don't program. In which case why are you showing them lol.
For example (assuming you're a C programmer) are you impressed by this python syntax
[a] * 3 == [a, a, a]
junon 5 hours ago [-]
Man just let people enjoy small things.
mathisfun123 5 hours ago [-]
I don't know what you're implying - I asked a genuine question: what is impressive about that syntax.
1718627440 5 hours 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 5 hours 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 hours 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.
rini17 2 hours 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.
ReDress 8 hours 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 7 hours 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 5 hours 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.
xhroot 6 hours 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 7 hours 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.
viega 6 hours 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.
tyromaniac 7 hours ago [-]
Or pull out the C++ and use std::vector<bool>,... But you should probably never do that
randomNumber7 6 hours 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 5 hours 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 4 hours 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.
ReDress 7 hours ago [-]
[dead]
layer8 5 hours 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
pwdisswordfishq 3 hours ago [-]
Yes. And this is what C++ iterators evolved from.
kvemkon 6 hours 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 5 hours 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 4 hours 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 19 minutes 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!
layer8 5 hours 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 4 hours ago [-]
But in order to able to show proper diagnostics and error messages, you should still return all the way to the top.
im3w1l 4 hours 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 1 hours 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 22 minutes 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.
aw1621107 53 minutes 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)
steveklabnik 24 minutes ago [-]
(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.)
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?
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 way to do it:
char tmp[sizeof(int)];
memcpy(tmp, &i, sizeof(int));
char c = tmp[1];
bjackman 7 hours 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.
cohani 6 hours 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++.
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)
nilslindemann 5 hours ago [-]
Were you intentionally rude with your first sentence or just unaware?
cohani 4 hours ago [-]
You are being worse than rude, while I am being accurate and not rude. Kindly do better.
TripolitianFish 4 hours ago [-]
Jesus Christ you suck.
wannabe44 3 hours 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 hours ago [-]
What would you have preferred the devs work on instead?
the__alchemist 6 hours 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 6 hours ago [-]
[flagged]
the__alchemist 4 hours 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 5 hours 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.
kingforaday 7 hours 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 7 hours 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 4 hours 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.
_dain_ 8 hours 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 7 hours 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.
le-mark 6 hours 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 5 hours 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 4 hours 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 8 hours 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++.
rramadass 5 hours ago [-]
Read the following if you want to be a C Programmer;
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 4 hours 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.
4 hours ago [-]
elendilm 5 hours ago [-]
My own journey has been quite different.
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.
jmclnx 6 hours ago [-]
Really just another use Rust instead of c article.
BitProgram 2 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?
elendilm 3 hours 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.
caaqil 6 hours 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?
cohani 6 hours 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 6 hours 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.
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.
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.
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
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.
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.
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.
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.
Who have you shown this to? What kind of person is impressed by this? Anyone that programs in any other language already knows that syntax is fungible so who cares if C chooses to use addition and brackets this way. So the only people that might be impressed by this are people who don't program. In which case why are you showing them lol.
For example (assuming you're a C programmer) are you impressed by this python syntax
- 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.
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.
SQL Server still has no boolean type and groups bits in the same row into a byte if possible.
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.
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.
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
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 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.
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?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 way to do it:Nowadays I actually believe I'll likely see a world without memory corruption within my lifetime.
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)
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.
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.
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++.
Fluent C: Principles, Practices and Patterns by Christopher Preschern - https://www.oreilly.com/library/view/fluent-c/9781492097273/
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.
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.
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.
Why would a Rust programmer learn C? Isn't that basically a regression?
That also reflects in that many embedded systems offer C support and do not offer Rust support.