RFC-0083:FIDL 版本管理

RFC-0083:FIDL 版本管理
狀態已接受
區域
  • FIDL
說明

提供一種方法,可為 FIDL 元素建立版本,並在特定版本產生繫結。

問題
Gerrit 變更
作者
審查人員
提交日期 (年-月-日)2021-02-12
審查日期 (年-月-日)2021-04-05

摘要

本文件提出了一種方法,可在 FIDL 元素中加上版本註解,以及一種機制,可在特定版本中產生繫結。這可將 API 的演進與採用作業分開,讓程式庫作者更容易進行變更,同時為最終開發人員提供穩定性。這為 RFC-0002:平台版本控制中 FIDL 的角色奠定基礎。

提振精神

雖然 FIDL 在變更期間提供許多 ABI 相容性的便利功能,但實際上要改進 API 並不容易。在 Fuchsia SDK 中,如果要變更 ABI 相容但不相容於 API 的 FIDL,就必須進行精心協調的軟性轉換,以免中斷下游編譯作業。當某些東西發生故障時,我們通常必須在 Fuchsia 中還原變更。隨著 SDK 程式庫的使用量增加,進行這些變更的難度也會隨之增加。

FIDL 版本管理功能可解決這個問題,讓 FIDL 程式庫作者和消費者可以按照自己的步調前進。程式庫作者新增、移除或修改 API 時,變更會在新的 API 級別中發布。針對舊版 API 級別指定目標的應用程式,在採用新版 API 級別之前,不會看到繫結的變更。除了提供穩定性之外,這項功能還可讓最終開發人員一次遷移一個元件的 API,因為每個元件都會指定目標 API 級別。

圖 1 說明破壞性的 API 變更。如果沒有版本控制,應用程式就會中斷,並導致還原。有了版本控制功能,應用程式就會繼續固定在舊版 API 級別。當然,如果嘗試將應用程式的固定 API 級別從 12 提升至 13,也會發生相同的問題。不過,這些問題可以非同步方式修正,無須還原 Fuchsia 中的原始變更,也不會導致專案停止運作。

圖表上方有文字說明的 API 演進圖

圖 1:在 FIDL 版本控制前 (左圖) 和後 (右圖) 的 API 演進歷程

術語

「API 級別」和「ABI 修訂版本」這兩個詞彙已在 RFC-0002:平台版本設定中定義,並進行以下變更:

RFC-0002 修訂案。Fuchsia API 級別是未簽署的 63 位元整數。換句話說,它是 64 位元,但高位元必須為零。

FIDL 元素是 FIDL 程式庫的個別部分,會影響產生的繫結。蒐集的資料包括:

  • FIDL 程式庫本身
  • 常數、列舉、位元、結構體、表格、聯集、通訊協定和服務宣告
  • 別名和新類型 (來自 RFC-0052:類型別名和新類型)
  • 列舉、位元、結構體、表格、聯集和服務的成員 (包括表格和聯集的 reserved 成員)
  • 通訊協定中的 compose 節和方法
  • 方法中的請求和回應參數

FIDL 屬性是 FIDL 元素的可修改層面,但本身並非獨立元素。蒐集的資料包括:

  • 屬性
  • 修飾符 strictflexibleresource
  • 常數、列舉成員和位元成員的值
  • 結構體成員的預設值
  • 類型限制 (針對成員、參數和類型別名)
  • 方法類型 (單向、雙向、事件)
  • 方法錯誤語法 (是否存在和類型)

因此,只有少數幾個項目既不是 FIDL 元素,也不是屬性:

  • 個別 .fidl 檔案
  • 匯入其他 FIDL 程式庫
  • 僅限 FIDL 的 using 別名,RFC-0052 已將其從語言中移除
  • 實驗性 resource_definition 宣告
  • 註解,包括文件註解

設計

本文件所述的設計提供通用工具,可為 FIDL 元素建立版本。其主要用途是根據 API 級別為 Fuchsia 平台中的 FIDL 程式庫命名。

範圍

這項設計會將版本管理做為 FIDL 語言中的概念,為 FIDL 程式庫提供時間維度。它會指定版本屬性的語法和語意,包括與 FIDL 的其他層面互動的方式,例如父項-子項和使用-定義關係。它會建立產生 FIDL 繫結時,如何提供版本做為輸入內容。

這項設計不會為 FIDL 提供套件管理工具。版本解析演算法、套件發布和依附元件衝突等主題則不在範圍內。不過,解決這些問題的系統應可重複使用此設計提供的工具。

