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

数值类型

指南:遵循 Rust 指南,默认使用 i32,并在适当的时候使用明确指定大小的有符号和无符号整数类型。

说明:大多数驱动程序编写者都具有 C / C++ 背景,他们经常使用无大小的有符号整数进行算术运算。

指导原则:当约束成立时,使用 std::num::NonZero<T> 类型。

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

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

说明:确保我们将零作为特殊情况处理。促使我们在堆栈中更高级别的位置处理特殊情况,并将裸非零类型传递到较低级别。

准则:对于将转换为不可为 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 用法相匹配。

示例:

use zx;

struct Data {}

impl Data {
    pub async fn new() -> Result<Data, 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};
use zx;

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“newtype”,请使用 #[repr(transparent)]

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

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

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

指南:对于需要固定内存中表示形式的每种类型,都应有单元测试来检查每种类型的大小和对齐方式,以及每种类型成员的偏移量。

说明:从供应商文档翻译到 Rust 并非易事,我们使用测试来降低出错风险。

指南:指定自定义表示形式时,#[derive()]应包含以下特征:CopyClonezerocopy::FromByteszerocopy::Immutablezerocopy::IntoByteszerocopy::KnownLayout

说明

  • Copy 使指针操作易于推理
  • CloneCopy 所必需的
  • zerocopy::FromBytes 证明该类型可用于读取任何位模式
  • zerocopy::FromZeroszerocopy::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 路径的官方默认值:

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

说明:与《Rust 编程语言》一书中关于惯用使用路径的部分中的既定建议相符。

指南:将 zx crate 的顶级模块纳入范围。

说明zx crate 会导出 ChannelEvent 等通用类型名称,这些名称原本应带有 zx:: 前缀,例如 zx::Event 应读作“Zircon event”。这是有意偏离有关惯用使用路径的 Rust 编程图书部分,该部分建议将涉及名称冲突的两种类型的父模块纳入作用域。

指南:将父模块纳入注册或 ABI 定义类型的范围。在合理的情况下,将模块别名设为 abiregisters

说明:与上述规则的推理类似。我们之所以偏离 Rust 惯例,是因为寄存器和 ABI 类型名称很可能会与 Rust 驱动程序类型名称重叠,因为它们涵盖了相同的领域。我们沿用了 C++ 驱动程序中的这种做法,它在清晰度和简洁性之间实现了良好的平衡。

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

说明:与上文的推理相同。FIDL 和 Rust 驱动程序类型名称可能会重叠。

示例:

use fidl_next_fuchsia_sysmem2 as fidl_sysmem2;
use fidl_next;

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(mmio: MmioRegion<VmoMemory, Arc<VmoMemory>>) {
        // 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 = SubUnit1::new(subunit1_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;
use zx;

#[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 或更高级别记录日志。对于经常变化的数据,请使用 Inspect

说明:与上文的推理相同。

示例:

use zx;

/// Errors if the hardware returns an invalid version.
///
/// All error conditions are logged.
pub fn read_version() -> Result<u32, zx::Status> {
    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.
    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 运算符。这样可以生成更简洁的代码,并减少冗余的日志记录。

示例:

use zx;

/// 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 来命名用于统计集合中元素数量的变量。使用 len() 表示函数。

说明:与 Rust 惯例(例如 Vec::len())相匹配。

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

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

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

说明:与 Rust 惯例(例如 Vec::capacity)相匹配。

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

说明:根据 Rust API 文档编制指南推荐。 rust-analyzer 可以导航链接。

精选的消歧器前缀列表,用于建议 Rustdoc 可以链接到的内容:值、常量、原语、模块、函数、类型、类型别名、结构体、字段、方法、特征、枚举、变体、联合、宏、派生。

示例:

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

尽量减少嵌套级别

指南:调用可能出错的函数后,立即检查错误,可以选择性地记录错误,然后返回。

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

指南:将重要的错误处理提取到专用函数中。 简单错误处理是记录并返回。

说明:实现单个流程的函数更易于人工审核员分析。

实地可见性

指南:复合类型中的字段必须全部为私有字段或全部为公共字段。公开表示 pubpub(crate)

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

同步

指南:使用以下类型的 fuchsia_sync 实现:CondvarMutexRwLock。请勿使用 std::syncparking_lot 中的变体。

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

其他指南

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

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