以 Rust 编写的显示驱动程序的评分标准

数值类型

准则:遵循 Rust 指南,默认使用 i32,并在适当情况下使用具有明确大小的有符号和无符号整数类型。

说明:大多数驱动程序编写者都具有 C / C++ 背景,其中无大小的有符号整数通常用于算术。

准则:当约束条件成立时,请使用 std::num::NonZero<T> 类型。

说明:确保我们将零作为特殊情况处理。启用选项表示法优化。

准则:当零表示特殊情况时,请使用 Option<std::num::NonZero<T>> 类型。

说明:确保我们将 0 作为特殊情况处理。提示我们在堆栈中更高级别处理特殊情况,并将裸非零类型传递给较低级别。

准则:对于将转换为不可为 null 的指针的逻辑内存地址,请使用 std::num::NonZero<usize>;对于可为 null 的指针,请使用 Option<std::num::NonZero<usize>>。

说明:根据上述准则,可以得出明确的结论。Option 类型会强制使用可为 null 的指针的代码明确处理 null 情况。使用创建的指针是不安全的 Rust,在后面的部分中不建议这样做。

准则:对于传递给 Zircon 或从 Zircon 获取的 CPU 物理内存地址,请使用 zx_sys::zx_paddr_t。

说明:清楚地传达预期用途,与 Zircon API 预期的类型匹配。

准则:对于将写入寄存器的 CPU 或设备物理内存地址,请使用 u32(如果适用非零假设,则使用 std::num::NonZero<u32>)/u64(或 std::num::NonZero<u64>)。

示例:

use std::num::NonZero;
use zx_sys::zx_paddr_t;

// Useful displays have at least one pixel.
let display_width: NonZero<u16>;

// Our FIDL APIs use 0 as an invalid ID.
let imported_image_id: Option<NonZero<u64>>;

// To be obtained from a memory pinning API.
let mut image_physical_address: zx_paddr_t;

// Will be written into a register.
let image_physical_address: NonZero<u32>;

// [`None`] when the plane is disabled.
let image_physical_address_reg_value: Option<NonZero<u32>>;

创建实例

准则:默认将工厂函数命名为 new()。此默认值适用于易出错函数和异步函数。

说明:与当前主流的 Rust 用法一致。

示例:

struct Data {}

impl Data {
    pub async fn new() -> Result<Self, zx::Status> { /* ... */ }
}

准则:如果类型还公开了可靠的工厂函数,请为不可靠的工厂函数使用 try_new() 名称。

说明:与当前主流的 Rust 用法一致。(不过,这种情况很少见。)

准则:实现 From<OtherType> 或 TryFrom<OtherType>,以便从其他类型进行可靠 / 不可靠的转换。请勿提供可有效执行类型转换的构造函数。

说明:有助于区分转换和更复杂的实例创建。

FIDL 绑定

准则:仅使用 fidl_next 绑定。

说明:驱动程序传输需要 fidl_next 绑定。标准化这些绑定可避免(人类和 AI)开发者在两种绑定之间切换上下文。

未使用的代码

准则:将 #[expect(dead_code)] 与说明性注释搭配使用。请勿使用 #[allow(dead_code)]。

说明:#[expect] 版本由编译器强制执行,因此不会过时。

准则:在可以使用 #[expect(dead_code)] 的情况下,请勿使用下划线 (_) 变量名称前缀。

说明:下划线前缀等效于 #[allow(dead_code)],因此上述推理适用。

准则:在适用的情况下使用下划线表达式。如果舍弃值(例如 Result)的原因不明显,请解释原因。

说明:下划线表达式与变量名称中的下划线前缀是不同的语言结构。

示例:

use fdf_component::{Driver, Node};

struct DisplayDriver {
    // We must keep the Node alive for the lifetime of the driver.
    #[expect(dead_code)]
    device_node: Node,
}