此提案並未處理執行階段行為:而是著重於 API,而非 ABI。因此,通訊協定演進這個主題超出範圍。包括「如何讓 FIDL 伺服器支援多個 ABI 修訂版本?」等問題。FIDL 和元件管理服務日後可採用多種通訊協定演進策略。這項提案在 FIDL 中引入版本概念,為通訊協定演進鋪路,但不會進一步說明。

在這種設計下,能否呈現轉場效果,與轉場效果是否安全或相容無關。相反地,版本化的 FIDL 程式庫幾乎可以代表任何語法有效變更序列。

形式主義

平台 ID 是用於為版本提供背景資訊的標籤。平台 ID 必須是有效的 FIDL 程式庫名稱元素,也就是說,截至本文撰寫時,必須符合正規表示式 [a-z][a-z0-9_]*

版本 ID 是介於 1 和 2^63-1 (含首尾) 之間或等於 2^64-1 的無號 64 位元整數。後者版本 ID 稱為 HEAD,並會特別處理。

修訂 (2022 年 10 月)。為了支援舊版方法,我們改為為 HEAD 使用 2^64-2,為 LEGACY 使用 2^64-1。

修訂 (2024 年 4 月)。HEADLEGACY 已在 RFC-0246 中重新定義。

版本 ID 的排序完全依據「較新」關係。當 X > Y 時,版本 X 就會比版本 Y 新。

以平台而言,FIDL 元素的可用性是指元素推出的版本,以及淘汰移除的版本。停用和移除日期必須晚於推出日期。1 如果同時提供這兩個日期,則移除日期必須晚於停用日期。

如果 FIDL 元素與平台相容,則會在該平台下進行版本控制。如果在任何平台下進行版本管理,則會建立版本

如果平台版本與元素的推出時間相同或更晚,但早於元素淘汰和移除時間 (如有),則該元素與該 FIDL 版本相容。如果版本大於或等於元素淘汰日期,但小於移除日期 (如有),則該版本為已淘汰。如果可用或已淘汰,則為「存在」。否則為「缺少」

版本選項是指將版本指派給一組平台。舉例來說,您可以選取 red 的 2 版和 blueHEAD 版。

如果 FIDL 元素可在所有平台上使用,則該元素對於版本選項而言是可用的。如果此屬性適用於所有平台,則為已淘汰;如果適用於一或多個平台,則為已淘汰。否則為「缺少」

語法

可用性屬性的格式如下,2 參考自 Swift 的 available 屬性

@available(added=<V>, deprecated=<V>, removed=<V>)

每個 <V> 都是版本 ID。addeddeprecatedremoved 欄位分別代表元素的推出、淘汰和移除。這些都是選用項目,但至少必須提供一個。

在程式庫中,必須提供 added 欄位 (deprecatedremoved 為選用項目)。另外還有選用欄位 platform,可用來指定平台 ID。程式庫中的所有版本 ID 都會參照這個平台的版本。例如:

@available(platform="red", added=2)
library colors.red.auth;

如果省略,系統會預設為程式庫名稱的第一個元件:

@available(added=HEAD)  // implies platform="blue"
library blue.auth;

使用 deprecated 欄位時,您可以提供額外的 note 欄位,用於納入警告訊息。例如:

@available(added=12, deprecated=34, note="Use X instead")

供應情形屬性會使 RFC-0058:引入 [Deprecated] 屬性 中的 [Deprecated] 屬性淘汰。

版本管理元素

FIDL 元素會使用可用性屬性進行版本控制。每個 FIDL 元素最多只能有一個可用性屬性,且只能在版本化程式庫中執行此操作。換句話說,如果程式庫中的任何 FIDL 元素加上註解,則程式庫也必須加上註解。

FIDL 程式庫中的每個檔案都有專屬的程式庫宣告,但都代表相同的 FIDL 元素:程式庫。這與 FIDL 樣式指南一致:

將程式庫分割成檔案,不會對程式庫的使用者造成任何技術影響。... 將程式庫分割成多個檔案,以便讀取。

因此,程式庫中只有一個程式庫宣告可以具有可用性屬性。文件註解受到相同的限制,因此建議您選擇相同的檔案,指定程式庫的可用性和其文件註解。

版本管理屬性

您無法直接為 FIDL 屬性建立版本。如要變更屬性,您必須交換屬性所屬的元素。也就是複製元素、移除舊副本,並在同一個版本中引入新副本。例如,如要變更版本 12 繫結的字串,請執行下列操作:

