|
|
Subscribe / Log in / New account

Insulating layer?

Insulating layer?

Posted Oct 11, 2024 14:20 UTC (Fri) by asahilina (subscriber, #166071)
In reply to: Insulating layer? by jkingweb
Parent article: On Rust in enterprise kernels

Rust macros are much, much more powerful than C macros. To implement multiple firmware ABIs ergonomically and maintainably, you need to be able to encode the differences between the ABIs. In terms of structs, this means that some fields might be added, removed, or moved between firmware versions. Not only does the struct definition have to change, but also all the code that uses it.

In C, the best you can do is multiply compile the same file with a bunch of #ifdefs declaring the conditions under which certain structs exist or have a different type. This gets verbose quickly, it's ugly to compile, and you also need to wrap every single public top-level identifier in the file so it has a unique name when compiled for each version combination. You can't mix and match code that changes by version and code that doesn't in the same file, which forces code organization that might be different from what logically makes sense. And the more of the driver you cover with this mechanism, the more awkward it gets, which encourages you to add an explicit abstraction layer with dynamic dispatch, which is then slower and reduces the compiler's ability to optimize. Or you end up covering the entire driver in the scheme, which means all the internal interfaces within the driver need the name mangling applied, or you end up wanting to put everything into fewer files so you can use static, or doing something like having all the source files #included into a single compilation unit... and then there's the bug-proneness of this all, because C doesn't force you to initialize all fields of a struct, so if a given firmware build adds a field the compiler won't check that you actually handle it in every code paths that uses that struct.

Basically, it's a bunch of bad tradeoffs. We're doing something like this for the DCP (display) driver (written in C) and it's quite messy and bug prone.

In Rust, I wrote a rather ugly but functional proc macro [1] that allows you to apply multiple compilation to any top level block in a source file, and then conditionally compile any statement or declaration. The name mangling is automatic for declarations, and to reference them you just append ::ver to the name. Firmware definitions end up looking like [2] and the code that uses them like [3] (search for "ver"). Other parts of the driver can be pulled into the multiple compilation easily without any changes to code organization, and I only use dynamic dispatch (Rust trait objects) where it makes sense to insulate a significant chunk of shared code from the versioning. Then the compiler takes it all and basically creates N variants of the driver for each firmware/GPU combination, optimizing everything together, and sharing the bits that can be shared. If I accidentally mismatch the firmware declarations and their usage (extra field, missing field, wrong type) for any version variant, the compiler will complain.

[1] https://github.com/AsahiLinux/linux/blob/asahi-wip/rust/m...
[2] https://github.com/AsahiLinux/linux/blob/asahi-wip/driver...
[3] https://github.com/AsahiLinux/linux/blob/asahi-wip/driver...

That's just the versioning story, but Rust also helps get complex firmware interfaces right in general with its rich type system. Firmware pointers can be typed with the firmware type they point to as a generic parameter, even though from the CPU/Rust's perspective they aren't pointers or references at all, just a u64. I even have lifetime-bound strong firmware pointers that guarantee that the firmware object they point to outlives the firmware object containing the pointer.

We don't have this yet, but it's also possible to use macros to verify that a firmware struct declaration has no implicit padding and only uses types that allow all bit patterns, which is important to avoid undefined behavior and portability issues, and allow you to read/write those structs from the firmware without having to use "unsafe".


to post comments

Insulating layer?

Posted Oct 11, 2024 14:29 UTC (Fri) by adobriyan (subscriber, #30858) [Link] (2 responses)

Speaking of macros, can they do:

struct S {
int a;
#ifdef CONFIG_B
int b;
#endif
};

I've seen std::conditional_t implementation on some video (which looks almost exactly like C++ version) which makes me think they can not.

Insulating layer?

Posted Oct 11, 2024 15:11 UTC (Fri) by farnz (subscriber, #17727) [Link]

That's built-in, and can be done via macros, or via the existing #[cfg] attribute. Macros in general in Rust get an AST, and output a new AST, so can do pretty much anything (noting that only function-like macros get an arbitrary AST; attribute macros and derive macros must be given valid Rust source code).

But using the built-in #[cfg] mechanism, along with feature flags, you'd write that as:

struct S {
    a: i32,
    #[cfg(feature = "config_b")]
    b: i32
}

This says that the field b only exists if the feature flag "config_b" is set at compile time; there's also preset cfg items, like target_has_atomic, which lets you write #[cfg(target_has_atomic = "64")] to only include a chunk of code on platforms with support for 64 bit atomics.

Insulating layer?

Posted Oct 11, 2024 15:26 UTC (Fri) by khim (subscriber, #9252) [Link]

You don't need macros for that because that's handled by separate facility in Rust.

> I've seen std::conditional_t implementation on some video (which looks almost exactly like C++ version) which makes me think they can not.

You probably misunderstood the precise limitation, I'm afraid. Macros in Rust are incredibly powerful, but they still work with a token trees. You can support configurable code and conditional compilation with them just fine, but they don't have access to the type information. std::conditional_t (and whatever Rust equivalent you saw) work with types, not with defines, and yes, that's something macros couldn't touch (neither in C/C++ nor in Rust) simply because macros work before types come into existence.

If you go from C++ to Rust then yes, this limitation feels quite stifling. If you go from C to Rust… nope: C simply doesn't have anything similar to reflection. magic_enum couldn't be replicated in Rust, but then it doesn't work with C, either.

Insulating layer?

Posted Oct 12, 2024 1:46 UTC (Sat) by rcampos (subscriber, #59737) [Link]

Thanks for the great and detailed explanation!


Copyright © 2025, Eklektix, Inc.
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds