back
101 comments
The LKM post mentions binary size improvements.

One issues I have had with Rust applications is the huge binary size (yes, I know this has improved a bit lately). Is there a good comparison between kernel C and kernel Rust code in this regard?

This is a good guide on building small Rust binaries: https://github.com/johnthagen/min-sized-rust

This talks about going to extreme lengths on making the smallest Rust binary possible, 400 bytes when it was written, https://darkcoding.net/software/a-very-small-rust-binary-ind...

The thing is, you lose a lot of nice features when you do this, like panic unwinding, debug symbols, stdlib… for kernel and some embedded development it’s definitely important, but for most use cases, does it matter?

> debug symbols

In ye olden days it was common to distribute a binary without debug symbols, but to keep a copy of them for every released build¹. If an application crashed (panicked, signalled, etc.) you got a core dump that you could debug using the stripped binary together with the symbol file. This gave you both smaller binary sizes and full debugging capability at the cost of some extra administration. I'm not sure if this is possible with "stock" Rust, but if you need lean binaries but want to do forensic investigation it's something to look into.

1. https://sourceware.org/gdb/current/onlinedocs/gdb.html/Separ...

From (1) I guess most of the binsize comes from stdlib that the kernel does not use, so I guess that's two different problems.

(1) https://github.com/johnthagen/min-sized-rust

That wouldn’t explain what the size improvements are from the upgrade as the kernel always built nostd.
Most size issues come from not using release builds, using too many dependencies, or overuse of generics. The rust std lib being linked in statically also contributes.The kernel shouldn't suffer from any of these problems. Plenty of embedded use is able to use rust in highly constrained environments without size issues compared to C.
If you do release builds, strip debug symbols and turn LTO from thin to full, do dependencies and the static stdlib still matter? You should be only paying for code that's called at that point.

At that point I suspect the biggest culprits are overuse of monomorphisation, and often just more stuff happening compared to equivalent C++ code because the language makes larger code bases more maintainable. I'd also count some niceties in that category like better string formatting or panic handling, which is an insignificant cost in any larger software but appears big in tiny hello-world type programs.

Saying part of the problem is “using too many dependencies” is not an overly helpful thing if the ecosystem keeps on trying to download 3Gb of build dependencies because you tried to use some simple little library. The problem is obvious, it’s the solution that is much more difficult.
> One issues I have had with Rust applications is the huge binary size

Turn off the standard library and your binaries can be incredibly small. This is how it’s used in microcontrollers and the Linux Kernel doesn’t use the full standard library either.

Due to dead code elimination, the compiler already omits all of that part of stdlib that your code doesn’t use.
So one issue I can imagine being the culprit with rust is the specializing / c++ style semantics of rust generics. C code generics tend to be void* flavored or point to a struct of function pointers. Which will generate less code. Not sure how this translates to the kernel setting thoughb
That is true. Rust makes it easy to overuse monomorphisation. There are tools like `cargo-bloat` that find these.

However, most complaints are about size of “Hello World”, which in Rust is due to libstd always having debug info (to be fixed soon), and panic handling code that includes backtrace printing (because print to stdout can fail).

Printing of backtrace is very bloaty, because it parses and decompresses debug info.

Another thing just to mention here is `strip`, which IIRC `cargo build --release` doesn't do by default. I think `stripping` binaries can reduce binary size by up to 80-85% in some cases (but certainly not all; just tried it locally on a 1M rust binary and got 40% reduction).

FWIW, you can configure this in Cargo.toml:

[profile.release] strip = true