@available(removed=12)
string:50 info;
@available(added=12)
string:100 info;

或者,如要將列舉從 strict 變更為 flexible

@available(removed=12)
strict enum Color { ... };
@available(added=12)
flexible enum Color { ... };

除了程式庫之外,所有 FIDL 元素都可以切換。由於可用時間不重疊,因此不會發生命名衝突。

這項提案並未排除未來的語法,可將可用性屬性直接套用至 FIDL 屬性。如果導入這類語法,則只能支援 addedremoved,因為 deprecated 在所有 FIDL 屬性中都沒有合理的解讀方式。

繼承

FIDL 元素會形成有向非循環圖表,子元素會從父項元素繼承可用性。

頂層宣告會繼承自程式庫。列舉、位元、結構體、資料表、聯集和服務的成員會繼承包函宣告。要求/回應參數會繼承其方法。方法和 compose 段落會繼承包函的通訊協定。組合方法會繼承原始方法和 compose 節。不使用通訊協定組合時,圖表會是樹狀結構。

如果子項元素具有可用性屬性,則會覆寫繼承的可用性。在做這麼做時,請務必避免重複或相互矛盾:導入版本只能是較新的版本,淘汰和移除版本只能是較舊的版本。

就組合方法而言,如果兩個父項都是在同一個平台下進行版本控制,則其可用性就是父項可用性的交集 (最新推出、最早淘汰和最早移除)。如果這些方法在不同平台上有版本,則組合方法會繼承兩個不同的可用性。在這種情況下,「可供選擇的版本」的定義就很重要。無論採用哪種做法,系統都會從兩個父項中合併淘汰 note

以下是平台內組合的一般情況:

library foo;
protocol Def { @available(added=A, deprecated=B, removed=C) Go(); };
protocol Use { @available(added=D, deprecated=E, removed=F) compose Def; };

原始方法 foo/Def.Go 是在 A 中引入,在 B 中淘汰,並在 C 中移除。組合方法 foo/Use.Go 是在 max(A,D) 中引入,在 min(B,E) 中淘汰,並在 min(C,F) 中移除。這表示所有組合方法都受 compose 節的供應情形所限,但如果 Defcompose 導入後才導入這些方法,或是在 compose 淘汰/移除前淘汰/移除這些方法,則部分方法的供應情形可能會更狹窄。

使用驗證

某些 FIDL 元素之間存在關聯,因為一方會使用另一方。如果元素出現在結構體/資料表/聯集成員或要求/回應參數的型別中,則會使用 FIDL 元素;如果元素出現在錯誤型別中,則會使用方法;如果元素出現在值中,則會使用常數或枚舉/位元成員;如果元素出現在預設值中,則會使用結構體成員。以下列舉部分範例:

const uint32 X = 10;
const uint32 Y = X;  // Y uses X

table Entry {};
protocol Device {};
resource struct Info {
    vector<Entry>:X entries;  // entries uses Entry and X
    request<Device> req;      // req uses Device
    uint32 val = Y;           // val uses Y
    Info? next;               // next uses Info
};

在指定版本的情況下,如果符合下列情況,fidlc 就會產生錯誤:

  • 現有元素使用缺少的元素;或
  • 可用的元素使用已淘汰的元素。

生命週期語意

在指定版本選項的情況下,如果有可用的 FIDL 元素,系統會照常發出該元素。如果已淘汰,我們會在 JSON IR 中註明,綁定中的行為則如 RFC-0058:引入 [Deprecated] 屬性所述。如果沒有,我們會從 JSON IR 中省略該屬性。

如果 FIDL 元素未遭其他元素使用,使用 @available(removed=<N>) 加上註解,就等同於從 .fidl 檔案中刪除該元素,只是使用 removed 屬性可維持歷史準確性,而刪除元素則不會。這可避免 .fidl 檔案因累積變更而變得臃腫且難以讀取。

HEAD 的用途

HEAD 版本 ID 代表開發的最新進度。用戶端可以自由針對 HEAD 繫結編寫程式,但不應預期這些繫結會穩定。舉例來說,假設您下載 red.fidl,其中註解中使用的最高版本 ID 為 12 和 HEAD。如果您下載較新的 red.fidl 副本,可以合理預期版本 12 的 API 會相同,也就是作者不會變更歷史記錄。但 HEAD API 可能完全不同。

