编码指南

本文档描述了如何在内核中编写 Rust 代码。

风格与格式化

代码应当使用 rustfmt 进行格式化。这样一来,偶尔为内核贡献代码的人就不需要去学习和记住另一套风格指南。更重要的是,审查者和维护者无需再花时间指出风格问题,因此合并一个更改所需的补丁往返次数也会减少。

注意

关于注释和文档的约定不会被 rustfmt 检查。因此,这些方面仍然需要人工注意。

使用的是 rustfmt 的默认设置。这意味着遵循地道的 Rust 风格。例如,使用 4 个空格而不是制表符进行缩进。

我们可以方便地配置编辑器/IDE,使其在键入、保存或提交时进行格式化。然而,如果出于某种原因在某个时刻需要重新格式化整个内核的 Rust 源码,可以运行以下命令

make LLVM=1 rustfmt

也可以检查是否所有内容都已格式化(否则打印 diff),例如在 CI 中,可以使用

make LLVM=1 rustfmtcheck

类似于内核其余部分的 clang-formatrustfmt 作用于单独的文件,并且不需要内核配置。有时它甚至可以处理有语法错误的代码。

导入

默认情况下,rustfmt 格式化导入的方式容易在合并(merge)和变基(rebase)时产生冲突,因为在某些情况下它会将多个项压缩到同一行。例如

// Do not use this style.
use crate::{
    example1,
    example2::{example3, example4, example5},
    example6, example7,
    example8::example9,
};

相反,内核使用的是如下所示的垂直布局

use crate::{
    example1,
    example2::{
        example3,
        example4,
        example5, //
    },
    example6,
    example7,
    example8::example9, //
};

也就是说,每个项独占一行,并且只要列表中有一个以上的项,就会使用大括号。

末尾的空注释可以用来保持这种格式。不仅如此,当添加了空注释时,rustfmt 实际上会以垂直方式重新格式化导入。也就是说,通过对如下输入运行 rustfmt,可以轻松地将原始示例重新格式化为期望的风格

// Do not use this style.
use crate::{
    example1,
    example2::{example3, example4, example5, //
    },
    example6, example7,
    example8::example9, //
};

末尾的空注释既适用于如上所示的嵌套导入,也适用于单项导入 —— 这对于最小化补丁集(patch series)中的 diff 非常有用

use crate::{
    example1, //
};

末尾的空注释可以放在大括号内的任何一行,但最好将其保留在最后一个项中,因为这让人联想到其他格式化工具中的尾随逗号。有时,为了避免由于列表的更改而在补丁集中多次移动注释,这样做可能会更简单。

在某些情况下可能需要做出例外处理,即这些都不是硬性规定。还有一些代码尚未迁移到这种风格,但请不要引入其他风格的代码。

最终的目标是让 rustfmt 在稳定版本中自动支持这种格式化风格(或类似风格),而无需末尾的空注释。因此,在某个时候,我们的目标是移除这些注释。

注释