Check out Making Rust binaries smaller by default (https://kobzol.github.io/rust/cargo/2024/01/23/making-rust-b...). Previously discussed a few weeks ago at https://news.ycombinator.com/item?id=39112486.

That change will be live on 21st March, so manual strips won't be required after that.

You can strip C compiled binaries too. And that halves the binary size. The point is for example a hello world Rust binaries is 300kb after striping while C compiled one is 15kb. A difference of 20 times.
Are you talking about https://github.com/rust-lang/compiler-team/issues/688 ? I think that issue provides a lot of interesting context for this specific improvement.
> is the huge binary size

Can you quantify this? How big is too big? Ideally for a real program, and not an experiment to make the tiniest possible program.

On my Windows machine ripgrep rg.exe is just 4.2mb. Making that smaller feels irrelevant.

I’m not convinced that binary size is a real problem. But I’m open to evidence!

It would be nice if it fit on a standard size 1.44MB floppy, but given that I haven't used a floppy drive in about a decade, yeah I guess it doesn't matter much.
Do I understand it correctly that upgrading to a new rust version is mostly implementing new best practices and new features, instead of needing to "fix" your code, as rust is backwards compatible?

I've only used rust nightly for my own projects and didn't give too much thought about rust versions

For normal projects yes.

And you can use clippy to tell you about changes you should make.

For example, in my projects I run this in the CI pipeline:

  cargo clippy --all-targets --all-features
and

  cargo fmt --all --check
In addition to the regular test and build steps.

This both means that I follow clippy recommendations and cargo fmt in the first place, and also that my CI tells me about any clippy changes if I didn’t notice them myself as well as any formatting I’m not following. In my main IDE I auto format the code of course. But sometimes I make small changes in vim and don’t run the format step myself so it’s nice to have for that reason as well.

For the integration of Rust into the Linux kernel I imagine it’s a bit more convoluted.

For Rust on Linux it's a bit more involved because they use nightly features, which can change from day to day. In practice there's an implicit tiered strata, with features that rarely change and features that frequently change in bursts. I would like it if we formalized that distinction a bit. We already mark when a feature is very likely to change (unstable_features), but not when they are very close to being stabilized.
Yes, generally. For example, recently David Tolnay shared the amount of burden Meta has when upgrading the compiler: https://old.reddit.com/r/rust/comments/19dtz5b/freebsd_discu...

> I estimate it's about ½ hour per 1 million lines, on average.

That being said, Rust for Linux isn't using stable Rust, so they have a higher burden than projects that do.

Rust-for-Linux made it more complicated for themselves, because they chose to enable unstable/experimental features of the compiler without waiting until they’re released, so they don’t get the stability and compatibility guarantees that normal Rust projects get.
The alternative was to do the same and also not merge what they had. Stuff like custom allocator support is not optional.
Rust is backwards compatible when you stick to stable features, but the kernel uses unstable features that can and do incur breaking changes.

https://github.com/Rust-for-Linux/linux/issues/2

It seems prudent to limit rust usage in the kernel until that list can be burned down to zero. It makes sense that you need to at least get rust in the kernel to find out what missing features you need to have implemented and stabilized, but excessive use will make folks lives painful as they try to track upstream rust releases.
If you ignore dependencies and stick to stable features yes.

If you include dependencies then it can happen that a dependency relies on unstable features. In which case you might have to upgrade the library version (if they support the new compiler version). The library might have changed the API by then which would force you to change your code.

Except for the above use case, upgrades to the latest version of the compiler have been painless for me.

Dependencies are not allowed to use unstable features either with the stable compiler. The only exception is the standard library, which uses numerous unstable features even with a stable distribution of Rust.
So this is a huge tangent but couldn't most of the uses of the non_null!() macro in this diff just be (safe!) pointer comparisons or subtractions, without the unsafe{} logic to convert a pointer value to a reference just for the purposes of comparison or subtraction? https://lore.kernel.org/lkml/20240217002717.57507-1-ojeda@ke...
Although I think I recall that the kernel doesn't use Rust's standard library (which is consistent with the diff you linked), it's possible that the standard library's documentation on the pointer subtraction might reference a concern they could share (https://doc.rust-lang.org/std/primitive.pointer.html#method....):

> If any of the following conditions are violated, the result is Undefined Behavior:

> * Both the starting and resulting pointer must be either in bounds or one byte past the end of the same allocated object.

> * The computed offset cannot exceed isize::MAX bytes.

> * The offset being in bounds cannot rely on “wrapping around” the address space. That is, the infinite-precision sum must fit in a usize.

> Most platforms fundamentally can’t even construct such an allocation. For instance, no known 64-bit platform can ever serve a request for 263 bytes due to page-table limitations or splitting the address space. However, some 32-bit and 16-bit platforms may successfully serve a request for more than isize::MAX bytes with things like Physical Address Extension. As such, memory acquired directly from allocators or memory mapped files may be too large to handle with this function.

> Consider using wrapping_sub instead if these constraints are difficult to satisfy. The only advantage of this method is that it enables more aggressive compiler optimizations.

If their pointer subtraction uses similar semantics, there might be issues if they want to compare pointers from different allocation objects, are worried about 32-bit or 16-bit platforms, or maybe even consider the performance concerns too worrisome. The Rust I write tends to be a bit higher-level and doesn't require unsafe, so my instinctual reaction to "using unsafe for performance" generally errs on the same of abject terror, but it's a fundamental part of what makes the safe side of abstractions I use possible, and the kernel is probably one of those places that needs to do that sometimes, so I'd reluctantly have to admit I'm probably not qualified to evaluate whether these cases would merit unsafety for performance alone, but the first two concerns sound like legitimate things that the kernel would need to handle.

> Although I think I recall that the kernel doesn't use Rust's standard library

Rust's standard library has three elements

core has stuff you get with the Rust language, like any use of Rust, Rust for Linux has core. You could technically implement Rust without core, or at least, without most of it, but that's not really the Rust language, you've instead made your own weird fork.

[T]::sort_unstable() is a core function which sorts a slice of some Ordered type T but may re-arrange elements despite them comparing equal hence the word "unstable".

alloc depends on an allocator. You may not have an allocator, e.g. you're a tiny embedded controller, in which case you likely don't want and can't use this. Rust for Linux re-implements alloc, basically cloning the "official" alloc and fiddling with it.

Vec::try_reserve() is a feature found in alloc, it tries to allocate enough space to ensure your Vec has a certain amount of capacity beyond its current size, and if not reports it could not.

std further depends on an Operating System, it offers exciting features like knowing what the time is, reading a file, connecting to a remote service over TCP/IP, or making a thread. Rust for Linux does not provide std.

File::create() is a std function which creates files.

The function you were interested in is part of core (although you were looking at its re-export from std) and so yes, it exists in Rust for Linux.

> If their pointer subtraction uses similar semantics, there might be issues if they want to compare pointers from different allocation objects, are worried about 32-bit or 16-bit platforms, or maybe even consider the performance concerns too worrisome.

Any of these type of issue would equally invalidate using unsafe{} to cast the pointer to a reference, which is what non_null! does.

My wishlist would include gradually refactoring core in Rust and formal verification a-la seL4 to prove correctness. There's no point to refactor churn from one language religion to another for low entropy, core code without improvements in assurance that it's also provably bug-free, race-free, and secure while also being as fast or possibly faster.
How many % of the kernel is Rust now, in terms of LoC?
Rust: 20,887 lines

C: 33,351,596 lines

(That's just doing 'wc -l' rather than using any proper code metrics tool)

I've run tokei [0] on the Linux git repository on the latest commit [1] and this was the output:

  ===============================================================================
   Language            Files        Lines         Code     Comments       Blanks
  ===============================================================================
   C                   33553     23772322     17694564      2662642      3415116
   C Header            24554      9562920      7395591      1436546       730783
   Device Tree          5041      1512839      1240129        76384       196326
   ReStructuredText     3473       711669       539971            0       171698
   JSON                  788       443098       443096            0            2
   YAML                 3905       433860       352107        16626        65127
   GNU Style Assembly   1317       372131       271873        55613        44645
   Shell                 894       172036       120033        21590        30413
   Plain Text           1739       151033            0       123992        27041
   Makefile             2946        76889        52985        12355        11549
   Python                203        68545        54627         4452         9466
   SVG                    74        49420        48159         1171           90
   Perl                   59        43992        34124         4074         5794
   Happy                  10         6069         5359            0          710
   Assembly                5         3319         3065            0          254
   C++                     5         2138         1860           61          217
   BASH                   59         1943         1318          335          290
   Unreal Script           5          707          445          158          104
   ASN.1                  16          660          445           87          128
   Autoconf                5          429          373           26           30
   LD Script               8          376          288           29           59
   CSS                     3          295          172           69           54
   Gherkin (Cucumber)      1          291          199           58           34
   TeX                     1          236          156           74            6
   XSL                    10          200          122           52           26
   HEX                     2          173          173            0            0
   Module-Definition       2          128          113            0           15
   C++ Header              2          125           59           55           11
   RPM Specfile            1          108           93            1           14
   Objective-C             1           89           72            0           17
   Vim script              1           42           33            6            3
   Markdown                1           36            0           27            9
   Automake                3           31           23            3            5
   Ruby                    1           29           25            0            4
   INI                     2           13            6            5            2
   TOML                    1           12            2            9            1
   Apache Velocity         1           12           12            0            0
   CMake                   2            8            8            0            0
  -------------------------------------------------------------------------------
   Rust                   64        12637         9489         1612         1536
   |- Markdown            55         8243          808         5557         1878
   (Total)                          20880        10297         7169         3414
  -------------------------------------------------------------------------------
   HTML                    2           28           22            3            3
   |- JavaScript           1            7            7            0            0
   (Total)                             35           29            3            3
  ===============================================================================
   Total               78760     37400888     28271191      4418115      4711582
  ===============================================================================

So If we would only count code and not comments, it is only 9489 LoC Rust. Which would be about 0.03% and if we take all lines and not only LoC it would be around 0.05%

[0] https://github.com/XAMPPRocky/tokei

[1] https://github.com/torvalds/linux/commit/b401b621758e46812da...

How much of those 20K lines are Rust infrastructure, versus drivers written in Rust?
Ok. Does that include device drivers?
At least according to the Github's language breakdown for https://github.com/Rust-for-Linux/linux, C is still 98.3% of the repository, and Rust is in the 0.1% of "others".
Curious, why Rust instead of Zig?
Can Zig guarantee perfect memory safety?
It doesn't need to. Memory issues are easy to catch with good tests and Rust approach to memory safety has its own trade-offs.
Rust is going to kill LFS
LFS is Linux from Scratch right? (https://www.linuxfromscratch.org/index.html)

What are the implications of using Rust on building Linux?

Surely LFS will survive? Might need to update a few steps is all.
Explain because I don't see that happening.