這項功能可在採用 FIDL 版本管理功能時提供連續性。依附版本化程式庫的 HEAD 繫結,與依附未版本化程式庫的繫結相同。

這也讓協作專案中的 FIDL 變更更容易。編寫 CL 時,查詢目前版本會相當繁瑣,且容易發生競爭狀態,尤其是在程式碼審查期間發生變更時。相反地,貢獻者可以直接使用 HEAD,而專案擁有者可以稍後用特定版本取代。

舊版支援

修訂 (2022 年 10 月)。這個部分是在 RFC 接受後新增。

使用 @available(removed=<N>) 移除 API 後,該 API 就不會再出現在版本 N 以上版本產生的繫結中。這會導致難以建構支援多個 API 級別的 Fuchsia 系統映像檔。如果系統映像檔是針對 N-1 繫結建構,就無法為在 N 新增的方法提供實作項目。如果是針對 N 繫結建構,就無法為 N 移除的方法提供實作項目。

為解決這個問題,我們推出名為 LEGACY 的新版本,其運作方式與 HEAD 相同,但也包含舊版方法。舊版方法是標記為 @available(removed=<N>, legacy=true) 的方法。這會使用名為 legacy 的新布林值引數,預設為 false,且僅在 removed 存在時才允許使用。例如:

@available(added=1)
library example;

protocol Foo {
    @available(removed=2)  // implies legacy=false
    NotLegacy();

    @available(removed=2, legacy=true)
    Legacy();
};

以下是針對不同版本時,範例繫結項目中包含的方法:

目標版本 包含的方法
1 NotLegacyLegacy
2
HEAD
LEGACY Legacy

根據政策規定,Fuchsia 平台中的所有方法在移除時,都應保留舊版支援。一旦 Fuchsia 平台在方法移除前停止支援所有 API 級別,就可以安全地移除 legacy=true 和方法的實作項目。

當 Fuchsia 平台充當用戶端而非伺服器時,舊版方法可讓平台繼續針對指定舊版 API 層級的用戶端呼叫方法。如果是指定較新 API 級別且不預期該級別的應用程式,則方法必須標示為 flexible,才能忽略呼叫。詳情請參閱 RFC-0138:處理不明互動

legacy 引數可用於任何 FIDL 元素,而不僅限於方法。舉例來說,如果您要移除類型和使用該類型的程式碼,則該類型也必須標示為 legacy=true。這是使用驗證的結果,而非新規則。

舉例來說,請考慮在要求中使用的資料表。移除其中一個欄位時,您可能會想使用 legacy=true,以便伺服器繼續支援設定該欄位的用戶端。另一方面,如果忽略欄位即可保留 ABI,就不需要舊版支援。同樣地,如果您在回應中使用資料表,只有在需要設定該欄位以便為舊版用戶端保留 ABI 時,才需要使用 legacy=true 移除欄位。

替換元素時,請勿使用舊版支援功能,因為可用性代表變更,而非移除。否則會導致錯誤:

protocol Foo {
    @available(removed=2, legacy=true)
    Bar();

    @available(added=2)
    Bar();
}

由於第一個 Bar 會在 LEGACY 中重新加入,而第二個 Bar 從未移除,因此兩者都會出現在 LEGACY 中,而 fidlc 會針對具有重疊可用性的同名元素,發出與現有情況相同的錯誤。

JSON IR

為了在 IR 中表示已淘汰的項目,我們新增了兩個欄位:

deprecated: <bool>,          // required
deprecation_note: <string>,  // optional

這些項目會新增至下列 JSON IR 結構定義

#/definitions/bits
#/definitions/bits-member
#/definitions/const
#/definitions/enum
#/definitions/enum-member
#/definitions/interface
#/definitions/interface-method
#/definitions/service
#/definitions/service-member
#/definitions/struct
#/definitions/struct-member
#/definitions/table
#/definitions/table-member
#/definitions/union
#/definitions/union-member
#/definitions/type_alias

請注意,IR 並非代表程式庫淘汰。但仍會透過繼承產生影響,以及下一節所述的警告。

指令列介面

如要指定版本選項,fidlc 會接受 --available <P>:<V>,其中 <P> 是平台 ID,<V> 是版本 ID。您可以為不同的平台 ID 多次提供這個標記。例如:

fidlc --json out.json --available red:2 --available blue:HEAD
      --files red.fidl --files blue.fidl

如果版本選取項目缺少平台,或有未使用的平台 (相較於指定程式庫的版本平台),fidlc 會產生錯誤。如果版本選取項目中已淘汰/缺少任何程式庫,fidlc 就會產生警告/錯誤。3