impl Driver for DisplayDriver {
    async fn stop(&self) {
        // Intentionally ignoring failure during device shutdown. There's
        // nothing we can do at this point.
        let _ = fallible_function_that_logs();
    }
}

fn fallible_function_that_logs() -> Result<(), zx::Status> { /* ... */ }

表示法

准则:将 #[repr(...)] 属性放在所有其他属性之上。

说明:尽早定义表示形式,可以提高对结构进行操作的宏使用正确数据布局的可能性。

准则:除非类型的内存中表示法必须固定,否则请坚持使用默认表示法 (rust)。当且仅当值直接从驱动程序和不同软件或硬件之间共享的内存中加载或存储到其中时,内存中表示法才必须是固定的。请遵循以下准则来选择非默认表示法。

说明:默认表示法可最大限度地提高编译器的优化机会。当可以保证内存由同一编译二进制文件的多个实例使用时,我们可以在共享内存中使用 rust 表示法。当有多个软件(不同的二进制文件)或硬件(设备)使用内存中的值时,我们必须使用固定的内存表示法。

准则:对于需要固定内存中表示法的 Rust“新类型”,请使用 #[repr(transparent)]。

说明: #[repr(transparent)] 会对“newtype” intent 进行编码。编译器会强制要求结构体封装单个非零大小的类型字段。

准则:对于需要固定内存中表示的多字段复合类型,请使用 #[repr(C)]。

说明: #[repr(C)] 对生成具有确定性字段偏移量和对齐方式的复合类型的意图进行编码。

准则:对于需要固定内存表示的每种类型,请进行单元测试,以检查每种类型的大小和对齐方式,以及每个字段的偏移量。

说明:将供应商文档转换为 Rust 并非易事,我们使用测试来降低出错风险。

准则:指定自定义表示法时,#[derive()]以下特征:Copy、Clone、zerocopy::FromBytes、zerocopy::Immutable、zerocopy::IntoBytes、zerocopy::KnownLayout。

说明:

  • Copy 可让您轻松推断指针操作
  • Copy 需要 Clone
  • zerocopy::FromBytes 证明该类型可用于读取任何位模式
  • zerocopy::FromZeros 由 zerocopy::FromBytes 隐式指定
  • zerocopy::Immutable 证明该类型不使用内部可变性
  • zerocopy::IntoBytes 证明该类型可以被视为字节序列
  • 其他 zerocopy 个派生特征需要 zerocopy::KnownLayout

示例:

use bitfield::bitfield;
use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};

bitfield! {
    #[repr(transparent)]
    #[derive(Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
    struct CommandFlags(u32);

    /* ... */
}

#[repr(C)]
#[derive(Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
struct Command {
    pub flags: CommandFlags,
    pub id: u32,
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::mem::{align_of, offset_of, size_of};

    #[fuchsia::test]
    fn test_command_abi() {
        assert_eq!(size_of::<Command>(), 8);
        assert_eq!(align_of::<Command>(), 4);
        assert_eq!(offset_of!(Command, flags), 0);
        assert_eq!(offset_of!(Command, id), 4);
    }
}

use 条路径

准则:遵循官方默认设置,使用符合 use 语言习惯的路径:

  • 引入作用域:类型、派生和类似属性的宏
  • 将父级模块引入作用域:函数、类似函数的宏

说明:与 The Rust Programming Book 中关于惯用路径的部分中确立的建议一致。

准则:通过 zx:: 路径引用 zx 项。请勿将 zx::Status 等个别项纳入范围。

说明: zx crate 会导出通用类型名称,例如 Channel 和 Event,这些名称应与 zx:: 前缀一起读取,例如 zx::Event 读作“Zircon 事件”。这有意偏离了 Rust 编程手册中关于惯用使用路径的部分,该部分建议将名称冲突中涉及的两种类型的父级模块都纳入作用域。

准则:将寄存器或 ABI 定义类型的父级模块纳入范围。在适当情况下,将模块别名设置为 abi 或 registers。

