C for Rust programmers (bd103.dev)
uecker 14 hours ago
kingforaday 13 hours ago
bjackman 13 hours ago
swinglock 13 hours ago
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.
cohani 12 hours ago
tialaramex 11 hours ago
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.
jminnl 12 hours ago
gerdesj 3 hours ago
Tribalism is sooo ... sadly now.
Please don't.
bennettnate5 8 hours ago
- 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.
jan_m_savage an hour ago
weinzierl 40 minutes ago
(I say this as an aspiring C major pianist.)
zeroq 27 minutes ago
... but that ship has already sailed
phamilton 12 hours ago
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.mr_00ff00 12 hours ago
jackling 11 hours ago
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 12 hours ago
At end of day minimalism is a neat but not decisive feature. If minimalism was decisive we'd all be writing Brainfuck.
brabel 11 hours ago
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 8 hours ago
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.
WalterBright 2 hours ago
The rise of C in the 1980s induced CPU makers to design instructions that cater to C semantics.
KerrAvon 11 hours ago
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 10 hours ago
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 6 hours ago
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 10 minutes ago
'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.
pwdisswordfishq 11 hours ago
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 11 hours ago
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 10 hours ago
ynik 8 hours ago
skydhash 11 hours ago
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.
creata 10 hours ago
rini17 8 hours ago
skydhash 3 hours ago
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.
nofriend 2 hours ago
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.
ReDress 14 hours ago
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.
bjackman 13 hours ago
tyromaniac 13 hours ago
randomNumber7 12 hours ago
Also for most code it will be premature optimization to worry about that.
KerrAvon 11 hours ago
tialaramex 10 hours ago
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.
viega 12 hours ago
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.
cogman10 13 hours ago
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.
xhroot 12 hours ago
SQL Server still has no boolean type and groups bits in the same row into a byte if possible.
quikoa 11 hours ago
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.
layer8 11 hours ago
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).
Joel_Mckay 10 hours ago
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
pwdisswordfishq 9 hours ago
bjackman 13 hours ago
Nowadays I actually believe I'll likely see a world without memory corruption within my lifetime.
kingforaday 13 hours ago
bjackman 13 hours ago
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 10 hours ago
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__alchemist 12 hours ago
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 12 hours ago
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 11 hours ago
wannabe44 9 hours ago
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 Lectureaw1621107 8 hours ago
slopinthebag 3 hours ago
aw1621107 8 hours ago
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 hours ago
slopinthebag 3 hours ago
kvemkon 12 hours ago
If malloc() fails, there is no need to exit the program completely with exit(), only return from the current function with an error.
randomNumber7 11 hours ago
So it is a good advice for beginners.
layer8 11 hours ago
1718627440 10 hours ago
im3w1l 10 hours ago
smj-edison 10 hours ago
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 6 hours ago
smj-edison 5 hours ago
steveklabnik 4 hours ago
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 hours ago
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.
eesmith 7 hours ago
I think it's because the new_uninit_slice call Rust will trigger a panic? Or abort? With little-to-no chance for recovery? (I know little about Rust.)
If so, I can see why someone that someone coming from Rust might consider exit() to be the appropriate solution for C, even for library code which should never be in charge of deciding how a program should exit.
I think the essay could be improved by highlighting the different worldviews.
I'm also old enough that
// Allocate enough room for the string and its null terminator.
char* reversed = malloc(len + 1);
makes me nervous. Even for char -- I've never been on a system where sizeof(char) != 1 -- I want to see the sizeof included in the calculation, like: char* reversed = malloc((len + 1) * sizeof(*reversed))
so I don't have to think about sizeof(char) being special.As long as I'm here, I'm a bit confused about the purpose of the "char* error_message" in the proposed Result. Why a char* vs a const char * or even better, an int with an error code? Who sees the message? Do we expect they know English, or will they be localized? Will the error message text be frozen forever, or might it change in the future?
aw1621107 7 hours ago
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 6 hours ago
eesmith 6 hours ago
https://smd.hu/Data/Analog/DSP/SHARC/C&C++%20Compiler%20&%20... says the cc21k compiler for ADSP-21xxx DSP systems has char as 32 bits signed, and that the compiler handles ANSI/ISO standard C.
So I don't believe your statement "sizeof(char) is guaranteed to always be 1" is correct.
> chars are also how you access the byte-level representation of objects in C
Where does the spec say that a char can be used to address any point in an object?
There's all sorts of oddities like tagged architectures which the C spec handles which I know essentially nothing about, but which break common expectations about how C works. I believe this is one of them.
I believe the following is undefined behavior in C, even though your compiler may let you do it, at least sometimes, and on modern desktop hardware:
int i = 12345;
char *s = ((char *)&i) + 1;
char c = *s;
I believe the following is the correct (or less incorrect) way to do it: char tmp[sizeof(int)];
memcpy(tmp, &i, sizeof(int));
char c = tmp[1];aw1621107 4 hours ago
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
steveklabnik 6 hours ago
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 an hour ago
_dain_ 14 hours ago
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.
ReDress 14 hours ago
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++.
assimpleaspossi 13 hours ago
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 12 hours ago
randomNumber7 11 hours ago
Very reasonable imo as universitys educated people that get positions with responsibilitys.
creata 10 hours ago
rramadass 11 hours ago
Fluent C: Principles, Practices and Patterns by Christopher Preschern - https://www.oreilly.com/library/view/fluent-c/9781492097273/
blourvim 12 hours ago
pwdisswordfishq 11 hours ago
It's also a shame the article uses the self-delusional C++ style of pointer declarators.
Otherwise pretty okay.
tialaramex 10 hours ago
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 hours ago
jmclnx 12 hours ago
elendilm 11 hours ago
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.
BitProgram 3 days ago
elendilm 9 hours ago
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 12 hours ago
Why would a Rust programmer learn C? Isn't that basically a regression?
orbitaldesk 12 hours ago
cohani 12 hours ago
That also reflects in that many embedded systems offer C support and do not offer Rust support.