编码规范¶
本文档描述如何在内核中编写 Rust 代码。
风格 & 格式化¶
代码应使用 rustfmt
进行格式化。 这样,偶尔向内核贡献代码的人就不需要学习和记住另一个风格指南。 更重要的是,审阅者和维护者不再需要花费时间指出样式问题,因此可能需要更少的补丁往返才能提交更改。
注意
注释和文档的约定不受 rustfmt
检查。 因此,仍然需要注意这些。
使用 rustfmt
的默认设置。 这意味着遵循惯用的 Rust 风格。 例如,缩进使用 4 个空格而不是制表符。
在键入、保存或提交时指示编辑器/IDE进行格式化是很方便的。 但是,如果由于某种原因需要在某个时候重新格式化整个内核 Rust 源代码,则可以运行以下命令
make LLVM=1 rustfmt
也可以检查是否所有内容都已格式化(否则打印差异),例如对于 CI,使用
make LLVM=1 rustfmtcheck
就像内核其余部分的 clang-format
一样,rustfmt
适用于单个文件,不需要内核配置。 有时,它甚至可以处理损坏的代码。
代码文档¶
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 的条件。请注意,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
命名¶
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
特定的警告(诊断、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
更复杂。
因此,当涉及多个配置或当 lint 可能由于非局部更改(如 dead_code
)触发时,使用条件 expect
可能不值得。
有关 Rust 中诊断的更多信息,请参阅
注释¶
“普通” 注释(即
//
,而不是以///
或//!
开头的代码文档)以 Markdown 编写,与文档注释的编写方式相同,即使它们不会被渲染。 这提高了 一致性,简化了规则,并允许在两种注释之间更轻松地移动内容。 例如此外,与文档一样,注释在句首大写,并以句点结尾(即使它是一个单句)。 这包括
// SAFETY:
、// TODO:
和其他“标记”注释,例如// FIXME: The error should be handled properly.
注释不应以文档为目的:注释旨在用于实现细节,而不是用户。 即使源文件的读者既是 API 的实现者又是用户,这种区分也是有用的。 实际上,有时同时使用注释和文档很有用。 例如,对于
TODO
列表或对文档本身进行注释。 对于后一种情况,可以将注释插入中间;也就是说,更靠近要注释的文档行。 对于任何其他情况,注释写在文档之后,例如一种特殊的注释是
// SAFETY:
注释。 这些必须出现在每个unsafe
块之前,并且它们解释了为什么块内的代码是正确的/健全的,即为什么它在任何情况下都不会触发未定义的行为,例如// SAFETY:
注释不要与代码文档中的# Safety
部分混淆。# Safety
部分指定了调用者(对于函数)或实现者(对于 trait)需要遵守的约定。// SAFETY:
注释显示了为什么调用(对于函数)或实现(对于 trait)实际上尊重# Safety
部分或语言参考中声明的前提条件。