说明:原因与上述规则类似。我们不遵循 Rust 惯例,因为寄存器和 ABI 类型名称可能会与 Rust 驱动程序类型名称重叠,因为它们涵盖相同的网域。我们重复使用 C++ 驱动程序中的做法,在清晰度和简洁性之间实现了良好的平衡。

准则:为每个 fidl_next 绑定模块设置别名。为所有别名使用 fidl_ 前缀。使用别名来限定对结构体和函数的访问。

说明:原因同上。FIDL 和 Rust 驱动程序类型名称可能会重叠。

示例:

use fidl_next_fuchsia_sysmem2 as fidl_sysmem2;

pub async fn use_buffer_collection(
    sysmem_buffer_collection: &mut fidl_next::Client<fidl_sysmem2::BufferCollection>,
) {
    /* ... */
    log::warn!("Failed to get hardware pixel formats, falling back to safe set");
    /* ... */
}

MMIO 区域管理

准则:对所有 MMIO 内存区域使用 MmioRegion<VmoMemory, Arc<VmoMemory>> 类型。

说明: std::sync::Arc 满足 MmioSplit 特征约束条件,允许任何模块进一步细分其接收的 MMIO 区域。Arc 与任何线程模型兼容(与 Rc 相反)。原子开销可以忽略不计,因为引用计数仅在驱动程序启动和停止期间发生变化。

准则:使用 split_off。请勿使用 try_split_off。

说明:硬件会呈现众所周知的静态 MMIO 映射。用于构建地图的驱动程序代码不应需要任何条件逻辑。

示例:

use fidl_next_fuchsia_hardware_platform_device as fidl_platform_device;
use mmio::MmioSplit;
use mmio::region::MmioRegion;
use mmio::vmo::VmoMemory;
use std::sync::Arc;

/// Obtains an MMIO region from the Platform Device.
///
/// Logs on failure.
async fn map_mmio_range(
    platform_device: &fidl_next::Client<fidl_platform_device::Device>,
    range_name: &str,
) -> Result<MmioRegion<VmoMemory, Arc<VmoMemory>>, zx::Status> {
    let region = platform_device.map_mmio_by_name(range_name).await.map_err(|err| {
        log::error!("Failed to map MMIO range {range_name}: {err:?}");
        err.log_to_status()
    })?;
    Ok(region.into_split_send())
}

impl FunctionalUnit {
    pub fn new(mut mmio: MmioRegion<VmoMemory, Arc<VmoMemory>>) -> Self {
        // Subunit 1 manages the MMIO range 0x0000..0x1000.
        let subunit1_mmio = mmio.split_off(0x1000);
        let subunit1 = SubUnit1::new(subunit1_mmio);

        // Subunit 2 manages the MMIO range 0x1000..0x2000.
        let subunit2_mmio = mmio.split_off(0x1000);
        let subunit2 = SubUnit2::new(subunit2_mmio);

        // Subunit 3 manages the MMIO range from 0x2000 onwards.
        let subunit3 = SubUnit3::new(mmio);

        Self { subunit1, subunit2, subunit3 }
    }
}

指针和共享内存

准则:使用指针访问与硬件共享的内存。

说明:Rust 内存模型的假设会在引用创建时触发,不需要访问引用。

准则:立即将系统调用返回的 usize 转换为 std::num::NonZero<usize>。

说明:强制执行函数以断言系统调用在成功时返回非 null 指针。

准则:当指针的目标可以用 Rust 类型表示时,请立即将 std::num::NonZero<usize> 转换为指向该类型的 std::ptr::NonNull。

说明:尽量减少出错的可能性。

准则:如果需要进行指针运算,请立即将 std::num::NonZero<usize> 转换为 std::ptr::NonNull,然后使用 add() / byte_add() 和 cast() 等方法。

说明:减少出错的可能性。指针来源会保留。

示例:

use std::num::NonZero;
use std::ptr::NonNull;

#[repr(...)]
#[derive(...)]
struct Header { /* ... */ }

#[repr(...)]
#[derive(...)]
struct Trailer { /* ... */ }

struct SharedMemory {
    header_ptr: NonNull<Header>,
    trailer_ptr: NonNull<Trailer>,
}

impl SharedMemory {
    pub fn new() -> Result<Self, zx::Status> {
        let data_address = fuchsia_runtime::vmar_root_self().map(...)?;
        let data_address = NonZero::<usize>::new(data_address)
            .expect("zx::vmar::map() returned null address");

        // [`Option::unwrap()`] is guaranteed not to panic. The [`NonZero::new()`]
        // call above already checked that the pointer is non-null.
        let header_ptr = NonNull::new(
            std::ptr::with_exposed_provenance_mut(data_address.get())
        ).unwrap();

        // SAFETY: The memory allocation covers both [`Header`] and [`Trailer`].
        let trailer_ptr = unsafe { header_ptr.add(1) }.cast::<Trailer>();

        Ok(Self { header_ptr, trailer_ptr })
    }
}

日志记录

准则:仅使用 ERROR 级别报告由 Fuchsia 中的 bug 引起的情况。不要假设在返回错误时 ERROR 级别始终合适。

说明:纠正了对 RFC-0003 的普遍误解。

准则:使用 WARNING 级别报告在驱动程序正常运行的情况下可能发生的硬件故障。

说明:遵循 RFC-0003。

准则:仅使用 INFO 级别为当前调查提供上下文。在日志语句前面加上一条注释,其中包含指向调查跟踪问题的链接。

说明:内核串行日志中包含 INFO 或更高级别的驱动程序日志,用于评估设备的整体健康状况。

准则:请勿在高频代码路径上记录 INFO 或更高级别的日志。对经常更改的数据使用检查。

说明:原因同上。

示例:

/// Errors if the hardware returns an invalid version.
///
/// All error conditions are logged.
pub fn read_version() -> Result<u32, zx::Status> {
    log::debug!("read_version()");

    let version_value: u32 = read_from_register();

    // TODO(https://fxbug.dev/12345678): We suspect that the crashes are
    // correlated with specific hardware versions. Reduce the log level to DEBUG
    // after proving or disproving the hypothesis.
    log::info!("Component version: {version_value}");

    if version_value == 0 {
        log::warn!("Invalid version, device may be off: {version_value}");
        // ...
    }
    // ...
}

使用 try 运算符

准则:优先使用 try 运算符 (?),而不是所有其他错误处理替代方案。在设计函数接口时,请优先考虑让调用方能够使用 try 运算符。

说明:简洁的错误处理可让读者专注于更宏观的层面。try 运算符与 C++ 显示驱动程序中的错误处理相匹配,其中 zx::result<> 错误会在堆栈中向上冒泡。

准则:设计返回值类型中使用的错误,以便于使用 ?。具体而言,最好在 Result 中使用 zx::Status 作为错误类型。

说明:目前,请遵循与 C++ 显示驱动程序相同的错误处理方法。

准则:清楚地记录函数何时记录产生错误结果的条件。

说明:如果开发者确信条件已记录,则可以使用 try 运算符。这样可以生成更简洁的代码,并减少冗余日志记录。

示例:

/// Errors if the hardware returns an invalid version.
///
/// All error conditions are logged.
pub fn read_version() -> Result<u32, zx::Status> { /* ... */ }

/// Initializes the hardware so it can receive commands.
///
/// All error conditions are logged.
pub fn initialize_hardware() -> Result<(), zx::Status> {
    let version_value = read_version()?;

    /* ... */
}

命名:length、size 和 capacity

准则:使用 length 为统计集合中元素数量的变量命名。对函数使用 len()。

说明:符合 Rust 常见做法,例如 Vec::len()。

