英语

编码规范

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

样式 & 格式

代码应使用 rustfmt 格式化。 这样,偶尔为内核做贡献的人就不需要学习和记住另一个风格指南。 更重要的是,审阅者和维护者不再需要花费时间指出样式问题,因此可能需要更少的补丁往返才能提交更改。

注意

关于注释和文档的约定不受 rustfmt 的检查。 因此,仍然需要注意这些。

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

指示编辑器/IDE 在输入、保存或提交时进行格式化很方便。 但是,如果由于某种原因需要在某个时候重新格式化整个内核 Rust 源代码,则可以运行以下命令

make LLVM=1 rustfmt

也可以检查所有内容是否已格式化(否则打印差异),例如对于 CI,可以使用

make LLVM=1 rustfmtcheck

就像内核其余部分的 clang-format 一样,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 块之前,并且它们解释了为什么块内的代码是正确的/健全的,即为什么它在任何情况下都不会触发未定义的行为,例如

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

// SAFETY: 注释不应与代码文档中的 # Safety 部分混淆。 # Safety 部分指定了调用者(对于函数)或实现者(对于特征)需要遵守的约定。 // SAFETY: 注释显示了为什么调用(对于函数)或实现(对于特征)实际上尊重 # 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 端的名字,以避免混淆,并在 C 和 Rust 端来回切换时提高可读性。 例如,来自 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 特定警告(诊断、lints),使编译器忽略给定函数、模块、块等中给定的警告实例。

它类似于 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,因此当不再需要 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() 是 dead code。 我们可以在这里使用 expect 吗?

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

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

如果设置了 CONFIG_X,这将发出一个 lint,因为它在该配置中不是 dead code。 因此,在这种情况下,我们不能按原样使用 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() 的另一个调用(例如,无条件),那么就会发现它不再是 dead code。 但是,cfg_attr 比简单的 allow 更复杂。

因此,当涉及多个或两个以上的配置,或者由于非本地更改(例如 dead_code)可能触发 lint 时,可能不值得使用条件 expect

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

错误处理

有关 Rust for Linux 特定错误处理的一些背景信息和指南,请参阅