News for hackers. Stuff that matters.
Re:Linux Rust Support by geofft on Tuesday July 06, 2021 @10:11AM (#27748819)
Linus has been critical of C, before, too (e.g. https://lkml.org/lkml/2018/6/5/769).

If you look at Linus's criticism of C++ (e.g., https://lkml.org/lkml/2004/1/20/20), he criticizes a few specific things about the language - exception handling, implicit allocations, complex OO. Rust, as it happens, doesn't do any of that. Errors are passed back via return values (which is what the kernel already does), memory allocations are explicit by convention and "zero-cost abstractions" are favored, and the OO model doesn't have multiple inheritance or anything, just interfaces (which is also what the kernel already does, in C, using structs of function pointers).

So I'm not surprised that he doesn't have similar harsh words for Rust. It solves a lot of the issues with C that have been causing practical problems for the kernel (apart from confusing aliasing rules and confusing overflow rules, there was also the problem a while back where a bunch of != NULL checks got optimized out), and it doesn't have the problems C++ has - in fact it tends to solve those use cases in a way similar to the kernel's existing approach.

For my part, I've been involved a very tiny bit in Linux kernel development for well over a decade because I find systems programming cool, and I've also been involved a very tiny bit in Rust starting around 1.0 because I find systems programming cool. I started this project because I genuinely thought Rust would be a good choice, not because I was deeply involved with the Rust community and I wanted to evangelize it.

(I looked up the IRC logs where Alex and I decided to start working on this, and the context was that Alex had run into some bug with buffer length handling in the kernel, he was considering sending in some patches to improve the C abstractions about this, and since PyCon was the following week I semi-jokingly asked if we should instead spend our sprints time writing support to ship kernel modules in Rust. Which we did.)

Re:Linux Rust Support by tialaramex on Tuesday July 06, 2021 @11:19AM (#27749719)
The allocations thing in particular. You can watch serious C++ practitioners staring at a class definition and usage and trying to figure out answers to a quiz question, "How many allocations does this do?". Is that line a Move or a Copy? That's a bad sign for a language you'd supposedly want to write OS kernels in.

Rust got luckier here, by the time Rust was invented programmers knew they really care about this, and so Rust explicitly distinguishes Copy (this object is just bits, so you can duplicate it by copying the bits), from Clone (this object can be duplicated, but you need to call its clone() method to explicitly do that work) and gives Move semantics to all assignments of types which do not have Copy.

So in Rust "let a = b" definitely isn't doing memory allocation, it just moves bits. If you misunderstand the situation and assumed b is still valid because it was Copy, but actually it isn't, the compiler has your back and will politely refuse to let you use variable b after it was Moved. If you did still need b you can write "let a = b.clone()" and now they both still exist but anybody can see this code might be doing something expensive 'cos it calls this clone method. And if the compiler says "Ahem, you can't Clone that" then you've just learned something very important about the thing that a minute ago you assumed was just a few trivially duplicated bits.

Re:Linux Rust Support by blub on Tuesday July 06, 2021 @05:35PM (#27754624)
Given that this is the kernel, using typical C++ libraries would be out of the question, so it should be easy enough to design things right from the start. C++ is after all used in embedded contexts where allocations are forbidden and it offers excellent support for this use case.

In such a project where allocations matter one would not stare at class definitions, one would ensure that there are no allocations by forbidding use of libraries like the STL and for example disabling operator new and the copy constructor. If the project is safety-critical there are tools which can enforce such rules.

Edit: I've clarified that I was referring to C++ libraries only.

Re:Linux Rust Support by tialaramex on Tuesday July 06, 2021 @08:46PM (#27756135)
Ah, I think there is a misconception at work here.

The problem isn't "Kernel code should not allocate" but "Kernel code should not implicitly allocate".

And C++ thinks this is none of your business. You can have no allocation by not providing an allocator - but you can't have no implicit allocation because the language will go out of its way to hide some of the obvious places allocation might happen rather than expose its warts.

Re:Linux Rust Support by blub on Wednesday July 07, 2021 @04:09AM (#27758638)
Could you give me an example of such implicit allocations that are done by the language and would be a problem in a kernel context?

Here's my list:

* RTTI: would be disabled

* Exceptions: would be disabled

* STL, stdlib: would not be used

Re:Linux Rust Support by tialaramex on Wednesday July 07, 2021 @06:34AM (#27759476)
For example the C++ language implicitly runs constructors, which may (or may not, it depends) cause allocation, whenever it sees fit to construct any object, even if that object is temporary and will be destroyed immediately afterwards.

Now of course you could say, "Oh I never use objects, those are awful" but then you ought to ask yourself what exactly the point was of having C++ at all. Or you could say, "We'll ban constructors from allocating" but C++ won't help you enforce that rule and the resulting code won't be idiomatic C++. Implied allocation is just normal in C++.

Also note that although Rust's full standard library won't be available in the kernel, you do get Vectors, you get Rust's reference counted "smart pointer" types, Iterators, Option and Result and so on. In Rust such things don't have implied allocation, only explicit allocation which is fine.

Rust used today in the real world does have some places with implied allocation, a notable example is String concatenation. Rust won't let you write

  let hw = "Hello, " + "world";
However, the String implementation in the Rust standard library, and currently in kernel Rust, will let you write:

  let h = "Hello".to_owned(); // Now it's a String
  let hw = h + ", world";
But, that's one of the anticipated objections from Linus, and you'd just remove AddAssign (and possibly Add) from the Strings to cure it by forcing programmers to actually spell out what they want to happen here instead of acting as though concatenating strings is free.
Re:Linux Rust Support by blub on Tuesday July 13, 2021 @06:42AM (#27819460)
If C++ constructors don't call allocation functions (1), don't initialize members which call allocation functions (2) and don't use features like RTTI which I've mentioned in a previous post, then they do not allocate. It's not trivial to ensure the above and while it's not usual for regular C++ projects, it's certainly manageable because C++ is used in some kernels and has a track record of being used in projects with constraints much tighter that the Linux kernel itself.

(2) seems the most tricky, but in reality, in a context like the Linux kernel where the libraries would be written from scratch or wrap existing C code, this can be controlled well. If one is including a new language in the kernel, it should be comparably low effort for example to write a custom vector container which exactly fulfills their requirements.

In the end I realize that C (and I assume Rust) makes it easier to reason about allocations in simple pieces of code which don't call functions, but this is not a deal breaker for C++ either.

So I don't think that allocations is the answer to the question of "Why Rust over C++?", as you claimed when you said "The allocations thing in particular". It's perhaps part of the answer, together with other technical reasons and personal likes or dislikes. And I believe that the latter are an important part of that answer that few if any are bringing up in this thread.

Re:Linux Rust Support by geofft on Wednesday July 07, 2021 @05:43AM (#27759220)
You'd need some sort of standard library, even if not the normal userspace one. The kernel has a pretty sizable "lib" directory which includes all the string functions etc. you'd expect in normal userspace C. For the Rust-for-Linux work we're using the Rust standard library's libcore and liballoc, just a slightly forked version of liballoc with the goal of getting any changes upstream once we get some experience about what works well. (Along those lines, the kernel has a non-allocating printk() that works like userspace printf(), and we provide non-allocating Rust wrappers that mimic userspace println!(). C++ in the kernel would want a non-allocating iostream, I think.)

I think you could address this point by using a slightly forked version of a C++ standard library that marked all its allocating constructors and allocating conversion functions with the "explicit" keyword. (But you wouldn't be able to upstream this, ever.)

I'm not sure if there's any other case where you'd have implicit allocations that you wouldn't be able to avoid. You may be right that this is doable entirely with library changes provided you avoid RTTI and exceptions.

One specific weird case is the copy done when capturing things into a lambda: https://godbolt.org/z/Y7fG5hjqq I expect the first allocation here because I'm explicitly calling the constructor, and that's fine, but the second one surprises me a bit, because the copy constructor is marked "explicit". Maybe people who work with C++ more regularly than me find this less surprising.

But that goes back to the problem 'tialaramex raised, about how "How many allocations does this do?" is a quiz question. Distinguishing zero from at-least-one is important, and you can do that by avoiding any heap-allocated-container types entirely, but so is distinguishing one from more-than-one.

Re:Linux Rust Support by kibwen on Tuesday July 06, 2021 @06:31PM (#27755218)
Using Rust's stdlib isn't out of the question for the kernel, at least not the most fundamental bits of the stdlib. Rust offers a well-defined subset of its stdlib, called libcore, that is suitable for embedded work: no allocations, no OS-provided abstractions, etc. So you don't get HashMap or Vec, but you do get Option, Iterator, str (but not String, the heap equivalent), etc.
Re:Linux Rust Support by blub on Tuesday July 06, 2021 @05:39PM (#27754676)
Exception handling and implicit allocations are very easy to control in C++, especially in a project where one would not use the STL or any other typical user space libraries. The language has been used in safety-critical projects and low-level embedded work for decades, so it must support such use cases well.
Re:Linux Rust Support by jcelerier on Tuesday July 06, 2021 @10:36AM (#27749198)
> Rust, as it happens, doesn't do any of that.

Rust does not allocate implicitly when you use Vec or String (or any other container) ? come on..

Re:Linux Rust Support by geofft on Tuesday July 06, 2021 @10:44AM (#27749301)
No, it allocates explicitly when you do that. Vec and String are heap-allocated types, so whenever you use them, you know you're doing an allocation.

In C++ you can write code like this:

    #include <string>
    #include <iostream>

    void print(const std::string &s) {
        std::cout << s << std::endl;
    }

    int main() {
        print("Hello world!");
    }
This compiles with no warnings, and at no point in the function "main" do you see that you're causing a memory allocation and a copy by implicitly converting your static message to a std::string, passing it to the function, and deallocating it. If you code-review "main", it doesn't look like you're doing an allocation because you're passing a const char *; if you code-review "print", it doesn't look like you're doing an allocation because you're accepting a std::string, which has already been allocated. So you get an extra, unneeded allocation in the chasm between the two, and it's very hard to track down the performance problem (or, in the worst case, a hang - memory allocation can block on flushing buffers or swapping things out, so you'd better not run into this situation in code that could itself be involved in writing to disk!).

Compare the equivalent Rust code:

    fn print(s: &String) {
        println!("{}", s);
    }

    fn main() {
        print("Hello world!");
    }
which produces a "mismatched types" error: "expected reference `&String`, found reference `&'static str`."

To get this to compile, you have to write something like print(&"Hello world!".to_owned()); , where you specifically instruct an allocation to happen.

The objection is to implicit allocations. Explicit allocations are fine.

Re:Linux Rust Support by jcelerier on Thursday July 08, 2021 @07:04AM (#27770425)
> This compiles with no warnings, and at no point in the function "main" do you see that you're causing a memory allocation and a copy by implicitly converting your static message to a std::string, passing it to the function, and deallocating it

Such a warning would be a bug in this specific case as "Hello World!" is under the SSO size though - there is no call to malloc/new in this specific std::string creation.

I fail to see how that is relevant though: the kernel does not use the C standard library - it's not going to use the standard C++ library either except maybe the freestanding, compile-time-only bits such as type_traits, concepts, ...

In which case it is trivial to have a string type akin to rust's:

    class kstring {
     public:
      explicit kstring(const char* str, std::size_t len);
    
     private:
      const char* m_storage{};
      std::size_t m_len{};
    };
    
    auto operator""_to_owned(const char* str, std::size_t len) {
      return kstring{str, len};
    }
    
    void print(const kstring& str) 
    {
      // ...
    }
    
    int main() {
      // fails:
      // print("foo");
    
      // ok
      print("foo"_to_owned);
    }

Here, the correct answer would be std::string_view anyways (or a custom c_string_view).

> If you code-review "main", it doesn't look like you're doing an allocation because you're passing a const char *; if you code-review "print", it doesn't look like you're doing an allocation because you're accepting a std::string, which has already been allocated.

My experience really disagrees with this part, every time I've been involved in code reviews, it was common knowledge that a function taking a const& or && can cause an allocation at the call site. It's barely a beginner mistake to not take it into account.

Re:Linux Rust Support by tialaramex on Tuesday July 06, 2021 @11:46AM (#27750081)
Amusingly if you create a Vec or String that operation is guaranteed not to allocate :D

The new() functions for these types are const (ie they can be evaluated at compile time) and produce an "empty" placeholder.

Because Rust's Strings don't have NUL termination, the empty string doesn't need any backing storage, likewise for an empty vector.

When (and more importantly in many programs if) you actually add something to the string or vector that may prompt an allocation, but you called an explicit method to do that, so it oughtn't to come as any surprise.

Re:Linux Rust Support by Ericson2314 on Tuesday July 06, 2021 @10:39AM (#27749239)
https://github.com/rust-lang/rust/pull/84266 We have been excising this with a new opt-out mechanism so there are no allocations behind your back but the same abstractions are notheless used as the rest of the Rust ecosystem.