5 ms·
I don't know Linus's reasons specifically, but our presentation at Linux Security Summit last year laid out why we think that Linus's past objections to C++ don
by geofft 6y ago
I don't know Linus's reasons specifically, but our presentation at Linux Security Summit last year laid out why we think that Linus's past objections to C++ don't apply to Rust. See slides 19-21 of https://ldpreload.com/p/kernel-modules-in-rust-lssna2019.pdf https://ldpreload.com/p/kernel-modules-in-rust-lssna2019.pdf .
His previous objections were:
In fact, in Linux we did try C++ once already, back in 1992.
It sucks. Trust me - writing kernel code in C++ is a BLOODY STUPID IDEA.
The fact is, C++ compilers are not trustworthy. They were even worse in
1992, but some fundamental facts haven't changed:
- the whole C++ exception handling thing is fundamentally broken. It's
_especially_ broken for kernels.
- any compiler or language that likes to hide things like memory
allocations behind your back just isn't a good choice for a kernel.
- you can write object-oriented code (useful for filesystems etc) in C,
_without_ the crap that is C++.
In brief, Rust does not rely on C++-style exception handling/unwinding, it does not do memory allocations behind your back, and its OO model is closer to the existing kernel OO implementation in C than it is to C++'s model. (There are other good safe languages besides Rust that I personally like in general but do not satisfy these constraints for this particular use case.)
- vojtechkral 6y ago> In brief, Rust does not rely on C++-style exception handling/unwinding, it does not do memory allocations behind your back It kind of does a little bit. Panicking is implemented via the same machinery. But of course code is not expected to panic unless things are terribly wrong (ie. kind of like the existing kernel panic thing). It also does sometimes allocate things, but it is true that Rust is a lot more explicit about this - ie. you have to call clone() or Box::new() etc.
- ChrisSD 6y ago`no_std` environments are different. They don't allow allocations unless you explicitly add an allocator. You also have to define the panic handler yourself. Generally `no_std` libraries will not allocate or panic themselves. There is still the possibility of panicking (e.g. out of bounds array access) but there are alternatives that don't panic if that's a concern even with a custom handler.
- remexre 6y agoYeah, and it shouldn't be too hard to hook up existing nightly Rust to use the Kernel's panic functionality; it already supports user-overridable panics for no_std, iirc
- jeffbee 6y agoThese seem like the statements of a person who knows next to nothing about C++.
- andai 6y agoHow do you write OO code in C?
- amw-zero 6y agoThis is a really in depth read about OO design patterns in the Linux Kernel using C: https://lwn.net/Articles/444910/ https://lwn.net/Articles/444910/ The easiest to understand example (for me) is that you can manually implement polymorphism support using function pointers that take in a struct instance as an argument. Polymorphism is fundamentally just a method call that's dependent on which target object it's being called on, so this pattern allows you to pass in the target object (as a struct since it's C) explicitly. From the article: > Some simple examples of this in the Linux kernel are the file_lock_operations structure which contains two function pointers each of which take a pointer to a struct file_lock, and the seq_operations vtable which contains four function pointers which each operate on a struct seq_file.
- jeffbee 6y agoThe huge and important difference is when you use this pattern in C the compiler has no idea what you are doing and can't check anything, nor can it reduce the cost of this dispatch mechanism. A compiler for a language where this is a feature can check what you are doing and de-virtualize virtual dispatch. In C you pay the highest possible cost for polymorphism while enjoying the least benefits.
- Jasper_ 6y ago> The huge and important difference is when you use this pattern in C the compiler has no idea what you are doing and can't check anything Function pointers exist in C and are type-checked by the compiler just fine. > A compiler for a language where this is a feature can check what you are doing and de-virtualize virtual dispatch. Virtual dispatch is pretty much only used in the kernel in places where virtualization is necessary. Devirtualization is pretty rare in C++ in practice (I don't believe I've ever seen a "virtual final" method, and my day job is on a large C/C++ codebase).
- creato 6y agoHow does C++ do memory allocations behind your back?
- TheCoelacanth 6y agoConstructors and copy constructors, mainly. Something as innocuous as `T x;` or `T x = y;` will allocate in C++. In Rust, the equivalents are `let x = T::new();` and `let x = y.clone();` which are much more obvious as potential allocations.
- creato 6y ago> Something as innocuous as `T x;` or `T x = y;` will allocate in C++ I think saying these will allocate is wrong. They can allocate, but only if T allocates and manages memory, which depends on what T does. I am skeptical of the more general claim. Anything but a truly cursory skim of the code is going to tell you that these lines are constructing a new object, regardless of the syntax, and to actually know whether it allocates memory, you need to know more about T, which is true in either Rust or C++.
- boardwaalk 6y agoUnless someone implements Copy on a type that allocates in Rust (which you shouldn't do; Copy is specifically for types that are cheap to copy), you really won't get implicit copies, though. That means in any reasonable code, `let x = y` won't allocate in Rust while `T x = y` could in C++. `f(x)` in Rust won't either for the same reason. And that's on top of how C++ will implicitly take a reference, so even if you know x is a heavy object, you don't know if `f(x)` will be expensive (or if `f(move(x))` would be helpful). It's just not exactly equivalent. C++ is more implicit and has less baked-in good practices like `Copy` vs `Clone`. The indirection is often shallower (less often I have to inspect a function prototype or type I'm using for specifics), and I find that very useful.
- dthul 6y agoMinor clarification: afaik it's impossible to implement Copy such that it would allocate. Unlike Clone, the Copy trait is only a marker trait which doesn't have methods, so you cannot customize its behavior. It will always be a bitwise stack copy.
- wahern 6y agoIs the following code from page/slide 64 supposed to be a talking point? FFI: calling C from Rust extern { fn readlink(path: *const u8, buf: *const u8, bufsize: usize) -> i64; } fn rs_readlink(path: &str) -> Result<String, ...> { let mut r = vec![0u8; 100]; if unsafe { readlink(path.as_ptr(), r.as_mut_ptr(), 100) } ... The example doesn't bother using r.capacity(), which is exactly the kind of poor code hygiene leading to overflows you would see in bad C code--i.e. rather than using the sizeof operator, manually passing a literal integer, simple macro, or other ancillary variable that hopefully was actually used in the object's declaration. Also, notably, the presentation says one of the desirable characteristics for a kernel language is "Don't 'hide things like memory allocations behind your back'". I'm not very well versed in Rust, but when I write a small test program using vec![0u8; 100], the buffer is heap allocated. Which would be a hidden allocation in my book. I realize these are just introductions, but if you're pitching Rust code for the kernel, it seems disingenuous not to show the style of Rust code, presumably much more verbose, which would actually be used in the kernel. Constructs like boxes can't really be used, at least not easily. C++ was disliked because the prospect of fiddling and debugging hidden allocators is nightmarish. And to the extent Rust's type system relies on boxes and similar implicit allocation for ergonomics, some of the type safety dividends might be lost. How much would be lost is hard to judge without showing the kind of actual code that would be required when used in Linux. Toy Rust OS experiments don't count, because they can just panic on OOM and other hard conditions, and don't need to wrestle with difficult constructs like RCU.
- comex 6y ago`Vec` is always heap-allocated; it's not hidden if you know that. It's intentional that you have to write `vec![0u8; 100]` to get this, rather than just, say, `[0u8; 100]` (which will give you a stack allocation). It's true that the example should use capacity() rather than hardcoding 100. If you want a stack allocation, here's an example: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=df5c3f22652626ba50b8dcc0463df38c https://play.rust-lang.org/?version=stable&mode=debug&editio... It's about the same length as the original, but it's hard to make a direct comparison. On one hand, the original snippet is totally wrong: it doesn't properly handle the nul terminator of either the input or the output. I fixed that, but at the cost of making the code a bit more verbose. On the other hand, the original snippet does try to parse the result as UTF-8, which is not what you want, so I removed it (it would add 2 lines or so). That said… it's not true that avoiding heap allocation is what would actually be done in the kernel. Not for file paths, which is what readlink typically returns. Since kernel stack sizes are so small, paths have to be allocated on the heap. It's only in userland that you can plop PATH_MAX sized buffers on the stack. On the other hand, if you were really doing heap allocation in the kernel you would need to handle out-of-memory conditions. Rust's standard library doesn't support recovering from OOM at all, but it's not like you'd be using the standard library in the kernel anyway. In contrast, Rust the language is actually quite good at this. A fallible allocation function would return something like Result<String, OOMError>; the use of Result forces you to handle the error case somehow (unlike C, you can't just assume it succeeded), but all you need to do to handle it is use the `?` operator to propagate errors to the caller.