“普通”注释(即 //,而不是以 /////! 开头的代码文档)与文档注释一样,也使用 Markdown 编写,尽管它们不会被渲染。这提高了连贯性、简化了规则,并允许在两种注释之间更容易地移动内容。例如

// `object` is ready to be handled now.
f(object);

此外,就像文档一样,注释的句子首字母需要大写,并以句号结尾(即使它只是一个单句)。这包括 // SAFETY:// TODO: 以及其他带有“标签”的注释,例如

// FIXME: The error should be handled properly.

注释不应用作文档用途:注释旨在用于实现细节,而非面向用户。即使源文件的阅读器既是 API 的实现者又是用户,这种区分也是有用的。事实上,有时同时使用注释和文档会很有用。例如,用于 TODO 列表或对文档本身进行注释。对于后一种情况,可以将注释插入中间;也就是说,更靠近要注释的那行文档。对于任何其他情况,注释都写在文档之后,例如

/// Returns a new [`Foo`].
///
/// # Examples
///
// TODO: Find a better example.
/// ```
/// let foo = f(42);
/// ```
// FIXME: Use fallible approach.
pub fn f(x: i32) -> Foo {
    // ...
}

这既适用于公开项,也适用于私有项。这增加了与公开项的一致性,允许以更少的更改来修改可见性,并且使我们将来有可能也为私有项生成文档。换句话说,如果为私有项编写了文档,则仍应使用 ///。例如

/// My private function.
// TODO: ...
fn f() {}

一种特殊的注释是 // SAFETY: 注释。这些注释必须出现在每个 unsafe 块之前,并且它们解释了该块内的代码为什么是正确/健全的(sound),即为什么它在任何情况下都不会触发未定义行为,例如

// SAFETY: `p` is valid by the safety requirements.
unsafe { *p = 0; }

// SAFETY: 注释不应与代码文档中的 # Safety 章节相混淆。# Safety 章节规定了调用者(对于函数)或实现者(对于 trait)需要遵守的契约。// SAFETY: 注释展示了为什么某个调用(对于函数)或实现(对于 trait)确实遵守了 # Safety 章节或语言参考中所陈述的前提条件。

代码文档

Rust 内核代码的文档编写方式与 C 内核代码不同(即不通过 kernel-doc)。相反,它使用的是 Rust 代码通用的文档系统:rustdoc 工具,该工具使用 Markdown(一种轻量级标记语言)。

要学习 Markdown,市面上有许多现成的指南。例如,位于以下网址的指南

一个文档编写良好的 Rust 函数可能长这样

/// Returns the contained [`Some`] value, consuming the `self` value,
/// without checking that the value is not [`None`].
///
/// # Safety
///
/// Calling this method on [`None`] is *[undefined behavior]*.
///
/// [undefined behavior]: https://doc.rust-lang.net.cn/reference/behavior-considered-undefined.html
///
/// # Examples
///
/// ```
/// let x = Some("air");
/// assert_eq!(unsafe { x.unwrap_unchecked() }, "air");
/// ```
pub unsafe fn unwrap_unchecked(self) -> T {
    match self {
        Some(val) => val,

        // SAFETY: The safety contract must be upheld by the caller.
        None => unsafe { hint::unreachable_unchecked() },
    }
}

本示例展示了一些 rustdoc 的特性以及内核中遵循的一些约定

  • 第一段必须是一个简短描述所记录项作用的单句。进一步的解释必须放在额外的段落中。

  • 不安全函数必须在 # Safety 章节下记录其安全性前提条件。

  • 尽管此处未显示,但如果一个函数可能会 panic,则发生该情况的条件必须在 # Panics 章节中进行描述。

    请注意,panic 应当非常罕见,并且只有在有充分理由时才能使用。在几乎所有情况下,都应使用可出错的方法,通常是返回一个 Result

  • 如果提供使用示例对读者有帮助,则必须将它们写在名为 # Examples 的章节中。

  • Rust 项(函数、类型、常量等)必须适当地进行链接(rustdoc 会自动创建链接)。

  • 任何 unsafe 块前面都必须有一条 // SAFETY: 注释,描述内部代码为什么是健全的。

    尽管有时原因看起来很简单因而似乎不需要,但编写这些注释不仅是记录已考虑因素的好方法,更重要的是,它提供了一种途径,让人确信没有额外的隐式约束。

要了解更多关于如何为 Rust 编写文档以及额外特性的信息,请参阅 rustdoc 手册

此外,内核支持通过在链接目标前加上 srctree/ 来创建相对于源码树的链接。例如

//! C header: [`include/linux/printk.h`](srctree/include/linux/printk.h)

或者

/// [`struct mutex`]: srctree/include/linux/mutex.h

C FFI 类型

Rust 内核代码引用 C 类型(例如 int)时,使用的是诸如 c_int 的类型别名,这些别名可以从 kernel 预导入(prelude)中直接获取。请不要使用来自 core::ffi 的别名 —— 它们可能无法映射到正确的类型。

这些别名通常应直接通过其标识符引用,即作为单段路径。例如

fn f(p: *const c_char) -> c_int {
    // ...
}

命名

Rust 内核代码遵循常规的 Rust 命名规范

当把现有的 C 概念(例如宏、函数、对象等)包装到 Rust 抽象中时,为了避免混淆并提高在 C 和 Rust 之间来回切换时的可读性,应当使用尽可能接近 C 侧的名字。例如,来自 C 的宏(如 pr_info)在 Rust 侧具有相同的名称。

话虽如此,大小写应当进行调整以遵循 Rust 的命名规范,并且模块和类型引入的命名空间不应在项名称中重复。例如,当包装诸如以下的常量时

#define GPIO_LINE_DIRECTION_IN  0
#define GPIO_LINE_DIRECTION_OUT 1

Rust 中的等效代码可能看起来像这样(忽略文档)

pub mod gpio {
    pub enum LineDirection {
        In = bindings::GPIO_LINE_DIRECTION_IN as _,
        Out = bindings::GPIO_LINE_DIRECTION_OUT as _,
    }
}

也就是说,GPIO_LINE_DIRECTION_IN 的等效项应称为 gpio::LineDirection::In。特别是,不应将其命名为 gpio::gpio_line_direction::GPIO_LINE_DIRECTION_IN

检查(Lints)

在 Rust 中,可以在局部 allow(允许)特定的警告(诊断、lint),从而使编译器忽略给定函数、模块、块等内部的特定警告实例。

这类似于 C 中的 #pragma GCC diagnostic push + ignored + pop [1]

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-function"
static void f(void) {}
#pragma GCC diagnostic pop

但要简洁得多

#[allow(dead_code)]
fn f() {}

凭此优势,它使得默认启用更多诊断(即在 W= 级别之外)变得轻而易举。特别是那些可能会有一些误报,但在捕捉潜在错误方面却相当有用的诊断。

除此之外,Rust 还提供了 expect 属性,将这一点推得更远。如果未产生该警告,它会使编译器发出警告。例如,以下代码将确保当在某处调用 f() 时,我们必须移除该属性

#[expect(dead_code)]
fn f() {}

如果我们不这样做,就会收到来自编译器的警告

warning: this lint expectation is unfulfilled
 --> x.rs:3:10
  |
3 | #[expect(dead_code)]
  |          ^^^^^^^^^
  |
  = note: `#[warn(unfulfilled_lint_expectations)]` on by default

这意味着当不再需要 expect 时,它们不会被忘记。这种情况可能发生在多种情境下,例如

  • 在开发过程中添加的临时属性。

  • 编译器、Clippy 或自定义工具中 lint 的改进,这些改进可能会消除误报。

  • 当不再需要某个 lint 是因为预期它在某个时刻会被移除时,例如上面提到的 dead_code 示例。

它还提高了其余 allow 的可见性,并减少了错误应用的可能性。

因此,除非出现以下情况,否则应优先使用 expect 而不是 allow

  • 条件编译在某些情况下会触发警告,但在其他情况下不会。

    如果与总情况数相比,触发(或不触发)警告的情况只有少数几种,那么可以考虑使用条件 expect(即 cfg_attr(..., expect(...)))。否则,直接使用 allow 可能更简单。

  • 在宏内部,当不同的调用可能会生成在某些情况下触发警告而在其他情况下不触发的展开代码时。

  • 当代码可能会对某些架构触发警告而对其他架构不触发时,例如将 as 强制转换为 C FFI 类型。

作为一个更完整的示例,例如考虑这个程序

fn g() {}

fn main() {
    #[cfg(CONFIG_X)]
    g();
}

在这里,如果未设置 CONFIG_X,函数 g() 就是死代码。我们可以在这里使用 expect 吗?

#[expect(dead_code)]
fn g() {}

fn main() {
    #[cfg(CONFIG_X)]
    g();
}

如果设置了 CONFIG_X,这将发出一个 lint 警告,因为在该配置下它不是死代码。因此,在这样的情况下,我们不能直接使用 expect

一个简单的可能性是使用 allow

#[allow(dead_code)]
fn g() {}

fn main() {
    #[cfg(CONFIG_X)]
    g();
}

另一种选择是使用条件 expect

#[cfg_attr(not(CONFIG_X), expect(dead_code))]
fn g() {}

fn main() {
    #[cfg(CONFIG_X)]
    g();
}

这将确保,如果有人在某处引入了对 g() 的另一个调用(例如无条件调用),就会被发现它不再是死代码。然而,cfg_attr 比简单的 allow 更复杂。

因此,当涉及一个或两个以上的配置,或者当由于非局部更改(例如 dead_code)而可能触发 lint 时,使用条件 expect 可能并不划算。

有关 Rust 中诊断的更多信息,请参见

错误处理

有关 Rust for Linux 特定错误处理的背景和指南,请参见