Description
(I was talking to @huonw about embedded Rust the other day, and he suggested I write this up as an RFC issue. I hope this is in the correct place!)
I'm having a ton of fun hacking on kernels in Rust. Rust is a wonderful fit for the problem domain, and the combination of libcore
and custom JSON --target
specs makes the whole process very ergonomic. But there's one issue that keeps coming up on #rust-osdev
: libcore
requires floating point, but many otherwise reasonable environments place restrictions on floating point use.
Existing discussions of this issue can be found here:
- Compiling libcore without SSE leads to LLVM ERROR: SSE register return with SSE disabled. If you disable floats in LLVM and
rustc
, you break the parts oflibcore
that deal with floats. - Fixes #26449. A pull request to support
libcore
without floats, closed without merge. A version of this patch is provided by rust-barebones-kernel, and this patch is frequently recommended on#rust-osdev
. - Are floats really dependency free?. Briefly discusses the fact that
libcore
depends on floating point. - Pre-RFC: Dealing with broken floating point. This is a more general discussion of floating point issues, but at one point it proposes
#[cfg(float_is_broken)]
. Not sure how relevant this is.
Datum 1: Some otherwise reasonable processors do not support floating point
There's always been a market for embedded processors without an FPU. For the most part, these aren't pathologically weird processors. The standard ARM toolchain supports --fpu=none
. Many of the older and/or lower-end ARM chips lack FPUs. For example, the FPU is optional on the Cortex-M4.
Now, I concur (enthusiastically) that not all embedded processors are suitable for Rust. In particular, there are processors where the smallest integer types are u32
and i32
, making sizeof(char) == sizeof(uint32_t) == 1
in C, and where uint8_t
literally does not exist. There were once quite a few CPUs with 36-bit words. I agree that all these CPUs are all fundamentally unsuitable for Rust, because Rust makes the simplifying decision that the basic integer types are 8, 16, 32 and 64 bits wide, to the immense relief of everybody who programs in Rust.
But CPUs without floating point are a lot more common than CPUs with weird-sized bytes. And the combination of rustc
and libcore
is an otherwise terrific toolchain for writing low-level code for this family of architecture.
Datum 2: Linux (and many other kernels) forbid floating point to speed up syscalls and interrupts
Another pattern comes up very often:
- Everybody likes CPUs with a lot of floating point registers, and even a lot of vector floating point registers.
- Saving all those floating point registers during a syscall or hardware interrupt can be very expensive. You need to save all the registers to switch tasks, of course, but what if you just want to call
write
or another common syscall? - It's entirely possible to write large amounts of kernel code without needing floating point.
These constraints point towards an obvious optimization: If you forbid the use of floating point registers in kernel space, you can handle syscalls and interrupts without having to save the floating point state. This allows you to avoid calling epic instructions like FXSAVE
every time you enter kernel space. Yup, FXSAVE
stores 512 bytes of data.
Because of these considerations, Linux normally avoids floating point in kernel space. But ARM developers trying to speed up task switching may also do something similar. And this is a very practical issue for people who want to write Linux kernel modules in Rust.
(Note that this also means that LLVM can't use SSE2 instructions for optimizing copies, either! So it's not just a matter of avoiding f32
and f64
; you also need to configure your compiler correctly. This has consequences for how we solve this problem, below.)
Possible solutions
Given this background, I'd argue that "libcore
without floats" is a fairly well-defined and principled concept, and not just, for example, a rare pathological configuration to support one broken vendor.
There are several different ways that this might be implemented:
- Make it possible to disable
f32
andf64
when buildinglibcore
. This avoids tripping over places where the ABI mandates the use of SSE2 registers for floating point, as in Compiling libcore without SSE leads to LLVM ERROR: SSE register return with SSE disabled rust#26449. The rust-barebones-kernellibcore_nofp.patch
shows that this is trivially easy to do. - Move
f32
andf64
support out oflibcore
and into a higher-level crate. I don't have a good feel for the tradeoffs here—perhaps it would be good to avoid crate proliferation—but this is one possible workaround. - Require support for soft floats in the LLVM & rustc toolchain, even when the platform ABI mandates the use of SSE2 registers. But this is fragile and cumbersome, because it requires maintaining a (custom?) float ABI on platforms even where none exists. And this is currently broken even for
x86_64
(Compiling libcore without SSE leads to LLVM ERROR: SSE register return with SSE disabled rust#26449 again), so it seems like this approach is susceptible to bit rot. - Compile
libcore
with floats and then try to remove them again with LTO. This is hackish, and it requires the developer to leave SSE2 enabled at compilation time, which may allow SSE2-based optimizations to slip in even wheref32
andf64
are never mentioned, which will subtly corrupt memory during syscalls and interrupts. - Other approaches? I can't think of any, but I'm sure they exist.
What I'd like to see is a situation where people can build things like Linux kernel modules, pure-Rust kernels and (hypothetically) Cortex-M4 (etc.) code without needing to patch libcore
. These all seem like great Rust use cases, and easily disabling floating point is (in several cases) the only missing piece.