以 Rust 編寫的螢幕驅動程式評量標準

數值類型

準則:請遵循 Rust 指引,預設為 i32,並在適當情況下使用明確大小的帶正負號和不帶正負號整數型別。

說明:大多數驅動程式庫撰寫者都具備 C / C++ 背景,因此經常使用無大小的帶正負號整數進行算術運算。

指南:限制條件成立時,請使用 std::num::NonZero<T> 類型。

說明:確保我們將零視為特殊情況處理。啟用選項表示最佳化

準則:如果零代表特殊情況,請使用 Option<std::num::NonZero<T>> 型別。

說明:確保我們將零視為特殊情況處理。促使我們在堆疊中較高的位置處理特殊情況,並將裸露的非零型別傳遞至較低的層級。

規範:針對將轉換為不可為空指標的邏輯記憶體位址使用 std::num::NonZero<usize>,針對可為空指標則使用 Option<std::num::NonZero<usize>>

說明:根據上述規範,這項結論相當明確。Option 型別會強制使用可為空值指標的程式碼,明確處理空值情況。使用建立的指標是不安全的 Rust,因此不建議在後續章節中使用。

準則:針對傳遞至 / 從 Zircon 取得的 CPU 實體記憶體位址,使用 zx_sys::zx_paddr_t

說明:清楚說明預期用途,符合 Zircon API 預期的類型。

準則:使用 u32 (或 std::num::NonZero<u32>,如果適用非零假設) / u64 (或 std::num::NonZero<u64>) 做為將寫入暫存器的 CPU 或裝置實體記憶體位址。

例如:

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::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 路徑的官方預設值:

  • 納入範圍:型別、衍生和類似屬性的巨集
  • 將父項模組帶入範圍:函式、類似函式的巨集

說明:符合「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>

說明:強制函式斷言系統呼叫會在成功時傳回非空值指標。

準則:當指標的目標可由 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 })
    }
}

記錄

準則:只有在 Fuchsia 的錯誤導致狀況時,才使用 ERROR 層級回報。請勿假設傳回錯誤時,ERROR 層級一律適用。

說明:修正對 RFC-0003 的常見誤解。

準則:假設驅動程式庫操作正確,如果硬體故障,請使用 WARNING 級別回報。

說明:遵循 RFC-0003。

準則:僅使用 INFO 層級提供目前調查的脈絡。在記錄陳述式前面加上註解,其中包含調查追蹤問題的連結。

說明:INFO 以上的驅動程式記錄會納入核心序列記錄,用於評估裝置健康狀態。

準則:請勿在高頻率的程式碼路徑中,記錄 INFO 以上的層級。 使用「檢查」查看經常變更的資料。

說明:與上述相同。

例如:

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 的依名稱連結功能支援的所有 ID。

說明:這是 Rust API 說明文件指南建議的做法。rust-analyzer 可以瀏覽連結。

經過整理的消歧字首清單,可建議 Rustdoc 連結的項目:值、常數、原始型別、模組、函式、型別、型別別名、結構體、欄位、方法、特徵、列舉、變體、聯集、巨集、衍生。

範例:

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

盡量減少巢狀層級

準則:呼叫可能出錯的函式後,請立即檢查錯誤,並視需要記錄錯誤,然後傳回。

說明:人工審查員偏好以步驟序列呈現的程序,而非巢狀條件區塊。

規範:將非簡單的錯誤處理作業擷取至專屬函式。 微不足道的錯誤處理方式是記錄並傳回。

說明:如果函式實作單一程序,人工審查員就能更輕鬆地進行分析。

欄位顯示設定

準則:複合型別的欄位必須全為私有或全為公開。 公開表示 pubpub(crate)

說明:公開欄位不適用於不變量。

同步處理

規範:使用下列類型的 fuchsia_sync 實作項目:CondvarMutexRwLock。請勿使用 std::syncparking_lot 中的變體。

說明:fuchsia_sync 實作會在 debug 建構作業中執行鎖死偵測。

其他指南

本文著重於檢閱 AI 代理程式產生的程式碼時,常見的問題。

顯示器驅動程式也遵循下列最佳做法。