There's one area I wish we did differently which I think is a hang-over from big-endian. It's the order of bytes when we write out hex dumps of memory.
!tfel-ot-thgir eb dluow ,redro dleif sa llew sa ,sgnirts lla neht tuB
It could be argued that little endian is the more natural way to write numbers anyway, for both humans and computers. The positional numbering system came to the West via Arabic, after all.
Most of the confusion when reading hex dumps seems to arise from how the two nibbles of each byte being in the familiar left-to-right order clashes with the order of bytes in a larger number. Swap the nibbles, and you get "43 21", which would be almost as easy to read as "12 34".
Yep. We even have a free bit when writing hex numbers like 0x1234. Just flip that 0x to a 1x to indicate you are writing in little-endian and you get nice numbers like 1x4321 that are totally unambiguous little-endian hex representations.
You can apply that same formatting to little-endian bit representations by using 1b instead of 0b and you could even do decimal representations by prefixing with 1d.
For me I think the issue is the way you think of memory.
You can think of memory are a store of register sized values. Big endian sort of make some sense when you think of it that way.
Or you can think of it as arbitrarily sized data. It's arbitrary data then big endian is just a pain the ass. And code written to handle both big and little endian is obnoxious.
> - we have authentication everywhere in our stack, so I've started including the user id on every log line. This makes getting a holistic view of what a user experienced much easier.
Depends on the service, but tracking everything a user does may not be an option in terms of data retention laws
It is intended for release builds. The ReleaseSafe target will keep the checks. ReleaseFast and ReleaseSmall will remove the checks, but those aren't the recommended release modes for general software. Only for when performance or size are critical.
There are devices that do not have access to memory, and you can write a safe description of such a device's registers. The only thing that is inherently unsafe is building DMA descriptors.
I personally feel the Zig is a much better fit to the kernel. It's C interoperability is far better than Rust's, it has a lower barrier to entry for existing C devs and it doesn't have the constraints the Rust does. All whilst still bringing a lot of the advantages.
...to the extent I could see it pushing Rust out of the kernel in the long run. Rust feels like a sledgehammer to me where the kernel is concerned.
It's problem right now is that it's not stable enough. Language changes still happen, so it's the wrong time to try.
From a safety perspective there isn't a huge benefit to choosing Zig over C with the caveat, as others have pointed out, that you need to enable more tooling in C to get to a comparable level. You should be using -Wall and -fsanitize=address among others in your debug builds.
You do get some creature comforts like slices (fat pointers) and defer (goto replacement). But you also get forced to write a lot of explicit conversions (I personally think this is a good thing).
The C interop is good but the compiler is doing a lot of work under the hood for you to make it happen. And if you export Zig code to C... well you're restricted by the ABI so you end up writing C-in-Zig which you may as well be writing C.
It might be an easier fit than Rust in terms of ergonomics for C developers, no doubt there.
But I think long-term things like the borrow checker could still prove useful for kernel code. Currently you have to specify invariants like that in a separate language from C, if at all, and it's difficult to verify. Bringing that into a language whose compiler can check it for you is very powerful. I wouldn't discount it.
> Zig, for all its ergonomic benefits, doesn’t make memory management safe like Rust does.
Not like Rust does, no, but that's the point. It brings both non-nullable pointers and bounded pointers (slices). They solve a lot of problem by themselves. Tracking allocations is still a manual process, but with `defer` semantics there are many fewer foot guns.
> I kind of doubt the Linux maintainers would want to introduce a third language to the codebase.
The jump from 2 to 3 is smaller than the jump from 1 to 2, but I generally agree.
> I kind of doubt the Linux maintainers would want to introduce a third language to the codebase.
That was where my argument was supposed to go. Especially a third language whose benefits over C are close enough to Rust's benefits over C.
I can picture an alternate universe where we'd have C and Zig in the kernel, then it would be really hard to argue for Rust inclusion.
(However, to be fair, the Linux kernel has more than C and Rust, depending on how you count, there are quite a few more languages used in various roles.)
All logic, all state management, all per-device state machines, all command parsing and translation, all data queues, etc. Look at the examples people posted in other comments.
Yep. Its kind of remarkable just how little unsafe code you often need in cases like this.
I ported a C skip list implementation to rust a few years ago. Skip lists are like linked lists, but where instead of a single "next" pointer, each node contains an array of them. As you can imagine, the C code is packed full of fiddly pointer manipulation.
The rust port certainly makes use of a fair bit of unsafe code. But the unsafe blocks still make up a surprisingly small minority of the code.
Porting this package was one of my first experiences working with rust, and it was a big "aha!" moment for me. Debugging the C implementation was a nightmare, because a lot of bugs caused obscure memory corruption problems. They're always a headache to track down. When I first ported the C code to rust, one of my tests segfaulted. At first I was confused - rust doesn't segfault! Then I realised it could only segfault from a bug in an unsafe block. There were only two unsafe functions it could be, and one obvious candidate. I had a read of the code, and spotted the error nearly immediately. The same bug would probably have taken me hours to fix in C because it could have been anywhere. But in rust I found and fixed the problem in a few minutes.
I don't understand why. Working with hardware you're going to have to do various things with `unsafe`. Interfacing to C (the rest of the kernel) you'll have to be using `unsafe`.
In my mind, the reasoning for rust in this situation seems flawed.
The amount of unsafe is pretty small and limited to where the interfacing with io or relevant stuff is actually happening. For example (random selection) https://lkml.org/lkml/2023/3/7/764 has a few unsafes, but either: a) trivial structure access, or b) documented regarding why it's valid. The rest of the code still benefits.
I’ve done a decent amount of low level code - (never written a driver but I’m still young). The vast majority of it can be safe, and call into unsafe wrapped when needed. My experience is that a very very small amount of stuff actually needs unsafe and the only reason the unsafe C code is used is because it’s possible not because it’s necessary.
It is interesting how many very experienced programmers have not yet learned your lesson, so you may be young but you are doing very well indeed. Kudos.
Either way, the point you are making is an excellent one. Discipline makes for better programming, and to not use the features available to you is very often the right choice.
Unsafe in Rust doesn't mean anything goes. Specifically it means that you are going to 1) dereference a raw pointer; or 2) call an unsafe function/method; or 3) access/modify a mutable static variable; or 4) implement an unsafe trait; or 5) access fields of a union.
You still get the safety guarantees of Rust in unsafe code like bounds checking and lifetimes.
Why does that matter? Rust with some "unsafe" is still much nicer to use than C.
In fact one of the main points of Rust is the ability to build safe abstractions on top of unsafe code, rather than every line in the entire program always being unsafe and possibly invoking UB.
Learn rust to a level where all cross language implications are understood, which includes all `unsafe` behaviour (...because you're interfacing with C).
You'll always get something like this:
``` 00000000 : 00 01 02 03 04 05 06 07 00000008 : 08 09 0A 0B 0C 0D 0E 0F ```
On a big-endian machine, when you wrote 0x1234 to address 0x0000000 you got:
``` 00000000 : 12 34 02 03 04 05 06 07 00000008 : 08 09 0A 0B 0C 0D 0E 0F ```
On a little-endian machine you have to either do mental gymnastics to reorder the bytes, or set the item size to match your data item size.
``` 00000000 : 34 12 02 03 04 05 06 07 00000008 : 08 09 0A 0B 0C 0D 0E 0F ```
If we wrote the bytes with the LS byte on the right (just as we do for bits) then it wouldn't be an issue.
``` 00000000 : 07 06 05 04 03 02 12 34 00000008 : 0F 0E 0D 0C 0B 0A 09 08 ```
reply