政策

透過 FIDL 版本管理功能,您可以改進 API 而不會破壞應用程式,但這並非保證。為此,我們特別針對 Fuchsia 平台採用下列政策:

  • 標註所有發生在 HEAD 的變更。
  • 請勿變更 FIDL 程式庫的歷史記錄。唯一例外的是刪除舊 FIDL 元素的程序,請參閱下文。
  • 請先淘汰 FIDL 元素,再將其移除,除非您要交換屬性以進行變更。
  • 淘汰元素時:
    • 使用 note 欄位告知開發人員應改用哪個值。
    • 在文件註解中撰寫 # Deprecation 部分,提供更詳細的說明,並說明淘汰時程。
  • 變更 FIDL 屬性時請務必謹慎,舉例來說,將類型從「嚴格」變更為「彈性」,或是從「值」變更為「資源」,都可能對 API 造成重大影響。API 委員會應視情況判斷這些變更。

這些政策的強制執行方式如下:

  • SDK 中的所有 FIDL 變更仍須經過 API 委員會核准。
  • fidl-lint 應檢查已淘汰的元素是否已在其文件註解中設定 note 欄位和 # Deprecation 部分。
  • 日後,應有 CQ 工作會根據 FIDL API 摘要強制執行其他政策 (變更記錄、移除前淘汰,以及 API/ABI 不相容的變更)。

此外,我們也提出了兩項新程序,詳細資訊將在稍後的 RFC 中說明:

  • 發布新的 API 級別。這可能會在固定時間表上發生,其中會在新的 API 級別中發布自上次 API 級別發布以來所做的部分或所有變更,方法是將 HEAD 出現的情況替換為新的級別。
  • 刪除舊的 FIDL 元素。一段時間過後,系統會從 .fidl 檔案中刪除標示為已移除的元素。只有在元素未在任何地方被引用時,才能刪除,因此這個程序可能會在固定時間表上刪除所有舊版 API 級別的元素。

我們可以使用與 fidl 格式相同的樹狀結構檢視器方法,建立工具來簡化這兩個程序。

實作

這項設計大多可在 fidlc 中實作。剖析 @available 語法時,需要使用另一個 RFC 來變更 FIDL 的註解語法。語意可能會先在實驗旗標後方實作。

當 fidlc 編譯程式庫時,即使它會在單一版本產生 JSON IR,也應同時驗證所有可能的版本。因此,您「不應」依序產生及檢查每個版本。而是應將元素暫時分解為 (名稱、版本範圍) 元組。這項程序類似於將 NFA 轉換為 DFA。例如:

type MyTable = table {
    @available(added=2)
    1: name string;
    @available(added=HEAD)
    2: age uint32;
};

這會分解為以下內容 (使用偽語法示範):

type «MyTable, [0,1]»    = table {};
type «MyTable, [2,HEAD)» = table { 1: name «string, [0,HEAD]»; }
type «MyTable, HEAD»     = table { 1: name «string, [0,HEAD]»; 2: age «uint32, [0,HEAD]» };

在發出 IR 之前,fidlc 會修剪宣告,只保留在版本選取中要求的宣告。

公開問題。當 FIDL 程式庫以不同平台的版本編譯時,時間分解方法很難推廣。由於這項功能並非主要用途 (依 API 級別為 Fuchsia 平台命名) 所需,我們可以延後處理這個問題,並讓 fidlc 一開始只允許一個 --available 標記。

HEAD 版本 ID 可做為特定情境常數實作,類似於可做為字串和向量長度限制的 MAX 常數

也有一些實作工作是在 fidlc 之外進行。首先,fidldoc 需要考量版本化。舉例來說,如果元素已淘汰,說明文件應清楚標示這項資訊。也可以提供 API 層級的下拉式選單,讓您查看歷來文件。其次,fidlgen 後端需要在 JSON IR 中使用 "deprecated" 欄位。舉例來說,fidlgen_rust 可以將其轉譯為 #[deprecated] Rust 屬性。如需其他語言的範例,請參閱 RFC-0058:引入 [Deprecated] 屬性

在 SDK 中的程式庫開始使用註解之前,我們需要將 --available fuchsia:HEAD 新增至 GN 範本,以便建構 FIDL 繫結。這項假設是基於所有樹狀結構內的程式碼都會使用 HEAD 繫結。當我們提出 C++ 的平台版本化提案時,可能需要針對其他版本的 FIDL 繫結建構樹狀結構內結構碼,以便進行測試。