准则:使用 size_bytes 命名用于统计存储或传输某项内容所用字节数的变量和函数。请勿创建与 core::mem::size_of 调用冗余的函数。

说明:在 Rust 中,习惯上使用“Size”来命名此概念。 不过,“size”一词在硬件相关文档中经常出现。“_bytes”后缀有助于消除歧义。

准则:使用 capacity 命名变量和函数,这些变量和函数用于报告集合后备内存支持的最大元素数量。具体而言,后备存储空间永远不会发生变化的集合具有固定容量,其长度会随着元素的插入和移除而发生变化。

说明:符合 Rust 常见做法,例如 Vec::capacity。

评论格式

准则:手动换行注释和文档注释,以保持在 100 个字符的行限制内。

说明:Fuchsia 的 rustfmt 配置未启用不稳定的 wrap_comments 选项。fx format-code 不会封装注释。

准则:即使以下内容超出限制,也不要换行:

  • @ref(...)、@cite(...) 和 @alias(...) 记录。工具会将每条记录解析为一行。请参阅引用指南。
  • 网址和文档内链接。换行会破坏这些链接。
  • 无法拆分的令牌,例如长标识符或十六进制载荷。

说明:对这些内容进行换行会改变其含义或导致工具无法正常运行。

准则:链接 Rustdoc 的按名称链接功能支持的所有标识符。

说明:Rust API 文档指南推荐的做法。rust-analyzer 可以浏览这些链接。

精选的消歧前缀列表,用于建议 Rustdoc 可以链接到哪些内容:value、constant、primitive、module、function、type、typealias、struct、field、method、trait、enum、variant、union、macro、derive。

示例:

/// See [`DeviceBuilder`] for obtaining instances.
pub struct Device {}

尽量减少嵌套层级

准则:调用易出错的函数后,立即检查错误,选择性地记录错误,然后返回。

说明:人工审核者更喜欢阅读以一系列步骤(而不是嵌套条件块)形式呈现的流程。

准则:将复杂的错误处理逻辑提取到专用函数中。简单的错误处理是记录并返回。

说明:人工审核员可以更轻松地分析实现单个流程的函数。

公开范围和关联的内容

准则:在叶 crate(其他 crate 未使用的 crate)中将顶级项设为私有

说明:叶级 crate 中的公共顶级模块会绕过死代码分析。叶 crate 包括驱动程序、集成测试和工具。

准则:请勿在叶条板箱中使用 pub(crate)。请改用 pub。

说明:对于这些 crate,模块是唯一有意义的 API 屏障。

准则:复合类型的字段必须全部为私有,或全部为公开。“公开”是指 pub 或 pub(crate)。

说明:公共字段不适用于不变量。

准则:避免在固有 impl 块中定义签名不包含 Self 的关联函数。您可以尝试将函数移至其签名中某个类型的固有 impl 块,将函数与新类型相关联,或使用独立函数。

说明:AI 智能体倾向于在已读取的文件中定义函数,而不是查找最相关的类型,此提示可纠正这一倾向。

示例:

use std::num::NonZero;

/// Packages the information in a FIDL request.
struct MessageRectangle {
    pub width: NonZero<u32>,
    pub height: NonZero<u32>,
}

impl MessageRectangle {
    /// True iff the instance meets the [`HorizontalRectangle`] invariants.
    pub fn is_horizontal(&self) -> bool { self.width >= self.height }
}

/// The width is guaranteed to be at least as large as the height.
struct HorizontalRectangle {
    width: NonZero<u32>,
    height: NonZero<u32>,
}

impl HorizontalRectangle {
    /// Factory function.
    pub fn square(width: NonZero<u32>) -> Self {
        Self { width, height: width }
    }

    pub fn aspect_ratio(&self) -> f64 { /* ... */ }
}

避免基本类型偏执

