关注了就能看到更多这么棒的文章哦~
How to write Rust in the kernel: part 2
By Daroc Alden
June 27, 2025
Rust in the kernel
Gemini flash translation
https://lwn.net/Articles/1025232/
在 2023 年,藤田友则(Fujita Tomonori)为现有的Asix AX88796B嵌入式以太网控制器驱动程序编写了一个Rust版本。这个版本只有略多于 100 行代码,简洁到了驱动程序所能达到的极致,因此它成为了衡量在内核中编写 Rust 和 C 代码差异的一个有益基准。通过对比该驱动程序中使用的 Rust 语法、类型和 API 与对应的 C 版本,我们可以清晰地阐明这些差异。
对于已经熟悉 Rust 的读者来说,本文可能只是重温了一些基础知识,但我希望它仍然能为实现简单的 Rust 驱动程序提供有益的参考。AX88796B 驱动程序的C版本和Rust版本虽然惊人地相似,但仍存在一些重要差异,这些差异可能会让尝试简单地从一种语言重写到另一种语言的开发者感到困惑。
准备工作
这两个版本之间差异最小的部分是法律声明。与其他许多内核文件一样,Rust 驱动程序以一个SPDX注释(SPDX comment)开头,声明该文件受 GPL 许可证保护。在其下方是一个文档注释:
//! Rust Asix PHYs 驱动程序 //! //! 此驱动程序的C版本:[`drivers/net/phy/ax88796b.c`](./ax88796b.c)
正如前一篇文章所述,以 //! 开头的注释包含适用于整个文件的文档。接下来的几行是一个 use
语句,它相当于 C 语言中的 =#include=:
usekernel::{ c_str, net::phy::{self, reg::C22, DeviceId, Driver}, prelude::*, uapi,};
与 C 语言一样,Rust 模块也是从一个搜索路径开始,然后沿着目录树继续查找。但与 C 语言不同的是,一个 use
语句可以选择性地只导入模块中定义的某些项(item)。例如, DeviceId
并不是一个独立的模块,而是kernel::net::phy
模块内部的一个特定项。通过同时导入 kernel::net::phy::DeviceId
和整个 kernel::net::phy
模块,Rust 模块可以直接引用 DeviceId
,并以 phy::name
的形式引用 PHY 模块中的其他任何内容。这些项总是可以通过它们的完整路径来引用; use
语句只是引入了一个更短的本地别名。如果名称会引起歧义,编译器就会报错。
所有这些导入的项都来自 kernel
crate(Rust 库),它包含了主内核与 Rust 代码之间的绑定(bindings)。在用户空间的 Rust 项目中,程序通常也会从 std(Rust 的标准库)中导入一些内容,但在内核中这不可行,因为内核需要对内存分配和其他标准库抽象掉的细节有更精确的控制。基于同样的原因,内核 C 语言开发者也不能在内核中使用 libc 中的函数。 kernel::prelude模块包含了许多常见标准库函数的内核替代品;其余部分可以在 core
中找到,它是 std
中不涉及内存分配的子集。
在驱动程序的 C 版本中,下一步是定义一些常量,它们代表了该驱动程序支持的三种不同但相关的设备:AX88772A、AX88772C 和 AX88796B。在 Rust 中,项(item)不必在使用前声明——整个文件会被一次性考虑。因此,藤田选择稍微调整了顺序,将每个板卡的代码保存在各自的独立部分中;每个板卡对应的类型(例如 PhyAX88772A
等)则在后面定义。Rust 驱动程序的下一部分是一个宏调用,它为 PHY 驱动程序设置了必要的符号:
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 宏(macro)通常分为两种:属性宏(attribute macro),它们以 #[macro_name]
的形式书写,并修改其所出现之前的项;以及普通宏(normal macro),它们以 macro_name!()
的形式书写。还有一种不太常见的属性宏变体,以 #![macro_name]
的形式书写,它适用于其所出现的定义内部。普通宏可以使用任何匹配的括号来包含它们的参数,但总可以通过名称与括号之间强制性的感叹号来识别。约定俗成是,对于返回值的宏使用圆括号,而对于调用以定义结构体的宏使用花括号(此处即是这种情况),但这并非强制要求。用圆括号调用宏会得到相同的结果,但这样会使其他 Rust 程序员不太容易理解其作用。
宏的 drivers
参数包含了该驱动程序所涵盖的三种板卡类型的名称。每个驱动程序都必须与一些信息相关联,例如设备名称以及它应该激活的 PHY 设备 ID(PHY device ID)。在驱动程序的 C 版本中,这由一个单独的表来处理:
staticstructphy_driverasix_driver[]={...};
在 Rust 代码中,这些信息存储在每个板卡的代码中(见下文),因为所有 PHY 驱动程序都需要提供这些信息。总的来说, kernel::module_phy_driver!{}
宏的作用与 C 语言中的module_phy_driver()
宏相同。
接下来,Rust 驱动程序定义了代码稍后会用到的两个常量:
constBMCR_SPEED100: u16=uapi::BMCR_SPEED100asu16;constBMCR_FULLDPLX: u16=uapi::BMCR_FULLDPLXasu16;
在 Rust 中,每个值(value)的声明(与数据结构(data structure)不同)都以 const
或let
开头。前者是编译时常量——类似于 C 语言中简单的#define
宏。const
定义必须指定类型,而let
定义则可选。无论哪种情况,类型总是通过冒号与名称分隔开。因此,在这种情况下,这两个常量都是u16
类型的值,即 Rust 的无符号 16 位整数类型。末尾的as u16
部分是一个类型转换(cast),因为所引用的原始uapi::BMCR_*
常量是在 C 语言中定义的,并且默认情况下根据平台被假定为 32 位或 64 位。
一个实际的函数
在实际的驱动程序代码之前,最后一部分是一个用于对 Asix PHY 执行软复位(soft reset)的共享函数:
// 使用标准的BMCR_RESET位执行软件PHY复位,// 并轮询直到复位位被清除。// 关闭BMCR_RESET位是为了适应损坏的AX8796B// PHY实现,例如在Individual Computers的// X-Surf 100 Zorro卡上使用的那种。fnasix_soft_reset(dev: &mutphy::Device) -> Result{ dev.write(C22::BMCR, 0)?; dev.genphy_soft_reset()}
关于这个函数有几点值得注意。首先,它上面的注释不是文档注释。这不是问题,因为这个函数也是私有的——由于它是用 fn
而不是 pub fn
声明的,所以它在这个模块之外是不可见的。C语言中对应的就是 static
函数。在 Rust 中,默认情况正好相反,函数是私有的(static),除非另行声明。
函数的参数是一个名为 dev
的 &mut phy::Device
类型。引用(reference,以 & 符号书写)在许多方面是 Rust 最显著的特性;它们类似于指针,但在编译时提供了保证,使得某些类型的错误(例如没有同步机制的并发可变访问)不会发生。在这种情况下, asix_soft_reset()
接受一个可变引用(mutable reference)( &mut)。编译器保证在同一时间,没有其他函数可以持有对同一个phy::Device
的引用。这意味着函数体可以清除 BMCR
引脚并触发软复位,而无需担心并发干扰。
理解这个函数的最后一部分是其返回类型Result
,以及“try”操作符 ?=。在C语言中,一个可能失败的函数通常通过返回一个特殊的哨兵值(sentinel value)来指示,通常是一个负数。在Rust中也是如此,但这个哨兵值被称为 =Err
,它是 Result
枚举(enumeration)的一个可能值。另一个值是 Ok
,表示成功。 Err
和 Ok
都可以携带额外信息,但内核中的默认约定是 Err
携带一个错误号(error number),而 Ok
不携带额外信息。
检查错误然后立即将其传播给函数调用者的模式非常常见,以至于 Rust 引入了“try”操作符作为快捷方式。考虑一下驱动程序 C 版本中的相同函数:
staticintasix_soft_reset(structphy_device*phydev){intret;/* Asix PHY除非复位位翻转,否则不会复位 */ret=phy_write(phydev,MII_BMCR,0);if(ret <0)return ret;returngenphy_soft_reset(phydev);}
它执行了同样两个可能失败的库函数调用,但需要额外的语句来传播潜在的错误。在 Rust 版本中,如果第一次调用返回一个 Err
,try 操作符会自动将其返回。对于第二次调用,请注意该行没有以分号结束——这意味着函数调用的值也作为整个函数的返回值,因此任何错误也将返回给调用者。然而,漏掉分号并不容易被忽视,因为如果加上它,编译器会报错说该函数没有返回一个 Result
类型。
主要驱动程序
实际的驱动代码在三个不同的板卡之间略有不同。其中最简单的是 AX88786B,其实现从第124行开始:
structPhyAX88796B;
这是一个空结构体(empty structure)。这种类型的实际实例(instance)没有与之关联的存储空间——它不会占用其他结构体中的空间,size_of()
报告为 0,并且它没有填充字节(padding)——但整个类型仍然可以拥有全局数据(例如调试信息)。在这种情况下,空结构体被用来实现 Driver
抽象(abstraction),以便将 PHY 驱动程序所需的所有数据和函数捆绑在一起。当编译器被要求生成适用于 PhyAX88796B
的函数时( module_phy_driver!{}
宏会这样做),它将使用此定义:
#[vtable]implDriverforPhyAX88796B{constNAME: &'staticCStr=c_str!("Asix Electronics AX88796B");constPHY_DEVICE_ID: DeviceId=DeviceId::new_with_model_mask(0x003b1841);fnsoft_reset(dev: &mutphy::Device) -> Result{asix_soft_reset(dev)}}
常量和函数的定义方式与上述相同。 NAME
的类型使用了静态引用(static reference)(“ &'static CStr”),这种引用在程序的整个生命周期(lifetime)内都有效。C语言中对应的就是一个指向可执行文件数据段的 const
指针:它从不被分配、释放或修改,因此在程序的任何地方解引用(dereference)都是安全的。
驱动程序的这部分新的 Rust 特性是 impl
块,它用于实现一个trait(trait)。通常,一个程序会有多个不同的部分遵循相同的接口(interface)。例如,所有 PHY 驱动程序都需要提供名称、关联的设备 ID 以及一些实现驱动操作的函数。在 Rust 中,这种通用接口由一个 trait 来表示,它允许编译器执行静态类型分派(static type dispatch),根据 trait 函数的调用方式选择正确的实现。
当然,C语言不是这样工作的(尽管有时可以使用_Generic
手动实现类型分派)。在内核的 C 代码中,PHY 驱动程序由一个包含数据和函数指针的结构体(structure)表示。 #[vtable]
宏将 Rust 的 trait 转换为一个包含函数指针的单一 C 结构体。在上面对 module_phy_driver!{}
的调用中,对 PhyAX88796B
类型的引用让编译器找到了正确的 Driver
实现,并由此生成了正确的 C 结构体,以便与 C PHY 驱动程序基础设施集成。
显然,实现一个完整的 PHY 驱动程序会涉及更多函数。幸运的是,由于 PHY 设备存在标准接口(standard interface),这些函数在不同设备之间通常是相同的。如果驱动程序的定义中没有更具体的函数,C PHY 驱动代码将回退(fall back)到通用实现(generic implementation),因此 AX88796B 代码可以省略这些函数。此驱动程序支持的其他两个设备指定了更多自定义函数,以解决硬件怪癖(hardware quirks),但这些函数并不比已经展示的复杂多少。
总结
实现 PHY 驱动程序的步骤…
… 在 C 语言中: | … 在 Rust 中: |
编写模块样板代码(许可和作者信息, | 编写模块样板代码(许可和作者信息, |
实现驱动程序所需函数,跳过可以使用通用 PHY 代码的函数。 | 实现驱动程序所需函数,跳过可以使用通用 PHY 代码的函数。 |
将函数、名称、可选标志和 PHY 设备 ID 捆绑到 | 将函数、名称、可选标志和 PHY 设备 ID 捆绑到 trait 中; |
当然,许多驱动程序都有特定的硬件问题或其他复杂情况;内核软件的特点是其复杂性以及对底层细节的关注。本系列的下一篇文章将探讨内核中 C 和 Rust 代码之间接口的设计,以及在必要时添加新绑定的过程。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~