在 petal 建構系統中,我們會新增 fuchsia_api_level 宣告,並將其連接至 --available 標記。這項功能需要與 fidlc CLI 變更協調,首先接受並忽略 --available 標記,然後再要求使用。

成效

這項提案不會影響執行階段效能。這會影響建構效能,因為 fidlc 必須執行更多工作,但 FIDL 編譯從未是 Fuchsia 建構時間的重要因素。

安全性考量

這項提案應可對安全性產生正面影響,因為版本化可讓您更輕鬆地遷移至具備更佳安全性屬性的新 FIDL API。這項優點應大於因支援舊版 ABI 而增加攻擊面的負面影響。

本提案並未提供機制,以便根據應用程式的目標 ABI 修訂版本隱藏 ABI,如 RFC-0002 所述。雖然這麼做可以提升安全性,但最好在通訊協定演進的完整 RFC 中納入這項設計。

隱私權注意事項

這項提案應對隱私權產生正面影響,因為版本管理可讓您更輕鬆地遷移至具備更佳隱私權屬性的新 FIDL API。

測試

我們目前會同時使用單元測試和黃金測試,測試 FIDL 工具鍊。單元測試主要用於 fidlc 內部。黃金測試會編譯一組 .fidl 檔案,並確保產生的構件 (JSON IR 和所有繫結) 與先前審查的黃金檔案完全相同。

FIDL 版本管理也會採用類似做法。它會針對 fidlc 中的小邏輯片段使用單元測試。舉例來說,如果表格成員的註解指出該成員是在表格本身之前引入,系統會執行測試,確保編譯作業失敗並顯示適當的錯誤訊息。它也會使用黃金測試,但不會擴充現有的黃金測試架構。為每個版本產生程式庫構件會導致黃金檔案膨脹,且難以驗證正確性。相反地,這個專案會擁有自己的 .fidl 檔案組合,其中包含每個版本的 JSON IR 黃金diffs。這樣一來,您就能輕鬆驗證版本管理功能是否如預期運作。

這不會讓平台 API 實作人員的測試變得更困難:系統會針對 HEAD 編寫測試,就像我們目前不會針對舊版 Git 修訂版本的 FIDL 檔案執行測試一樣。這也不會讓 SDK 使用者更難進行測試:他們會以單一版本的平台進行測試,就像目前使用單一 SDK 版本進行測試一樣。

說明文件

@available 語法會在 FIDL 語言規格中記錄。一旦有發布新 API 級別的程序,就需要更多文件。舉例來說,我們需要教導程式庫作者在新增新的 API 元素時使用 @available(added=HEAD)。只要使用適當的工具,就不會忘記執行這項操作。詳情請參閱「政策」一節。

由於供應情形屬性已淘汰 [Deprecated] 屬性,因此我們也需要從 FIDL 屬性頁面中移除 [Deprecated] 屬性。

您應更新 FIDL 來源相容性說明文件,以便顯示使用可用性屬性的 FIDL 變更,或顯示如何在使用版本時套用不同類型的 FIDL 差異。說明文件也應說明 FIDL 版本控制與一般轉換作業的互動方式。有了版本控制功能,無論是否使用樹狀結構外,變更 FIDL 元素都一樣簡單。這樣一來,您就不需要使用某些類型的軟性轉場效果。但這並不會消除所有多步驟轉場效果,只是在協調這些轉場效果時,移除了單一共用時間軸的限制。

缺點、替代方案和未知事項

實施這項提案的成本為何?

這項提案會增加 FIDL (語言) 和 fidlc 的複雜度。這會讓程式庫作者更難進行簡單且安全的變更,但他們可以更輕鬆地進行其他類型的變更 (例如在嚴格列舉中新增成員)。

替代做法:使用舊版 SDK

FIDL 版本管理功能可讓應用程式繼續推出新的 SDK,同時仍固定使用舊版 API 級別。但為何不直接使用舊版 SDK,讓這項提案變得毫無必要呢?原因如下:

  • 使用最新的 SDK,使用者就能取得其他所有項目的最新副本,例如 FIDL 工具鍊。
  • 每個元件都會指定目標 API 級別。為每個元件使用不同的 SDK 既複雜又不切實際。

替代方法:變更記錄檔案

