yazzy

Plain ol' reading

How to write Rust in the kernel: part 2

LWN.net ・ lwn.net

Welcome to LWN.net

The following subscription-only content has been made available to you by an LWN subscriber. Thousands of subscribers depend on LWN for the best news from the Linux and free software communities. If you enjoy this article, please consider subscribing to LWN. Thank you for visiting LWN.net!

In 2023, Fujita Tomonori wrote a Rust version of the existing driver for the Asix AX88796B embedded Ethernet controller. At slightly more than 100 lines, it's about as simple as a driver can be, and therefore is a useful touchstone for the differences between writing Rust and C in the kernel. Looking at the Rust syntax, types, and APIs used by the driver and contrasting them with the C version will help illustrate those differences.

Readers who are already conversant with Rust may find this article retreads some basics, but it is my hope that it can still serve as a useful reference for implementing simple drivers in Rust. The C version and the Rust version of the AX88796B driver are remarkably similar, but there are still some important differences that could trip up a developer performing a naive rewrite from one to the other.

The setup

The least-different thing between the two versions is the legalities. The Rust driver starts with an SPDX comment asserting that the file is covered by the GPL, as many files in the kernel do. Below that is a documentation comment:

//! Rust Asix PHYs driver
    //!
    //! C version of this driver: [`drivers/net/phy/ax88796b.c`](./ax88796b.c)

As mentioned in the previous article, comments starting with //! contain documentation that applies to the entire file. The next few lines are a use statement, the Rust analogue of #include:

use kernel::{
        c_str,
        net::phy::{self, reg::C22, DeviceId, Driver},
        prelude::*,
        uapi,
    };

Like C, Rust modules are located starting from a search path and then continuing down a directory tree. Unlike C, a use statement can selectively import only some items defined in a module. For example, DeviceId is not a separate module, but rather a specific item inside the kernel::net::phy module. By importing both kernel::net::phy::DeviceId and kernel::net::phy as a whole, the Rust module can refer to DeviceId directly, and anything else from the PHY module as phy::name. These items can always be referred to by their full paths; a use statement just introduces a shorter local alias. If a name would be ambiguous, the compiler will complain.

All of these imported items come from the kernel crate (Rust library), which contains the bindings between the main kernel and Rust code. In a user-space Rust project, a program would usually also have some imports from std, Rust's standard library, but that isn't possible in the kernel, since the kernel needs more precise control over allocation and other details that the standard library abstracts away. Kernel C developers can't use functions from libc in the kernel for much the same reason. The kernel::prelude module contains kernel replacements for many common standard-library functions; the remainder can be found in core, the subset of std that doesn't allocate.

In the C version of the driver, the next step is to define some constants representing the three different, but related, devices this driver supports: the AX88772A, the AX88772C, and the AX88796B. In Rust, items do not have to be declared before use — the entire file is considered at once. Therefore, Fujita chose to reorder things slightly to keep the code for each board in its own section; the types for each board (PhyAX88772A and so on) are defined later. The next part of the Rust driver is a macro invocation that sets up the necessary symbols for a PHY driver:

kernel::module_phy_driver! {
        drivers: [PhyAX88772A, PhyAX88772C, PhyAX88796B],
        device_table: [
            DeviceId::new_with_driver::<PhyAX88772A>(),
            DeviceId::new_with_driver::<PhyAX88772C>(),
            DeviceId::new_with_driver::<PhyAX88796B>()
        ],
        name: "rust_asix_phy",
        authors: ["FUJITA Tomonori <fujita.tomonori@gmail.com>"],
        description: "Rust Asix PHYs driver",
        license: "GPL",
    }

Rust macros come in two general kinds: attribute macros, which are written #[macro_name] and modify the item that they appear before, and normal macros, which are written macro_name!(). There is also a less common variant of attribute macros written #![macro_name] which applies to the definition that they appear within. Normal macros can use any matching set of braces to enclose their arguments, but can always be recognized by the mandatory exclamation mark between the name and the braces. The convention is to use parentheses for macros that return a value and braces for macros that are invoked to define a structure (as is the case here), but that is not actually required. Invoking the macro with parentheses would have the same result, but it would make it less obvious to other Rust programmers what is happening.

The drivers argument to the macro contains the names of the three board types this driver covers. Each driver has to be associated with information such as the name of the device and the PHY device ID that it should be active for. In the C version of the driver, this is handled by a separate table:

static struct phy_driver asix_driver[] = { ... };

In the Rust code, this information is stored in the code for each board (see below), since all PHY drivers need to provide it. Overall, the kernel::module_phy_driver!{} macro serves the same role as the module_phy_driver() macro in C.

Next, the Rust driver defines two constants that the code uses later:

const BMCR_SPEED100: u16 = uapi::BMCR_SPEED100 as u16;
    const BMCR_FULLDPLX: u16 = uapi::BMCR_FULLDPLX as u16;

Every declaration of a value (as opposed to a data structure) in Rust starts with either const or let. The former are compile-time constants — like a simple #define in C. Types are mandatory for const definitions, but optional for let ones. In either case, the type always appears separated from the name by a colon. So, in this case, both constants are u16 values, Rust's unsigned 16-bit integer type. The as u16 part at the end is a cast, since the original uapi::BMCR_* constants being referenced are defined in C and assumed to be 32 or 64 bits by default, depending on the platform.

An actual function

The final piece of code before the actual drivers is a shared function for performing a soft reset on Asix PHYs:

// Performs a software PHY reset using the standard
    // BMCR_RESET bit and poll for the reset bit to be cleared.
    // Toggle BMCR_RESET bit off to accommodate broken AX8796B
    // PHY implementation such as used on the Individual
    // Computers' X-Surf 100 Zorro card.
    fn asix_soft_reset(dev: &mut phy::Device) -> Result {
        dev.write(C22::BMCR, 0)?;
        dev.genphy_soft_reset()
    }

There's a few things to notice about this function. First of all, the comment above it is not a documentation comment. This isn't a problem because this function is also private — since it was declared with fn instead of pub fn, it's not visible outside this one module. The C equivalent would be a static function. In Rust, the default is the opposite way around, with functions being private (static) unless declared otherwise.

The argument to the function is an &mut phy::Device called dev. References (written with an &) are in many ways Rust's most prominent feature; they are like pointers, but with compile-time guarantees that certain classes of bugs (such as concurrent mutable access without synchronization) can't happen. In this case,asix_soft_reset() takes a mutable reference (&mut). The compiler guarantees that no other function can have a reference to the same phy::Device at the same time. This means that the body of the function can clear the BMCR pin and trigger a soft reset without worrying about concurrent interference.

The last part of the function to understand is the return type,Result, and the "try" operator, ?. In C, a function that could fail often indicates this by returning a special sentinel value, typically a negative number. In Rust, the same thing is true, but the sentinel value is called Err instead, and is one possible value of the Result enumeration. The other value is Ok, which indicates success. Both Err and Ok can carry additional information, but the default in the kernel is for Err to carry an error number, and for Ok to have no additional information.

The pattern of checking for an error and then immediately propagating it to a function's caller is so common that Rust introduced the try operator as a shortcut. Consider the same function from the C version of the driver:

static int asix_soft_reset(struct phy_device *phydev)
    {
        int ret;

        /* Asix PHY won't reset unless reset bit toggles */
        ret = phy_write(phydev, MII_BMCR, 0);
        if (ret < 0)
            return ret;

        return genphy_soft_reset(phydev);
    }

It performs the same two potentially fallible library function calls, but needs an extra statement to propagate the potential error. In the Rust version, if the first call returns an Err, the try operator automatically returns it. For the second call, note how the line does not end with a semicolon — this means the value of the function call is also the return value of the function as a whole, and therefore any errors will also be returned to the caller. The missing semicolon is not easy to forget, however, because adding it in will make the compiler complain that the function does not return a Result.

The main driver

The actual driver code differs slightly for the three different boards. The simplest is the AX88786B, the implementation of which starts on line 124:

struct PhyAX88796B;

This is an empty structure. An actual instance of this type has no storage associated with it — it doesn't take up space in other structures,size_of() reports 0, and it has no padding — but there can still be global data for the type as a whole (such as debugging information). In this case, an empty structure is used to implement the Driver abstraction, in order to bundle all of the needed data and functions for a PHY driver together. When the compiler is asked to produce functions that apply to a PhyAX88796B (which the module_phy_driver!{} macro does), it will use this definition:

#[vtable]
    impl Driver for PhyAX88796B {
        const NAME: &'static CStr = c_str!("Asix Electronics AX88796B");
        const PHY_DEVICE_ID: DeviceId =
            DeviceId::new_with_model_mask(0x003b1841);

        fn soft_reset(dev: &mut phy::Device) -> Result {
            asix_soft_reset(dev)
        }
    }

The constant and function definitions work in the same way as above. The type of NAME uses a static reference (" &'static CStr "), which is a reference that is valid for the entire lifetime of the program. The C equivalent is a const pointer to the data section of the executable: it is never allocated, freed, or modified, and is therefore fine to dereference anywhere in the program.

The new Rust feature in this part of the driver is the impl block, which is used to implement a trait. Often, a program will have multiple different parts that conform to the same interface. For example, all PHY drivers need to provide a name, associated device ID, and some functions implementing driver operations. In Rust, this kind of common interface is represented by a trait, which lets the compiler perform static type dispatch to select the right implementation based on how the trait functions are called.

C, of course, does not work like this (although _Generic can sometimes be used to implement type dispatch manually). In the kernel's C code, PHY drivers are represented by a structure that contains data and function pointers. The #[vtable] macro converts a Rust trait into a singular C structure full of function pointers. Up above, in the call to module_phy_driver!{}, the reference to the PhyAX88796B type lets the compiler find the right Driver implementation, and from there produce the correct C structure to integrate with the C PHY driver infrastructure.

There are obviously more functions involved in implementing a complete PHY driver. Luckily, these functions are often the same between different devices, because there is a standard interface for PHY devices. The C PHY driver code will fall back to a generic implementation if a more specific function isn't present in the driver's definition, so the AX88796B code can leave them out. The other two devices supported in this driver specify more custom functions to work around hardware quirks, but those functions are not much more complicated than what has already been shown.

Summary

Steps to implement a PHY driver...

... in C:... in Rust:
Write module boilerplate (licensing and authorship information,#include statements, etc.).Write module boilerplate (licensing and authorship information, use statements, a call to module_phy_driver!{}).
Implement the needed functions for the driver, skipping functions that can use the generic PHY code.Implement the needed functions for the driver, skipping functions that can use the generic PHY code.
Bundle the functions along with a name, optional flags, and PHY device ID into a struct phy_driver and register it with the PHY subsystem.Bundle the functions along with a name, optional flags, and PHY device ID into a trait; the #[vtable] macro converts it into the right form for the PHY subsystem.

Of course, many drivers have specific hardware concerns or other complications; kernel software is distinguished by its complexity and concern with low-level details. The next article in this series will look at the design of the interface between the C and Rust code in the kernel, as well as the process of adding new bindings when necessary.





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