Re: [PATCH 00/13] [RFC] Rust support

From: Linus Walleij
Date: Tue Apr 27 2021 - 06:54:17 EST


Hi Wedson,

thanks for your replies, I have a bigger confidence in Rust for
drivers after your detailed answers.

On Mon, Apr 26, 2021 at 4:40 PM Wedson Almeida Filho
<wedsonaf@xxxxxxxxxx> wrote:

> Note here that we encode the size of the block at compile time. We'll get our
> safety guarantees from it.
>
> For this abstraction, we provide the following implementation of the write
> function:
>
> impl<const SIZE: usize> IoMemBlock<SIZE> {
> pub fn write<T>(&self, value: T, offset: usize) {
> if let Some(end) = offset.checked_add(size_of::<T>()) {
> if end <= SIZE {
> // SAFETY: We just checked above that offset was within bounds.
> let ptr = unsafe { self.ptr.add(offset) } as *mut T;
> // SAFETY: We just checked that the offset+size was within bounds.
> unsafe { ptr.write_volatile(value) };
> return;
> }
> }
> // SAFETY: Unimplemented function to cause compilation error.
> unsafe { bad_write() };
> }
> }

I really like the look of this. I don't fully understand it, but what
is needed for driver developers to adopt rust is something like a
detailed walk-through of examples like this that explains the
syntax 100% all the way down.

We do not need to understand the basic concepts of the
language as much because these are evident, the devil is
in details like this.

> Now suppose we have some struct like:
>
> pub struct MyDevice {
> base: IoMemBlock<100>,
> reg1: u32,
> reg2: u64,
> }
>
> Then a function similar to your example would be this:
>
> pub fn do_something(pl061: &MyDevice) {
> pl061.base.write(pl061.reg1, GPIOIS);
> pl061.base.write(pl061.reg2, GPIOIBE);
> pl061.base.write(20u8, 99);
> }
>
> I have this example here: https://rust.godbolt.org/z/chE3vjacE
>
> The x86 compiled output of the code above is as follows:
>
> mov eax, dword ptr [rdi + 16]
> mov rcx, qword ptr [rdi]
> mov dword ptr [rcx + 16], eax
> mov rax, qword ptr [rdi + 8]
> mov qword ptr [rcx + 32], rax
> mov byte ptr [rcx + 99], 20
> ret

This looks good, but cannot be done like this. The assembly versions
of writel() etc have to be used because the compiler simply will not
emit the right type of assembly for IO access, unless the compiler
(LLVM GCC) gains knowledge of what an IO address is, and so far
they have not.

I mostly work on ARM so I have little understanding of x86
assembly other than superficial.

Port-mapped IO on ARM for ISA/PCI would be a stressful
example, I do not think Rust or any other sane language
(except Turbo Pascal) has taken the effort to create language
abstractions explicitly for port-mapped IO.

See this for ARM:

#define outb(v,p) ({ __iowmb(); __raw_writeb(v,__io(p)); })

So to write a byte to a port we first need to issue a IO write memory
barrier, followed by the actual write to the IO memory where the
port resides. __iowmb() turns into the assembly instruction
wmb on CPUs that support it and a noop on those that do not,
at compile time.

One *could* think about putting awareness about crazy stuff like
that into the language but ... I think you may want to avoid it
and just wrap the assembly. So a bit of low-level control of the
behavior there.

> 2. The only unsafe part that could involve the driver for this would be the
> creation of IoMemBlock: my expectation is that this would be implemented by the
> bus driver or some library that maps the appropriate region and caps the size.
> That is, we can also build a safe abstraction for this.

I suppose this is part of the problem in a way: a language tends to be
imperialistic: the developers will start thinking "it would all be so much
easier if I just rewrote also this thing in Rust".

And that is where you will need compiler support for all targets.

> 7. We could potentially design a way to limit which offsets are available for a
> given IoMemBlock, I just haven't thought through it yet, but it would also
> reduce the number of mistakes a developer could make.

The kernel has an abstraction for memory and register accesses,
which is the regmap, for example MMIO regmap for simple
memory-mapped IO:
drivers/base/regmap/regmap-mmio.c

In a way this is memory safety implemented in C.

Sadly it is not very well documented. But regmap is parameterized
to restrict accesses to certain register areas, using explicit
code in C, so you can provide an algorithm for which addresses
are accessible for write for example, like every fourth address
on a sunday.

A typical usecase is clock drivers which have very fractured
and complex memory maps with random readable/writeable
bits all over the place.

If Rust wants to do this I would strongly recommend it to
try to look like regmap MMIO.
See for example drivers/clk/sprd/common.c:

static const struct regmap_config sprdclk_regmap_config = {
.reg_bits = 32,
.reg_stride = 4,
.val_bits = 32,
.max_register = 0xffff,
.fast_io = true,
};
(...)
regmap = devm_regmap_init_mmio(&pdev->dev, base,
&sprdclk_regmap_config);

It is also possible to provide a callback function to determine
if addresses are readable/writeable.

This is really a devil-in-the-details place where Rust needs
to watch out to not reimplement regmap in a substandard
way from what is already available.

Also in many cases developers do not use regmap MMIO
because it is just too much trouble. They tend to use it
not because "safety is nice" but because a certain register
region is very fractured and it is easy to do mistakes and
write into a read-only register by mistake. So they want
this, optionally, when the situation demands it.