除了可用性屬性外,單獨的變更記錄也能記錄 FIDL 程式庫的歷史記錄。其中一種方法是從每個 .fidl 檔案回溯至原始檔案,並取得一組文字差異。這麼做可以簡化許多事情,例如 FIDL 屬性版本管理的難度。如同所提及的設計,一次驗證所有程式庫版本是不切實際的做法。不過,由於這個替代做法可避免意外變更歷史記錄的問題,因此這項做法可能不太必要。但這會讓您更難回答「這個元素是何時推出的?」這類問題。這基本上會複製 Git 記錄,主要差異在於建立可下載的 SDK 時會保留記錄。

如果我們日後變更 FIDL 語法,文字差異比較就會難以維護。變更記錄設計的另一種變化版本會定義新格式,用於記錄 FIDL 程式庫的變更內容,以及變更發生時的版本。由於 fidlc 可以讀取變更記錄,並產生經過時間分解的 AST,因此這項設計可與一次驗證所有版本的功能相容。不過,這需要更多工具。舉例來說,我們可能會希望開發人員按照目前的方式編輯 .fidl 檔案,並在提交前執行工具,將內容附加至變更記錄檔案。

替代做法:依程式庫版本

另一種設計方式是為每個 FIDL 程式庫提供個別版本。這會導致從 API 級別對應至 SDK 中每個 FIDL 程式庫的版本。舉例來說,API 級別 42 可能代表 fuchsia.auth 1.2 版、fuchsia.device 5.7 版等等。

對於關心個別程式庫的使用者來說,這種做法有其優點。每個版本都會與該程式庫相關,您可以根據目前的版本編號,估算程式庫的演進程度。相較之下,如果是各平台版本,程式庫中的變更版本之間可能會有很大的差距。

但這也引發許多疑問。是否有個別程式庫版本,表示 SDK 程式庫必須追蹤其所依附的其他 SDK 程式庫版本?SDK 使用者可以混合使用不同版本的 SDK 程式庫嗎?如果您對這兩個問題的答案都是「是」,就會讓 FIDL 版本管理變得更加複雜。如何判斷特定版本組合是否能相容?如何避免同時編譯相同程式庫繫結的多個副本?如果兩個問題的答案都是「否」,那麼每個程式庫的版本管理功能就會變成不必要的間接管理,讓人誤以為版本管理是在程式庫層級進行,但實際上並非如此。

替代做法:非對稱式淘汰

RFC-0002 在「生命週期」部分中指出:

該元素可能已淘汰。針對舊版 ABI 修訂版本指定的元件,在較新的平台版本上執行時,仍可使用該元素。不過,以較新 API 級別為目標的最終開發人員將無法再使用該元素。

接著說明這對 FIDL 有何影響

當特定 API 級別淘汰某個通訊協定元素 (例如表格中的欄位或通訊協定中的訊息) 時,我們希望以該 API 級別為目標的元件能夠接收含有該通訊協定元素的訊息,但希望避免這些元件傳送含有該通訊協定元素的訊息。

FIDL 版本管理功能與這項行為不同,因此我們將其納入此處做為替代方案。要防止終端開發人員在特定 API 層級使用 FIDL 元素,同時讓 Fuchsia 平台中的程式碼在執行階段支援該元素,這項工作相當困難。如前所述,這項假設有誤,因為 Fuchsia 平台一律會充當伺服器,而 SDK 使用者一律會充當用戶端。有時角色可能會互換,甚至難以判斷。我們可以透過引入 @platform_implemented@user_implemented 等屬性來區分這些元素。這對方法很有幫助,但類型和類型成員 (以下稱為「類型元素」) 的不對稱行為較難解決。

如要讓類型元素非對稱性淘汰,一種方法是產生可防止使用這些元素的虛設檔。舉例來說,已淘汰的資料表欄位可能會在繫結中顯示為 FidlDeprecated 類型的值,這類值在使用時會產生類型檢查錯誤。Fuchsia 平台中的程式碼可透過新的 fidlgen 標記 --allow-deprecated 繼續支援已淘汰的元素,該標記會產生程式碼,就好像沒有任何元素遭淘汰一樣。但這種做法有兩個問題。首先,這會使您難以在 Fuchsia 中移除已淘汰的元素,因為這些元素不會顯示為已淘汰。其次,這麼做也能讓最終開發人員輕鬆使用標記。這會抵銷所需的獎勵

這種做法會將存取新 API 的權限與執行這些遷移作業連結,藉此鼓勵開發人員改用非淘汰的介面。具體來說,如要存取新推出的 API,開發人員必須變更目標 API 級別,這會要求他們遷移任何在該 API 級別中已淘汰的介面。

