Arbitrary sized integers do sound very convenient, although I guess the counterargument would be that they simply can't map to machine types so you'll have to have magic behind the scenes which may not make complete sense in low level languages. Still, C has bitfields in structs which are sort of the same thing so why not.
Pointer handling however I'm not sold on. I'd argue that the very cumbersome pointer handling in Rust is a feature, not a bug. It's explicitly discouraged, and for good reasons. Map your pointer into a clean and safe reference, slice or wrapper object ASAP and leave raw pointers to the messy details of C FFIs and ultra low-level code. If you end up having to mess with raw pointers everywhere in your codebase you're doing it wrong, as far as Rust is concerned.
I think this is reasonable even for kernel code. After all typically you wouldn't dereference register pointers directly in C code because of volatility issues and making it obvious that you're accessing hardware and not RAM (readl/writel or similar), so having a safe wrapper adds no overhead in my experience.
* I don't think that low level code needs to be "messy", and
* I don't think that adding an abstraction on top solves anything complexity wise.
For memory paging I used to have a whole library in Rust that handled hierarchy of page tables with very abstract pagetable classes and interfaces [0]. Now I just have 80 lines of zig that handles everything [1]. I much prefer the latter.
> I think this is reasonable even for kernel code.
It's not, for some of the reasons that Linus doesn't want C++ code in the kernel [2]
[0] https://github.com/gz/rust-x86/blob/28d5839933973e0b639ef354...
[1] https://git.sr.ht/~jzck/kernel/tree/master/src/arch/x86/pagi...
I don't think what Linus wrote in 2004 on C++ has much relevance for 2020 Rust, especially for a new kernel that doesn't have a ton of contributors. Most of his complaints seem to be on how C++ abstractions can make it hard to review unknown code and how 2004 C++ code bases often had extremely messy code. Rust IMO doesn't suffer from those problems near as much as 2004 C++ did.
Also, Google is using Rust for part of their new Fuchsia kernel so at least they think Rust has something good to offer kernel dev.
No, it's written in C++[1]. They're very different languages.
That said, Google seems to think C++ has something to offer to all kinds of development. It seems to work for them, but they are a heavily C++ shop. I wouldn't read too much out of their use of C++ anywhere; it'd be more surprising and interesting to see them use anything else.
[1]: https://fuchsia.googlesource.com/fuchsia/+/master/zircon/ker...
[0]: https://fuchsia.dev/fuchsia-src/concepts/kernel/zx_and_lk
Output:
Test [1/1] test "bit field access"...
*align(:3:1) u3
All 1 tests passed.
Zig's pointer type has optional metadata to describe alignment, sub-byte offset, and SIMD vector element index. This generally makes things just work, and you'll get a compile error if you expected an unadorned aligned pointer. If I were to swap: - fn deref(ptr: var) u3 {
+ fn deref(ptr: *u3) u3 {
Then I'd get: test.zig:13:23: error: expected type '*u3', found '*align(:3:1) u3'
assert(deref(&data.b) == 2);
^This is the main risk with bitfields in C, it makes reads and writes that appear to be independent actually access the same memory cell behind the scenes. I'm not convinced by the idea of adding even more magic on top of it to be honest, it is true that packing and unpacking bitfields is annoying and requires some boilerplate but in the end that is how it the hardware does it anyway.
Have a look at Project Oberon, Singularity, Interlisp-D, Smalltalk, Modula-3 Topaz, Blue Bottle (AOS), CosmosOS, lilith (Crystal variant) given that their source is available.
> I don't know Nim, but I believe it has a garbage collector so it could be tricky to use for kernel programming.
You're right. Still good for libraries though (or apps, but that may be outside of "system").
This is what I use for my own multithreading runtime in Nim and the memory subsystem makes it faster and more robust than any runtime (including OpenMP and Intel TBB) that I've been benchmarking against, see memory subsystem details here: https://github.com/mratsim/weave/tree/master/weave/memory
Example of atomic refcounting in this PR here: https://github.com/mratsim/weave/blob/025387510/weave/dataty...
Also one important thing, Nim current GC is based on TLSF (http://www.gii.upv.es/tlsf/) which is a memory reclamation scheme for real-time system, it provides provably bounded O(1) allocations. You can tune Nim GC with max-pauses for latency critical applications.
Also, the new ARC memory manager replaces GC and can run in a kernel.
As soon as you use sequences or strings or async you depend on the GC.
You can however compile with gc:destructors or gc:arc so that those are managed by RAII.
I meant that new ARC GC, that will replace the current one, can be used for the kernel.
It's still a GC, technically, but, quoting Araq on ARC:
Nim is getting the "one GC to rule them all". However calling it a GC doesn't do it justice, it's plain old reference counting with optimizations thanks to move semantics.
otherwise you should be able to use something like destructors eventually
Then I guess you would use new/dealloc
https://github.com/vitreo12/omni
I don't know enough to comment but it may be useful to look at things in the wild. This project also heavily relies on calling in C to interface with environments and the SuperCollider scsynth.
But you can also think of Nim as macro-able higher level C and write it like that (but indeed you probably still need this minimal allocation support)
That's really cool.
I apologize if this is a naive question. Might you or someone else elaborate on where arbitrary sized integers are used or necessary in kernel programming? Don't they all ultimately need to get padded out for the CPU registers to work with them anyway?
Proof (and they have a policy to only use unsafe when not otherwise possible):
https://github.com/redox-os/kernel/search?q=unsafe&unscoped_...
saying "if you write your kernel in rust, it will be memory safe" is a little goofy, because writing kernels in rust requires you to drop into unsafe blocks way too often.
Unsafe is definitely used, but it does not mean what I think is being implied, which is that it makes the software unsafe.
Source code is available.
I personally think the Rust community has missed the biggest advantage of unsafe, and that's tooling. Imagine an IDE that could highlight and warn when touching state that is accessed in unsafe blocks, or when state being accessed in unsafe blocks isn't being properly tested.
Or tools that kick off notification for a required code review because code that touches state used by an unsafe block got changed.
To me, these sorts of things will have a far greater effect on security and stability than just the existence of the unsafe block in code.
The rest are just plugins and tools that look for said blocks and “do stuff”. I don’t see why these tools should be part of the language.
Another poster responded to me stating they are starting to work on those tools, so it looks like the rust community is starting to come around.
It's just that I've seen a lot of people tout the safety of rust as if the unsafe keyword was a panacea that automatically prevented problems. That attitude is really what I was responding to.
The Rust community can be a bit... fanatical, lets say. I've seen so many people argue that the unsafe keyword by itself makes rust so much safer than alternatives, when really safe code can cause unsafe code to explode. So you end up writing modules to protect the unsafe code, which is what you do in any other language as well.
Which means the unsafe keyword is valuable, just not nearly as valuable as I've seen a lot of people claim.
Now the unsafe keyword combined with the sort of tooling I've mentioned? That to me is a killer combination. Unsafe isn't a silver bullet, it still requires work, but it enables tools to make that work immensely easier to deal with.
I've talked about this at length before, but just because you have 'less' `unsafe` code does not mean you code is any better off. Very few operations are actually `unsafe`, and you're still not allowed to break any of the (poorly or not documented) constraints that Rust requires even in `unsafe` code.
Point being, unless you can make "safe" abstractions around your `unsafe` code that cannot be broken (Which is largely what you do in any language anyway, when you can...), the distinction between "safe" and "unsafe" is thin, because the parts of the code you declare `unsafe` are not the parts that are actually going to have the bugs. And because of the fact that the constraints you are required to fulfill in `unsafe` code is unclear, there's little way to guarantee your code is broken in some subtle way the optimizer might jump on (either now, or in a later release).
And to be clear, I like Rust, but `unsafe` is poorly thought-out IMO.
Sure, but now we're back to the issue that it's unclear what constraints `unsafe` code actually has to hold, meaning ensuring your abstraction is safe is just about impossible to do with certainty (And OS kernel code is definitely going to hit on a lot of those corner cases). You may think you have a safe interface, only to find out later you're relying on internal details that aren't guaranteed to stay the same between compiler versions or during optimizations.
With that said, while I agree with what you're proposing about marking surrounding code "unsafe", it leads to lots of strange cases and most people get it wrong or will even disagree with this proposal completely. For example, it can lead to cases where you mark a function `unsafe` even though it contains nothing but "safe" code. And at that point, it's up to you to determine if something is actually "safe" or "unsafe", meaning the markings of "safe" and "unsafe" just become arbitrary choices based on what you think "unsafe" means, rather than marking something that actually does one of the "unsafe" operations.
One of the best examples is pointer arithmetic. It's explicitly a safe operation, but it's also the first things most people would identify as being "unsafe", even though it is dereferencing that is the "unsafe" operation. Ex. You could easily write slice::from_raw_parts without using any `unsafe` code at all, it just puts a pointer and length together into a structure (it doesn't even need to do arithmetic!). It's only marked `unsafe` because it can break other pieces of "safe" code that it doesn't use, but it itself is 100% safe. You could just as easily argue the "other code" should be the `unsafe` code, since it's what will actually break if you use it incorrectly.
Perhaps the biggest annoyance I have is that the official Rust documentation pretty much goes against the idea you presented, saying
> People are fallible, and mistakes will happen, but by requiring these four unsafe operations to be inside blocks annotated with unsafe you’ll know that any errors related to memory safety must be within an unsafe block. Keep unsafe blocks small; you’ll be thankful later when you investigate memory bugs.
Which is just incorrect - memory safety issues are likely to be due to the "safe" code surrounding your unsafe code - when you're writing C code, the bug isn't that you dereferenced NULL or an OOB pointer, the bug is the code that gave you that pointer in the first place, and in Rust that code is likely to all be "safe". Point being, most of the advice on keeping your unsafe blocks small just leads to people making silly APIs that can be broken via the safe API they wrap (Even in weird ways, like calling a method containing only safe code), and unfortunately there are lots of subtle ways you can unintentionally break you Rust code that most people aren't going to have any idea about.
The Rust Nomicon actually documents these constraints for each 'unsafe' operation. Code that can ensure that these constraints hold can be regarded as 'safe' and rely on an unsafe{ } block. Code that can't, should be marked unsafe and document its own constraints in turn.
> You may think you have a safe interface, only to find out later you're relying on internal details
If you're relying on internal details, you're most likely doing it wrong, even by the standards of 'unsafe'. There are ways to nail down these details where required, at which point they're not even "internal" details anymore, but this has to be opted-into explicitly, for sensible reasons.
> It's only marked `unsafe` because it can break other pieces of "safe" code that it doesn't use, but it itself is 100% safe. You could just as easily argue the "other code" should be the `unsafe` code, since it's what will actually break if you use it incorrectly.
No, "other code" should not be marked unsafe because this would mean that relying on slices is inherently unsafe. Which is silly; the whole point of a "slice" type, contrasted with its "raw parts", is in the guarantees it provides. This is why slice::from_raw_parts is unsafe: not because if what it does, but what it states about the result.
> Which is just incorrect - memory safety issues are likely to be due to the "safe" code surrounding your unsafe code
This is a matter of how you assign "blame" for a memory safety error. It's definitely true however, that the memory unsafe operations play a key role, and I assume that's the point that these docs are making. I agree that people shouldn't be creating faulty abstractions around "unsafe" blocks, but the reason we can even identify this as an issue is because we have "unsafe" blocks in the first place!
It's not that simple, what I'm getting at is that 'unsafe' code is still required to meet all the constraints that 'safe' code has to meet, even though those constraints are not fully documented (Because in general, in 'safe' code there are lots of things that are impossible to do but not explicitly UB), and they have changed them in significant ways in the past. You can see an incomplete list of them here[0]. There has been work to nail these details down for years, with the most recent being this one[1], but in general I would argue there hasn't been much progress since I first looked into it years ago now. Fun fact, the first Rust issue I remember reading that touched on this (and evolved into basically everything going on today) is this[4] one, which was made exactly 4 years ago today (And I mean exact!).
The 'Safety' sections for 'unsafe' operations are actually just extra requirements for those particular API on top that you also have to keep in addition to whatever safe Rust constraints apply. And I would argue in general they're not actually exhaustive, and they're mostly just a "best guess" - it's not a breaking change to introduce or realize there are more constraints. Ex. A little more then a year ago, they added the info that a slice can't be larger than `isize::MAX`[2], and a few months ago was this[3] one, where they added that the pointer length must be from a single allocation.
My point is that when you're doing something like writing an OS kernel, you end up running into these types of issues and corner-cases because Ex. They determine what your allocator is allowed to produce, or if some particular pointer can be validly put into a slice.
> No, "other code" should not be marked unsafe because this would mean that relying on slices is inherently unsafe. Which is silly; the whole point of a "slice" type, contrasted with its "raw parts", is in the guarantees it provides. This is why slice::from_raw_parts is unsafe: not because if what it does, but what it states about the result.
Sure, my point is that this is at best unclear, and I would argue wildly misunderstood. We've basically made two definitions of 'unsafe' - one for code that has to perform one of the five 'unsafe' operations (Which is a clearly defined 'yes' or 'no', and also the one described by the Rust docs), and one for code that can potentially break some type of invariant the will potentially cause UB somewhere else in the program (even if the code in question is completely safe and can't on its own cause any problems).
The problem is that Rust only guarantees the type of safety that it does if you do the second, but yet lots of people (I would almost argue most...) assume the first is sufficient and 'unsafe' by itself ensures all your memory bugs are in `unsafe` code. The Rust docs even describes safe vs. unsafe as two completely different languages (which they are) which brings into question why you would write `unsafe` code when you don't even need any of the `unsafe` operations. You arguably want a separate keyword for this - one that marks a function `unsafe`, but doesn't actually allow you to perform any of the `unsafe` operations in it.
[0] https://doc.rust-lang.org/nomicon/what-unsafe-does.html [1] https://github.com/rust-lang/unsafe-code-guidelines [2] https://github.com/rust-lang/rust/commit/1975b8d21b21d5ede54... [3] https://github.com/rust-lang/rust/commit/1a254e4f434e0cbb9ff... [4] https://github.com/rust-lang/rfcs/issues/1447
These breaking changes are allowed precisely because they're meant to address soundness concerns. Even if at any given time they're just a "best guess" of what's actually needed, that's still wildly better than what e.g. C/C++ do, which is just to not guess at all. Even wrt. formal memory models, perhaps the most complex issue among the ones you mention here, C/C++ only got an attempt at a workable memory model with the 201x standards (C11, etc.). It's not unreasonable to expect that Rust may ultimately do better than that.
> It's not unreasonable to expect that Rust may ultimately do better than that.
But, when? Rust is almost 10 years old, and questions about this stuff has been posed for years now - I was having these same conversations over two years ago. Last year's roadmap included working on the 'unsafe guidelines' I posted, and as an outsider looking in it's unclear to me how much progress has actually being made. Don't get me wrong - they've made a fair amount of progress, but there's still a lot to be done.
My big concern (which I don't consider to be unfounded) is that because there aren't that many big users of Rust actually developing really low-level stuff, the work on defining it isn't getting done. But to me it feels like a self fulfilling prophecy - a project like the Linux Kernel isn't going to end-up using Rust if there's big issues with connecting what they're doing now to Rust idioms (Ignoring the LLVM thing...), even though when Rust was first maturing it was being billed as the replacement for C (and still is).
When that said, my concerns are more with the state of `unsafe` overall, the memory model is only one part of that (though it somewhat spurred conversions about the other issues).
I bet that the pre-C11 memory semantics on Linux kernel aren't the same as e.g. on HP-UX kernel, across all supported hardware platforms.
It is also ironic that Java and .NET did it before C and C++, with their models being adopted as starting point.
> I bet that the pre-C11 memory semantics on Linux kernel aren't the same as e.g. on HP-UX kernel, across all supported hardware platforms.
Yeah, but if they both work, why does it matter? There exists more than one valid possible memory model. The point I was making is that with C, it has been possible to define valid semantics well before C11 because compilers and the language give enough guarantees already.
To that point, while I focused on atomics (Since atomics and threading are largely what was added in C11), the bulk of the actual memory model that made that possible was defined well before then. Strict-aliasing was a thing in C89 (Though I'm not sure compilers enforced it at that point) and is probably the only real notable "gotcha", and `gcc` and `clang` let you turn it off outright (And if you're not doing weird kernel stuff, it generally is easy to obey).
`gcc` and `clang` also have lots of documentation on the extra guarantees they give, such as very well documented inline assembly, type-punning via `union`, lots of attributes for giving the compiler extra information, etc.
Compared to what Rust offers right now, there is no comparison. With Rust, the aliasing model (Which `unsafe` code is not allowed to break) is still unknown, and a lot of the nitty/gritty details like the stuff I listed for `from_raw_parts` are simply leaky implementation details they're inheriting unintentionally from LLVM (And effectively from C - C is where things like "not allowed to go past one past the end of an allocated block" restriction comes from, along with a host of other things).
That said, for userspace, pthreads already gives enough guarantees on ordering via mutex's that there aren't really any problems unless you're trying to do atomics in userspace, which before C11 would have been hard to do portability. And the rest of the things I was talking about like the aliasing model are defined as part of the C standard (C89 and C99), so it's always the same regardless of the compiler (ignoring possible bugs).
It could be argued that's not a huge value gain since you're so far down the stack, but that's the value of the unsafe keyword.
I see two advantages to explicitly marking code as unsafe:
(1) You are very clearly marking the areas of code that are most suspect
(2) You can still have safe code outside of those “unsafe” blocks
You can also go further like I did here:
https://news.ycombinator.com/item?id=21840431
You're still worrying about a smaller portion of your code.
But I've always had a mental note to go back once it got a bit more stable because I really do like the ideas behind it.
It sounds to me like it's definitely getting there and that's awesome.
* zig has native support for arbitrary sized integers. In Rust I used to do bitshifts, Now I just have a packed struct of u3/u5/u7 whatever (see `src/pci/pci.zig`). Of course Rust has a bitflags crate but I didn't find it handy, this is a case where native support vs library support means a world of difference.
* zig has native support for freestanding target. I used to have to build with Xargo for cross compiling to a custom target, I also was forced to `[nostd]`, and some features I was using forced me to use nightly Rust. In zig I have a simple `build.zig` and a simple `linker.ld` script, just works.
* zig has nicer pointer handling. A lot of kernel programming is stuff that Rust considers unsafe anyway. It's not uncommon to have to write lines like `unsafe { (ptr as const u8) }` to deref a pointer in Rust, which is a pain because this kind of thing happens all of the time. Also you have to play around with mut a lot like this: `unsafe { &mut (address as mut _)`. It just felt wrong a lot of the time, where in zig you have either `const` or `var` and that's the end of it.
* zig is really fun to write! this is something that comes up often in the community, after years of C it's just very refreshing.
Some things zig is missing:
* Package manager, this is coming soon. [1]
* Missing documentation for inline assembly (I think this part is going to get overhauled, as Andrew Kelley is writing an assembler in zig atm [2]).
I don't know Nim, but I believe it has a garbage collector so it could be tricky to use for kernel programming.
[0] https://github.com/jzck/kernel-rs
[1] https://github.com/ziglang/zig/issues/943
[2] https://www.youtube.com/watch?v=iWRrkuFCYXQ