准则:表示双状态字段或参数时,请从以下选项中选择可读性最高的选项:

  • 谓词名称,布尔值;示例:enabled: bool
  • noun name, matching enum variants; example: trigger: Trigger, enum Trigger { Level, Edge }

说明:新开发者(AI 和人类)往往会过度使用布尔值。 布尔值有时比枚举更好,因此一律禁止使用布尔值并不合适。

准则:避免使用具有多个相同类型的非 self 形参的函数定义。请改为将布尔值替换为枚举、引入新类型,或将所有参数封装在 struct 中。重构不遵循此规则的函数。

说明:如果存在多个同类型参数,则在调用点很难发现顺序切换错误。典型示例是交换 width 和 height 参数。

准则:避免在函数调用点使用 /*argument_name=*/ 注释。请按照上述指南中的解决方案消除混淆。

说明:参数名称注释会掩盖选择不当的函数签名。我们的 C++ 工具通过检查注释是否与函数声明中的实参名称匹配来支持此模式。Rust 工具不支持此功能,因此注释可能会过时。

准则:请勿对具有多个字段的元组或元组结构体使用元组索引表达式(.0、.1)。定义具有命名字段的结构体,或解构您无法控制其类型的值。

说明:人工审核员很难跟踪 .1 等代码所指的内容。

示例:

use std::num::NonZero;

enum Polarity {
    ActiveHigh,
    ActiveLow,
}

/// Argument for [`set_device_timing`].
struct DeviceTiming {
    pub horizontal_sync_polarity: Polarity,
    pub vertical_sync_polarity: Polarity,
    pub horizontal_active: NonZero<u32>,
    pub vertical_active: NonZero<u32>,
}

fn set_device_timing(device_timing: DeviceTiming) { /* ... */ }

pub fn initialize_device() {
    set_device_timing(DeviceTiming {
        horizontal_sync_polarity: Polarity::ActiveHigh,
        vertical_sync_polarity: Polarity::ActiveHigh,
        horizontal_active: const { NonZero::new(640).unwrap() },
        vertical_active: const { NonZero::new(480).unwrap() },
    });
}

pub struct Point {
    pub x: u32,
    pub y: u32,
}

// Returning a struct to avoid tuple indexing expressions.
pub fn cursor_position() -> Point { /* ... */ }

pub fn connect() {
    // `zx::Channel::create()` returns `(zx::Channel, zx::Channel)`.
    let (driver_channel, device_channel) = zx::Channel::create();
    /* ... */
}

同步

准则:使用以下类型的 fuchsia_sync 实现:Condvar、Mutex、RwLock。请勿使用 std::sync 或 parking_lot 中的变体。

说明:fuchsia_sync 实现会在 debug build 中执行死锁检测。

散文写作风格

准则:一句话只表达一个观点。

说明:AI 智能体倾向于进行最少的局部修改,这会导致句子冗长。

准则:三个或更多并列项应放入列表或表格中。仅当商品具有值得对齐的属性时,才使用表格。

说明:针对连写句的主要原因提供具体修正。

准则:理由应单独成句或成段。遵循渐进式披露原则:先规则,后理由,再证据。

说明:针对连写句的主要原因提供具体修正。

准则:仅提供支持当前理由说明的历史信息。避免使用叙述性元素:不必要的突发事件详情、日期、方案迭代 ID。删除已被取代的决策和不再相关的事实。

说明:AI 智能体倾向于在评论和文档中构建历史信息的迷你归档,而不是依赖版本记录进行归档。

准则:在最相关的位置陈述每个事实,且仅陈述一次。使用引用,而不是复制。

说明:同上。

准则:在保持清晰的前提下,力求简洁。使用现在时和精确的技术语言。避免主观细节 - 大多数形容词都是主观的。

说明:调整 AI 智能体的语气,以平衡词元有效性和实用性。

其他指南

本文档重点介绍在审核 AI 智能体生成的代码时经常遇到的问题。

显示驱动程序也遵循以下最佳实践。