> > What I need to know as device driver infrastructure maintainer is:
> >
> > 1. If the language is expressive enough to do what device driver
> > authors need to do in an efficient and readable manner which
> > is as good or better than what we have today.
>
> What do you think of the example I provided above? I think that generics give
> Rust an edge over C in terms of expressiveness, though abusing it may
> significantly reduce readability.

It looks nice but it is sadly unrealistic because we need to wrap
the real assembly accessors in practice (write memory barriers
and such) and another problem is that it shows that Rust has an
ambition to do a parallel implementation of regmap.

> > 2. Worry about double implementations of core library functions.
>
> This indeed may be a problem, but I'm happy to have Rust wrappers call
> C/assembly functions. With LTO this should not affect performance.

Yeah see above about regmap too.

> > The syntax and semantic meaning of things with lots of
> > impl <T: ?Sized> Wrapper<T> for ... is just really intimidating
> > but I suppose one can learn it. No big deal.
>
> I agree it's intimidating, but so are macros like ____MAKE_OP in bitfield.h --
> the former has the advantage of being type-checked. Writing macros like
> ____MAKE_OP is a hit-and-miss exercise in my experience. However, I feel that
> both cases benefit from being specialised implementations that are somewhat
> rare.
(...)
> > I have no idea how to perform this in
> > Rust despite reading quite a lot of examples. We have
> > created a lot of these helpers like FIELD_GET() and
> > that make this kind of operations simple.
>
> Would you mind sharing more about which aspect of this you feel is challenging?

Good point.

This explanation is going to take some space.

I am not able to express it in Rust at all and that is what
is challenging about it, the examples provided for Rust
are all about nice behaved computer programs like
cutesey fibonnacci series and such things and not really
complex stuff.

Your binder example is however very good, the problem
is that it is not a textbook example so the intricacies of
it are not explained, top down. (I'm not blaming you for
this, I just say we need that kind of text to get to know
Rust in the details.)

As device driver maintainers we especially need to
understand IO access and so I guess that is what
we are discussing above, so we are making progress
here.

What we need is a good resource to learn it, that
skips the trivial aspects of the language and goes immediately
for the interesting details.

It's not like I didn't try.
I consulted the Rust book on the website of coure.

The hard thing to understand in Rust is traits. I don't understand
traits. I have the level of "a little knowledge is dangerous" and
I clearly understand this: all kernel developers must have
a thorough and intuitive understanding of the inner transcendental
meaning of the concept of a TRAIT, how it was devised, how the
authors of the language conceptualized it, what effect it is supposed
to have on generated assembly.

The language book per se is a bit too terse.
For example if I read
https://doc.rust-lang.org/book/appendix-02-operators.html

T: ?Sized : Allow generic type parameter to be a dynamically sized type

This is just self-referential. The description is written in a
strongly context-dependent language to make a pun ...
I think every word in that sentence except "allow"and "to be a"
is dependent on other Rust concepts and thus completely
unreadable without context.

Instead it is described in a later chapter:
https://doc.rust-lang.org/book/ch19-04-advanced-types.html

This is more to the point.

"Rust has a particular trait called the Sized trait to
determine whether or not a type’s size is known at compile time."
(...) "A trait bound on ?Sized is the opposite of a trait bound on
Sized: we would read this as “T may or may not be Sized.” This
syntax is only available for Sized, not any other traits."

But Jesus Christ. This makes me understand less not
more.

So I need to understand what traits are. So back to
https://doc.rust-lang.org/book/ch10-02-traits.html

This chapter is just *really* hard to understand. I
can blame myself for being stupid, but since it is
more convenient to blame the author I'm just going
to complain that this chapter is not very good for
low-level programmers. I'm probably wrong, this is
obviously a personal development exercise.

OK I will give it several second tries. It just feels
very intimidating.

To me, the Rust book is nowhere near "The C
Programming Language" in quality (meaning readability
and ability to transfer complex detailed knowledge) and
that is a serious problem.

Sadly, it is hard to pin down and define what makes it
so hard, but I would take a guess and say that
"The C Programming Language" was written by low
level programmers implementing an operating system
and the Rust book was not. I.e. the authors concept
of the intended audience.

So this is where we need good inroads to understand the
language.

The quality and versatility of the K&R book about The
C Programming Language has been pointed out by
Kernighan in "UNIX: A History and a Memoir"
and I think the Rust community needs to learn something
from this (page78, praising himself and Ritchie):

"We made many alternating passes over the main text (...)
It describes the language with what Bill Plauger
once called 'spine-tingling precision'. The reference
manual is like C itself: precise, elegant, and compact"

I think a main obstacle for getting Rust accepted by kernel
developers is not the language itself, but the lack of textbook
with the same qualities as The C Programming Language.

This is a serious flaw, not with the language itself but with
the surrounding materials.

Kernighan writes about *forcing* Ritchie to write the book
about C ("I twisted his arm harder and eventually he agreed"),
after implementing it, and this made it reflect the
language from the intent of the author and OS usecase
very well.

The Rust book is written "by Steve Klabnik and Carol Nichols,
with contributions from the Rust Community" and I do not mean
to criticize them, because I think they had very clear ideas
of what kind of people were going to read it. And I bet they did
not intend it for OS developers.

What I find very disturbing is that the authors of the Rust
language did NOT write it. I think this may be the source
of a serious flaw. We need this information straight from
the horse's mouth.

I would strongly advice the Rust community to twist the
arms of the original Rust authors to go and review and
edit the Rust book. Possibly rewrite parts of it. This is what
the world needs to get a more adaptable Rust.

I understand this is a thick requirement, but hey, you are
competing with C.

Yours,
Linus Walleij