也就是說,有了 --allow-deprecated,開發人員只要使用旗標,就能取得新推出的 API 存取權,而無須遷移已淘汰的 API。

類型元素的另一種做法是在執行階段產生錯誤。舉例來說,如果表格欄位已淘汰,當欄位存在時,繫結可能會在編碼期間產生錯誤 (但不會影響解碼)。不過,執行階段行為不在本提案的範圍內

總而言之,不對稱淘汰過於細微且複雜,因此無法納入這項提案。如果不對稱淘汰的優點值得付出複雜性的代價,這些挑戰可能會在日後的 RFC 中解決。

替代方案:完整記錄 IR

根據這項提案,版本資訊只會出現在 JSON IR 之前。產生 IR 後,我們會使用固定版本。這項資訊對於產生繫結來說已足夠,但對於可能需要使用版本資訊的 fidldoc 等工具來說,就沒那麼實用。除了這些工具會剖析 .fidl 檔案,或透過比較多個版本的 JSON IR 來推測生命週期,另一種做法是引入新的 JSON IR 模式,其中包含所有歷程記錄和可用性資訊。這與在 IR 中加入可用性屬性不同,因為這表示您加入的元素已在最新版本中標示為已移除。

這種做法有兩個問題。首先,有些 JSON IR 檔案的結構定義和用途與其他檔案略有不同,這並不理想。設計全新格式可能會更好,但這也有缺點。其次,如果不知道 UI fidldoc 應呈現的內容,就很難判斷完整歷史 IR 應為何種樣貌。舉例來說,如果某類型的 resource 修飾符已新增及移除十次,系統會顯示什麼?這類問題和所使用的表示法,應在另一個 RFC 中加以討論。

既有技術與參考資料

這項提案是 RFC-0002:平台版本管理中整體計畫的一部分。閱讀該 RFC 對於瞭解此 RFC 背後的背景和動機至關重要。其「先前技術和參考資料」一節著重於其他作業系統:Android、Windows 和 macOS/iOS。我們將在這裡著重於其他程式設計語言和 IDL,以及這些語言和 IDL 的 API 版本管理方式。

Swift、Objective-C

Swift 使用的 @available 屬性與這項提案中的屬性類似,Objective-C 則使用類似的 API_AVAILABLE 屬性。只能使用硬式編碼的 Apple 平台清單,例如 macOS 和 iOS。您也可以使用 swift 平台,根據編譯期間使用的 Swift 語言版本控制可用性。版本會以一個、兩個或三個數字指定,並以點號分隔,遵循 semver 語意。兩種語言都提供類似的語法,可在執行階段檢查平台版本。

荒漠油廠

Rust 會使用穩定性屬性 #[stable]#[unstable]#[rustc_deprecated] 為標準程式庫加上註解。每個不穩定元素都會連結至 GitHub 問題,且只能由使用對應 #[feature] 屬性選擇加入的開發人員使用。穩定屬性會指出元素穩定的 Rust 版本。不過,這只是用於說明,不會控制可見度。

Protobuf、gRPC

Protocol Buffers 不提供版本管理工具。相反地,他們比 FIDL 更重視前向和回溯相容性。舉例來說,系統沒有結構體 (只有類似 FIDL 資料表的訊息)、沒有嚴格型別 (所有型別都有彈性行為),且不支援對列舉的完整比對 (自 proto3 起)。

Google Cloud API 會搭配 gRPC 使用通訊協定緩衝區,並提供版本管理相容性指南。版本策略是根據慣例而非系統內建功能。API 會在 protobuf 套件結尾編碼主要版本號碼,並將其納入 URI 路徑中。如此一來,服務就能同時支援多個主要版本,而用戶端也會在原地收到回溯相容的更新,也就是不需要採取任何行動就能遷移。


  1. 在實作期間,這項規則已放寬,可同時導入和淘汰。這樣一來,您就可以透過交換,手動分解任何版本邊界上的 FIDL 宣告。 

  2. 本文使用 RFC-0086:更新 RFC-0050:FDil 屬性語法中所介紹的語法。 

  3. 在實作期間,這些規則已省略,以便簡化與建構系統的整合作業。針對版本選取,fidlc 預設會使用 HEAD,並忽略未使用的平台。對於程式庫宣告,除了繼承之外,可用性不會有任何影響,因此缺少的程式庫就等同於空白程式庫,且不會有淘汰的警告。