编码规范¶
本文档描述了如何在内核中编写 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 应该非常罕见,并且仅在有充分理由时才使用。 在几乎所有情况下,都应使用易错方法,通常返回
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 特定错误处理的一些背景信息和指南,请参阅
注释¶
“普通”注释(即
//
,而不是以///
或//!
开头的代码文档)以 Markdown 编写,就像文档注释一样,即使它们不会被呈现。 这提高了代码一致性,简化了规则,并允许更轻松地在这两种注释之间移动内容。 例如此外,就像文档一样,注释在句首大写,并以句点结尾(即使它是一个句子)。 这包括
// SAFETY:
,// TODO:
和其他“标记”注释,例如// FIXME: The error should be handled properly.
注释不应用于文档目的:注释旨在用于实现细节,而不是用户。 即使源文件的读者既是 API 的实现者又是用户,这种区别也很有用。 事实上,有时同时使用注释和文档是有用的。 例如,对于
TODO
列表或评论文档本身。 对于后一种情况,可以将注释插入中间; 也就是说,更靠近要注释的文档行。 对于任何其他情况,注释都写在文档之后,例如这适用于公共和私有项目。 这增加了与公共项目的一致性,允许以更少的更改参与更改可见性,并且将允许我们潜在地为私有项目生成文档。 换句话说,如果为私有项目编写文档,则仍应使用
///
。 例如一种特殊的注释是
// SAFETY:
注释。 这些必须出现在每个unsafe
块之前,并且它们解释了为什么块内的代码是正确的/健全的,即为什么它在任何情况下都不会触发未定义的行为,例如// SAFETY:
注释不应与代码文档中的# Safety
部分混淆。# Safety
部分指定了调用者(对于函数)或实现者(对于特征)需要遵守的约定。// SAFETY:
注释显示了为什么调用(对于函数)或实现(对于特征)实际上尊重# Safety
部分或语言参考中陈述的先决条件。