First, the context.
For those who are not aware, Rust for Linux is a project within the Linux developer community to add kernel support for the Rust programming language with the objective of developing drivers that can sit alongside regular C drivers in tree. The project is still somewhat young, but it's already reached the significant milestone of a first driver being merged.
Two years ago i came across Rust for Linux thank to Asahi Lina's
M1 GPU driver development streams. i went through a quick
tutorial, and voilĂ , there was a driver:
rust_foxes
.
Two Years Later
Now, two years later almost, what has changed?
First, the Rust for Linux project deprecated the rust
branch everyone used to
experiment. It was full of abstractions for many, many things (including
miscellaneous devices, user IO, etc), but its development had not followed the
regular process of (painstakingly) posting things on the LKMLs, and people
reviewing them.
So almost everything was basically thrown into the trash. What is coming now is
definitely an opportunity to do better (eg. properly handling multiple memory
allocators, properly defining memory allocation flags, a better structure for
the alloc
crate, etc).
That also means that the driver no longer really works unless you build against
the old rust
branch from back in the days of 6.0... Well it would have, if i
had rebased my code. Even the API for simple modules changed between me writing
the module, and when rust
was abandoned.
For example, consider Module::init
, the entrypoint of a module. In my code it
is:
fn init(_name: &'static CStr, _module: &'static ThisModule) -> Result<Self> {
The current definition used in both rust
and rust-next
(the up-to-date
branch used to push RFL's updates to Linus):
fn init(_module: &'static ThisModule) -> Result<Self> {
Thankfully, during the last two years, i have spent a big part of my time working on drivers using RFL's tree, including making abstractions.
Update Time
Going through the code, as someone who now has 2 more years of experience not only writing drivers for RFL but also, crucially, abstractions, i listed several things in need of update:
- The
Module
trait - Missing
miscdev
abstractions - Missing
io_buffer
abstractions
For the missing abstractions, i dove in the code of the old rust
branch and
pulled out the big chunks of code my driver had previously relied on. Then, i
brushed up very little details to update the code.
As an example, consider code that turns a Result
into an errno code. RFL's
rust
had a macro for that purpose, which is now replaced by
kernel::error::from_result
.
In a similar fashion, everything that allocates now needs specific memory
allocation flags to be passed so that the proper call is made and gfp
is not
always assumed.
i trimmed the code down to the minimum needed to open and read the device:
- Abstractions for
struct file_operations
- Abstractions to copy and write from userland
- Abstractions for IO buffers
- Abstractions for simple devices
- Abstractions to register a
miscdevice
The set of changes totals +1240 lines.
Now, onto the driver
After fixing imports and the problem of Module
trait implementation, i decided
to flex a little by adding a new feature to the driver.
Previously, the driver would mindlessly write foxes one after the other, ad infinitum. That was funny, but i can do better.
The trait for file operations has two associated types. One of these types
represents a data type passed to the handler of the open()
event. The precise
instance is stored in memory when we request the registration:
let reg = miscdev::Options::new()
.mode(0o444)
.register_new(kernel::fmt!("foxes"), ())?;
In this example, and in my current version of the driver, that type is ()
.
A second associated type tells the file operations what type is passed for all
callbacks within the same open/release
session. That is, the instance is
created during the open
callback, and destroyed at release
. A borrowed
version of the data is then passed for all callbacks... including read
.
The read
callback is the only one i implement, and the only one i need.
Ironically, i use it to write in the buffer the user provided. Previously, that
writing filled the buffer given by the user as much as possible, including with
partial foxes (first few bytes of an incomplete UTF-8 emoji). Now, with the
session data type set to Box<AtomicUsize>
, i obtained a &AtomicUsize
on all
callbacks, and could now play with it.
Importantly, all core atomic scalar types can be modified through a non-mutable reference. That was all i was going to get anyways:
#[vtable]
impl Operations for FoxDev {
type Data = Box<AtomicUsize>;
fn open(_: &(), _file: &File) -> Result<Self::Data> {
Ok(Box::new(AtomicUsize::new(200), GFP_KERNEL)?)
}
fn read(
data: &AtomicUsize,
_file: &File,
writer: &mut impl IoBufferWriter,
offset: u64,
) -> Result<usize> {
//...
}
}
The really simple part was changing the writing code so that it checks the
current count remaining, limits itself to that value (or not if the buffer is
small), and then subtracts the number of foxes fully written. Once the count is
exhausted, and a round of read
returns 0 bytes read, your program will likely
call close()
.
With that code in place, all calls to cat /dev/foxes
, for example, will show
the same number of foxes before exiting.
In a subsequent modification (one i have not pushed to the repository), i even
made it so the count became global to all different reading attempts. In order
to do that, i wrapped a AtomicUsize
in an Arc
(Atomic Referenced Counter),
and used that as a data type for the open
callback, at which point i cloned
the Arc
and used it for all callbacks as well. The count was decreased in a
similar way, except this time it was global to multiple parallel calls.
Conclusion
i underwent this little restoration project as an attempt to not lose my mind writing a more complex, fully-fledged driver for RFL. The cute little character device that prints foxes is a cute example and the first example of code i used in the project, so i would like it to remain up-to-date, and useful to people who want to get into the project.
Reflecting quickly on my (almost) two years of practice, i realized that while i
gained a certain amount of skepticism regarding the capacity of the project to
onboard newcomers to the kernel (myself included), i gained a lot of knowledge
of how Linux works internally, but also how FFI in Rust interacts with it, and
how deep primitives like cells, tools like Arc
or Box
, and regular code can
work together. It's been a journey, and while i hope to take a break from more
serious endeavors around RFL, i will definitely keep tinkering.