The Kernel Doesn't Speak Cargo
You've written a driver in Rust. It compiles. You run insmod my_driver.ko and the kernel screams at you. Or maybe you're staring at a C header file full of macros and wondering if there's a safer way to talk to hardware. Rust in the kernel is real, but it doesn't work like user-space Rust. The rules are different, the tooling is different, and the first thing you'll learn is that cargo is not your friend here.
Linux kernel modules usually come in two flavors: built-in and loadable. Built-in code is welded into the kernel image before the machine boots. Loadable modules are like USB drives; you plug them in and out while the system runs. Rust support in the kernel currently only covers the built-in flavor. You can write Rust code, but it has to be part of the kernel image from the start. You cannot compile a .ko file and load it dynamically. Think of Rust in the kernel as structural steel in a building. It's there from the foundation. It's not a furniture piece you can rearrange later.
Rust is Structural Steel
The Rust-for-Linux project integrates Rust into the kernel build system. This means your code goes through the same compilation pipeline as the C code, but with a Rust compiler. The kernel's build system, Kbuild, orchestrates everything. It finds your Rust files, invokes the specific rustc version pinned by the kernel, and links the result into vmlinux.
You don't run cargo build. You run make in the kernel source tree. The kernel crate provides the API. It's not std. It's a custom crate that mirrors kernel semantics. When the kernel boots, it calls your init function. If it returns Ok, the module lives. If it returns an error, the boot might halt or the module is skipped, depending on configuration.
Rust code in the kernel is welded into the image. If you need dynamic loading, you're out of luck for now.
Minimal Built-in Module
Here is a minimal example of a built-in Rust kernel module. This code lives inside the kernel source tree, typically under drivers/.
1. Create the Rust source file (drivers/example/rust_module.rs):
// drivers/example/rust_module.rs
use kernel::prelude::*;
/// The init module defines entry and exit points for the kernel module.
mod init {
use super::*;
/// Initialize the module. Returns Result to signal success or failure to the kernel.
pub fn init() -> Result<(), Error> {
// Log to the kernel ring buffer. This is how you debug in the kernel.
info!("Rust module initialized successfully!");
Ok(())
}
/// Cleanup when the kernel shuts down. Since this is built-in, exit rarely runs.
pub fn exit() {
info!("Rust module exiting.");
}
}
// Register the module with the kernel. Name must be unique across the entire kernel tree.
module!(
name: "rust_example",
init: init::init,
exit: init::exit,
);
2. Create the build configuration (drivers/example/Kconfig):
config RUST_EXAMPLE
tristate "Rust Example Module"
depends on RUST
default n
help
A simple example of a Rust kernel module.
3. Create the Makefile (drivers/example/Makefile):
obj-$(CONFIG_RUST_EXAMPLE) += rust_example.o
rust_example-objs := rust_module.o
4. Build the kernel:
Run the standard kernel build command from the kernel root directory:
make -j$(nproc)
If successful, the Rust code is compiled and linked directly into the vmlinux image. You can verify it is loaded by checking dmesg for the "Rust module initialized successfully!" message upon boot.
Convention aside: The community standard is to put Rust files alongside C files in the same directory. Kbuild handles the mixed language build automatically. You don't need a separate directory for Rust code.
Return an error on failure. The kernel will skip the module and keep booting, or halt if you marked it critical.
How the Build Actually Works
The kernel build process is strict. You must enable CONFIG_RUST=y in your kernel configuration. This flag tells Kbuild to look for Rust files and invoke the Rust compiler. Without it, your .rs files are ignored.
The kernel pins a specific version of rustc. You cannot use your system's Rust toolchain. The kernel ships with a toolchain definition, usually in the rust/ directory or fetched during build preparation. Using a mismatched compiler leads to ABI errors. The kernel relies on specific layout guarantees and compiler behaviors. If you use a newer rustc than the kernel expects, the build fails with obscure linker errors.
The kernel crate is built into the kernel tree. It provides types like Error, Result, and logging macros. It also provides safe wrappers around kernel APIs. You import everything from kernel::prelude::*. This is the convention. It pulls in the kernel-compatible versions of common types and traits.
Convention aside: kernel::prelude::* is the signal to other kernel developers that this file is using the kernel's Rust API. It replaces std::prelude::*. You never use std in the kernel.
The build system generates object files from your Rust code and links them with the rest of the kernel. The result is a single monolithic binary. There is no separate module file.
Realistic Driver Pattern
Real drivers usually register with subsystems and handle hardware state. A common pattern is checking for hardware presence and returning an error if the device is missing.
// drivers/example/rust_realistic.rs
use kernel::prelude::*;
mod init {
use super::*;
pub fn init() -> Result<(), Error> {
// Real modules often check for hardware presence or dependencies.
// If a required resource is missing, return an error to prevent loading.
if !is_hardware_present() {
error!("Required hardware not found. Aborting init.");
// Return a kernel error code. -ENODEV is standard for "No such device".
return Err(Error::from(-16));
}
info!("Hardware detected. Initializing Rust driver.");
Ok(())
}
pub fn exit() {
info!("Driver cleanup complete.");
}
}
// Helper function simulating hardware check.
fn is_hardware_present() -> bool {
// In real code, this would read a register or check ACPI tables.
// Kernel code often uses unsafe blocks for direct hardware access.
true
}
module!(
name: "rust_realistic",
init: init::init,
exit: init::exit,
);
This example shows error handling. The Error type in the kernel is an integer wrapper. You construct errors from negative integers representing standard POSIX error codes. -16 is ENODEV. The kernel uses these codes to communicate failure modes.
Convention aside: Keep unsafe blocks small and justified. The kernel codebase treats unsafe as a contract. You must prove that the code inside is safe. Use // SAFETY: comments to document the invariants. If you can't write the proof, you don't have a safe abstraction.
The No-Std Wall and Panics
The kernel is a no_std environment. This means the standard library is not available. std relies on OS syscalls for file I/O, networking, and threading. The kernel is the OS. You cannot call syscalls from the kernel. You use kernel APIs instead.
If you try to use String from std, the build fails. You must use kernel::prelude::* which provides kernel-compatible types. The kernel has its own allocator, its own string types, and its own synchronization primitives. Rc and Arc from std do not work. The kernel provides kref for reference counting. Mutex from std does not work. The kernel provides spinlocks and mutexes that integrate with the scheduler.
Panics are fatal. In user-space Rust, a panic might unwind the stack and terminate the thread. In the kernel, a panic crashes the machine. There is no recovery. Treat unwrap as a bomb. If you call unwrap on a None value, the system halts. Write defensive code. Check for errors. Return Err when things go wrong.
Convention aside: The kernel community prefers explicit error handling over unwrap. Use ? to propagate errors. Return Result from functions. This keeps the code robust and prevents crashes.
A panic in the kernel is a crash. Write defensive code or live with the reboot.
Toolchain and Configuration
You must configure the kernel to support Rust. Run make menuconfig or make nconfig and enable CONFIG_RUST. This option is usually under "Enable Rust support". You also need to ensure the Rust toolchain is available. The kernel build system checks for the toolchain and downloads it if necessary.
The toolchain is pinned to a specific commit or version. This ensures ABI stability. The kernel developers test against this version. If you update your toolchain manually, you risk breaking the build. Stick to the toolchain provided by the kernel.
Convention aside: Use make olddefconfig to update your configuration when switching kernel versions. This preserves your settings while adding new defaults. It helps avoid configuration drift.
Async support in the kernel uses a custom runtime. Standard tokio or async-std are not compatible. The kernel has its own scheduler and I/O model. Use the kernel crate's async primitives. They integrate with the kernel's task system.
When to Use Rust vs C
Use Rust for new drivers when you want memory safety and modern tooling, and the code will be built into the kernel image. Use C for legacy drivers when you are maintaining existing code that hasn't been ported, or when you need to interface with C-only APIs that lack Rust bindings. Use Rust for built-in modules when the code is critical to boot and doesn't need to be loaded dynamically. Use C for loadable modules when you need insmod or modprobe support, as Rust dynamic loading is not yet available. Use Rust when you are building a safe abstraction over hardware, like a device driver or a filesystem. Use C when you are writing low-level boot code or architecture-specific assembly wrappers.
Rust in the kernel is for the long haul. Build it in, keep it safe, and let the compiler catch the bugs before the machine boots.