Asterinas: OS kernel written in Rust and providing Linux-compatible ABI
github.com364 points by Klasiaster 9 months ago
364 points by Klasiaster 9 months ago
I personally dislike rust, but I love kernels, and so I'll always check these projects out.
This is one of the nicer ones.
It looks pretty conservative in it's use of Rust's advanced features. The code looks pretty easy to read and follow. There's actually a decent amount of comments (for rust code).
Not bad!
Otherwise is a decent language but what makes it difficult is the borrow semantics and lifetimes. Lifetimes are more complicated to get your head around.
But then there's this Arc, Ref, Pinning and what not - how deep is that rabbit hole?
Context: I'm writing a novel kernel in Rust.
Lifetimes aren't bad, the learning curve is admittedly a bit high. Post-v1 rust significantly reduced the number of places you need them and a recent update allows you to elide them even more if memory serves.
Arc isn't any different than other languages, not sure what you're referring to by ref but a reference is just a pointer with added semantic guarantees, and Pin isn't necessary unless you're doing async (not a single Pin shows up in the kernel thus far and I can't imagine why I'd have one going forward).
Would you not want to use Pin when sharing memory with a driver or extension written in a different language (eg: C)?
Pin is a pure compile time abstraction for a single problem: memory safety of self referential struct.
Pin leverages the type system to expose to the programmer receiving a pointer to a Pin'ned object, that this object has some pointer on itself (a self referencial struct). You better be mindful not to move this object to a different memory location unless you know for sure that it is safe to do so. The Pin abstraction makes it harder to forget; and easier to notice during code review; by forcing you to use the keyword unsafe for any operations on the pinned object that could move it around.
In C, there is no such way to warn the programmer besides documentation. It is up to the programmer to be very careful.
Nah not really. Pin is for self-refefential data typically. It's compile time only so that information would get lost in C anyway, and there's no way to distinguish that data at runtime.
The kernel is doing so much anyway with memory maps and flipping in / out pages for scheduling and context switching that Pin doesn't add any value in such cases anyway.
It was also specifically built for async rust. I've never personally seen it in the wild in any other context.
If you’re writing C and don’t track ownership of values, you’re in a world of hurt. Rust makes you do from day one what you could do in C but unless you have years of experience you think it isn’t necessary.
Okay, I think it is is more like Typescript. You hate it but one day you just write small JS program and convert it to Typescript to discover that static analysis alone had so many code paths revealed that would have resulted in uncaught errors and then you always feel very uncomfortable writing plain Javascript.
But what about tools like valgrind in context of C?
Valgrind can only tell you about issues that your testcases exercise. It doesn't provide the same guarantees as static checking of memory safety invariants. But, if you're really concerned (especially about unsafe code), belt-and-bracers is a good strategy, and valgrind will work with rust binaries as well. Rust also has a tool called MIRI which can similarly flag up issues in testcases (it's effectively an interpreter for the intermediate representation in the compiler, and it can detect undefined behaviour even if the compiled assembly would happen to look OK. Still has the same limitation of needing extensive testcases though)
A few years ago I was worrying on a Byzantine mess of a JS project. I converted everything to TS for the sole reason of somewhat safely refactoring the project as a whole.
There was so little trust in the fragility of the original, it took a few months to convince everyone the refactored TS branch was safe.
After that, feature development was a lot faster in terms of productivity again.
You probably should run your rust programs through valgrind regardless. Rust is safer than C, but any unsafe code drops you to approximately C level of safety and any C FFI calls are obviously outside of rust's control or responsibility.
Valgrind is great, especially if you write extensive tests and you actually run them through it regularly. And even then, it does not prove the absence of any kind of bugs. Safe rust has strong guarantees.
frama-c
Which is terrible for kernel development, and is generally very hard to work with, unfortunately.
It was true until LLMs arrive. Feature compilers + IDEs can be integrated with LLMs to help programmers.
Rust was a great idea, before LLMs, but I don't see the motivation for Rust when LLMs can be the solution initial for C/C++ 'problems'.
Relying on LLMs to code for you in no way solves the safety problem of C/C++ and probably worsens it.
probably?
even if the LLM is trained on flawless C code (which it isn't) it still has no way of reasoning about a complex system, it's just "what token is statistically most likely to come next"
I said that because it's very possible for someone to write a more flawed program without an LLMs help. The exact probabilities weren't central to my point.
Rust compiler checks things for you. People trust the Rust compiler because it enforces rules they want, so people don’t have to be in its place. Your suggestion is to be that checker to LLM-generated code. Back to square one.
On the contrary LLMs make using safe but constraining languages easier - you can just ask it how to do what you want in Rust, perhaps even by asking it to translate C-ish pseudocode.
I don’t entirely agree, you can get used to the borrow checker relatively quickly and you mostly stop thinking about it.
What tends to make Rust complex is advanced use of traits, generics, iterators, closures, wrapper types, async, error types… You start getting these massive semi-autogenerated nested types, the syntax sugar starts generating complex logic for you in the background that you cannot see but have to keep in mind.
It’s tempting to use the advanced type system to encode and enforce complex API semantics, using Rust almost like a formal verifier / theorem prover. But things can easily become overwhelming down that rabbit hole.
It's just overengineered. Many Rust folks don't realize it because they come from C++ and suffer from Stockholm Syndrome.
How is it overengineered?
That's my personal opinion after I've learned it and read Klabnik's book. I'm aware that other people's mileage differs. I'm listing a few reasons below.
- Overall too complex
- Wrong philosophy: demanding the user to solve problems instead of solving problems for the user
- Trying to provide infinite backwards compatibility with crates, which leads to hidden bitrot
- Slow compilation times
- Claims to be "safe" but allows arbitrary unsafe code, and it's everywhere.
- Adding features to fix misfeatures (e.g. all that lifetime cruft; arc pointers) instead of fixing the underlying problem
- Hiding implementations with leaky abstractions (traits)
- Going at great length to avoid existing solutions so users re-invent it (e.g. OOP with inheritance; GC), or worse, invent more complex paradigms to work around the lack (e.g. some Rust GUI efforts; all those smart pointer types to work around the lack of GC)
- A horrendous convoluted syntax that encourages bad programming style: lot's of unwrap, and_then, etc. that makes programs hard to read and audit.
- Rust's safe code is not safe: "Rust’s safety guarantees do not include a guarantee that destructors will always run. [...] Thus, allowing mem::forget from safe code does not fundamentally change Rust’s safety guarantees."
It already has similar complexity and cognitive demands as C++ and it's going to get worse. IMHO, that's also why it's popular. Programmers love shitty languages that allow them to show off. Boring is good.
> Claims to be "safe" but allows arbitrary unsafe code, and it's everywhere.
Sigh. This is not true. Not the first part, and especially not the last part. `Unsafe` doesn't allow arbitrary, unsafe code. It resets the compiler to a level where most manually managed languages are all the time. You still have to uphold all guarantees the compiler provides, just manually. That's why Miri exists.
Either it's safe or it's unsafe. If you use the keyword "unsafe" it should definitely not mean "safe" (and it doesn't, but you seem to suggest it).
You're being obtuse. Terms have contexts. It is unsafe in the sense that C++ is unsafe, in that you may cause undefined behavior which can't be entirely checked by the compiler. You're back to what Valgrind/C++ -wall/UBSan provide.
"Unchecked" or "Unconfirmed" would've perhaps been better choices, but Rust considers all other manual memory and reference management unsafe, so the word stuck.