| RFC-0083:FIDL 版本控管 | |
|---|---|
| 狀態 | 已接受 |
| 區域 |
|
| 說明 | 提供 FIDL 元素版本控管方式,並在特定版本產生繫結。 |
| 問題 | |
| Gerrit 變更 | |
| 作者 | |
| 審查人員 | |
| 提交日期 (年-月-日) | 2021-02-12 |
| 審查日期 (年-月-日) | 2021-04-05 |
摘要
本文建議為 FIDL 元素加上版本註解,並提供在指定版本產生繫結的機制。這樣一來,API 演進與採用作業就會分離,程式庫作者就能更輕鬆地進行變更,同時為終端開發人員提供穩定性。這為 FIDL 在 RFC-0002:平台版本控管中的角色奠定基礎。
提振精神
雖然 FIDL 在變更期間提供許多 ABI 相容性功能,但實際上 API 演進並不容易。在 Fuchsia SDK 中,如果 FIDL 變更與 ABI 相容,但與 API 不相容,則需要經過仔細協調的軟轉換,以免破壞下游編譯作業。如果發生問題,我們通常必須還原 Fuchsia 中的變更。隨著 SDK 程式庫的使用量增加,進行這些變更的難度也會提高。
FIDL 版本控管功能可解決這個問題,讓 FIDL 程式庫作者和消費者能按照自己的步調繼續前進。程式庫作者新增、移除或修改 API 時,變更會在新 API 級別中發布。應用程式採用新 API 級別前,指定舊 API 級別的應用程式不會看到繫結有任何變更。除了提供穩定性,這項做法也讓終端開發人員能一次遷移一個元件至新版 API,因為目標 API 級別是依元件指定。
圖 1 說明破壞性 API 變更。如果沒有版本控管,應用程式就會中斷,導致還原。透過版本控管,應用程式只需固定在舊版 API 級別即可。當然,如果嘗試將應用程式的固定 API 級別從 12 升級至 13,也會發生相同問題。但這些問題可以非同步修正,不必還原 Fuchsia 中的原始變更,也不會導致專案停滯。
圖 1:FIDL 版本控管前後的 API 演進 (左側為前,右側為後)
術語
API 級別和 ABI 修訂版本的定義請參閱 RFC-0002:平台版本控管,但有以下異動:
RFC-0002 修訂版。Fuchsia API 級別是不帶正負號的 63 位元整數。換句話說,這是 64 位元,但高位元必須為零。
FIDL 元素是 FIDL 程式庫的離散部分,會影響產生的繫結。包括:
- FIDL 程式庫本身
- 常數、列舉、位元、結構體、資料表、聯集、通訊協定和服務宣告
- 別名和新類型 (來自 RFC-0052:型別別名和新類型)
- 列舉、位元、結構體、資料表、聯集和服務的成員 (包括資料表和聯集的成員)
reserved - 通訊協定中的方法和
compose節 - 方法中的要求和回應參數
FIDL 屬性是 FIDL 元素的可修改部分,本身並非獨立元素。包括:
- 屬性
- 修飾符
strict、flexible和resource - 常數、列舉成員和位元成員的值
- 結構體成員的預設值
- 類型限制 (成員、參數和型別別名)
- 方法種類 (單向、雙向、事件)
- 方法錯誤語法 (存在與否和類型)
這樣一來,就只剩下少數既非 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 之間的無正負號 64 位元整數 (含首尾),或等於 2^64-1。後者稱為 HEAD,且會受到特殊處理。
修訂 (2022 年 10 月)。為支援舊版方法,我們改為使用 2^64-2 代表
HEAD,2^64-1 代表LEGACY。
修訂內容 (2024 年 4 月)。
HEAD和LEGACY已在 RFC-0246 中重新定義。
版本 ID 完全依「新於」關係排序。 如果 X > Y,則版本 X 比版本 Y 新。
FIDL 元素相對於平台的「可用性」是指元素「推出」時的版本,以及 (選擇性)「淘汰」和「移除」時的版本。淘汰和移除日期必須晚於推出日期。1如果提供這兩項日期,移除日期必須晚於淘汰日期。
如果 FIDL 元素適用於某個平台,就會根據該平台進行版本控管。如果任何平台有版本,即為有版本。
如果平台版本比 FIDL 元素的推出時間新或相同,但比其淘汰和移除時間舊 (如有),則該平台版本適用該元素。如果版本等於或新於元素的淘汰版本,但舊於移除版本 (如有),則為已淘汰。如果可用或已淘汰,則為「存在」。否則為「absent」。
版本選取是指將版本指派給一組平台。舉例來說,您可以選取 red 的第 2 版和 blue 的 HEAD 版。
如果 FIDL 元素適用於所有平台,則會適用於所選版本。如果所有平台都提供這項功能,但一或多個平台已淘汰這項功能,則這項功能會遭到淘汰。否則為「absent」。
語法
可用性屬性的形式如下,2靈感來自 Swift 的 available 屬性:
@available(added=<V>, deprecated=<V>, removed=<V>)
每個 <V> 都是版本 ID。added、deprecated 和 removed 欄位分別表示元素的推出、淘汰和移除。這些都是選填欄位,但至少須提供一項資訊。
在程式庫中,必須提供 added 欄位 (deprecated 和 removed 為選填欄位)。此外,您也可以在選填欄位 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")
「availability」屬性會讓 RFC-0058:導入 [Deprecated] 屬性中的 [Deprecated] 屬性過時。
版本管理元素
FIDL 元素會使用可用性屬性進行版本控管。每個 FIDL 元素最多只能有一個可用性屬性,且只能在版本化程式庫中執行此操作。換句話說,如果程式庫中的任何 FIDL 元素已加上註解,則程式庫也必須加上註解。
FIDL 程式庫中的每個檔案都有自己的程式庫宣告,但這些檔案都代表相同的 FIDL 元素:程式庫。這與 FIDL 樣式指南一致:
將程式庫劃分為多個檔案,對程式庫的消費者沒有技術影響。... Divide libraries into files to maximize readability.
因此,程式庫中只能有一個程式庫宣告具有可用性屬性。文件註解的限制相同,因此選擇相同檔案來指定程式庫的可用性和文件註解,是合理的做法。
版本管理屬性
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 屬性的語法。如果導入這類語法,由於沒有適用於所有 FIDL 屬性的 deprecated 解譯方式,因此只能支援 added 和 removed。
繼承
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 節可用性限制,但如果 Def 在 compose 推出後才推出或淘汰/移除部分方法,這些方法可能會有較窄的可用性。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 後,系統就不會再為 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 | NotLegacy、Legacy |
| 2 | |
HEAD |
|
LEGACY |
Legacy |
根據政策規定,Fuchsia 平台中的所有方法移除時,都應保留舊版支援。Fuchsia 平台停止支援方法移除前的所有 API 級別後,即可安全移除 legacy=true 和該方法的實作項目。
當 Fuchsia 平台做為用戶端而非伺服器時,平台可透過舊版方法繼續呼叫以舊版 API 級別為目標的方法。如果指定較新的 API 級別,但不需要此方法,則必須標示為 flexible,系統才能忽略呼叫。詳情請參閱 RFC-0138:處理不明互動。
legacy 引數可用於任何 FIDL 元素,而不僅限於方法。舉例來說,如果您要移除某個型別和使用該型別的方法,則該型別也必須標示為 legacy=true。這只是使用驗證的結果,並非新規則。
再舉一個例子,假設要求中使用的資料表。移除其中一個欄位時,您可能希望使用 legacy=true,讓伺服器繼續支援設定該欄位的用戶端。另一方面,如果忽略該欄位足以保留 ABI,則不需要舊版支援。同樣地,如果資料表用於回應,只有在移除欄位時才需要使用 legacy=true,前提是設定該欄位是保留舊版用戶端 ABI 的必要條件。
交換元素時,請勿使用舊版支援功能,因為可用性代表變更,而非移除。如果這麼做,就會導致錯誤:
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 級別發布後所做的部分或所有變更,以新級別取代
HEAD的出現位置,藉此發布新的 API 級別。 - 刪除舊的 FIDL 元素。經過足夠時間後,即可從
.fidl檔案中刪除標示為已移除的元素。只有在元素未遭任何位置參照時,才能刪除元素,因此這個程序可能需要定期刪除特定 API 級別的所有舊元素。
我們可以運用與 fidl-format 相同的樹狀結構訪客方法,建構工具來簡化這兩個程序。
實作
這項設計大多可在 fidlc 中實作。剖析 @available 語法取決於另一項 RFC,該 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 繫結建構樹狀結構內程式碼,以進行測試。
在花瓣建構系統中,我們會新增 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 黃金差異。這應該有助於輕鬆驗證版本管理行為是否符合預期。
這不會增加平台 API 實作人員的測試難度:測試會針對 HEAD 撰寫,就像我們目前不會針對舊版 Git 修訂版本的 FIDL 檔案執行測試一樣。也不會增加 SDK 使用者的測試難度:他們會以單一版本的平台進行測試,就像目前使用單一版本的 SDK 進行測試一樣。
說明文件
@available 語法將記錄在 FIDL 語言規格中。一旦有發布新 API 級別的程序,就需要更多文件。舉例來說,我們需要教導程式庫作者在新增 API 元素時使用 @available(added=HEAD)。只要使用適當的工具,就不會忘記執行這項操作。詳情請參閱政策部分。
我們也需要從 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 程式庫提供個別版本。這會導致 SDK 中每個 FIDL 程式庫的版本都對應到 API 級別。舉例來說,API 級別 42 可能代表 fuchsia.auth v1.2、fuchsia.device v5.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,以及這些語言的 API 版本管理方法。
Swift、Objective-C
Swift 使用的 @available 屬性與這項提案中的屬性非常相似,而 Objective-C 則使用類似的 API_AVAILABLE 屬性。這些平台僅限於 macOS 和 iOS 等 Apple 平台硬式編碼清單。
他們也可以使用 swift 平台,根據編譯期間使用的 Swift 語言版本控管可用性。版本會指定為一、二或三個以半形句號分隔的數字,遵循 semver 語意。這兩種語言都提供類似的語法,可在執行階段檢查平台版本。
荒漠油廠
Rust 會使用穩定性屬性
#[stable]、#[unstable] 和 #[rustc_deprecated] 註解標準程式庫。每個不穩定元素都會連結至 GitHub 問題,且只有透過對應 #[feature] 屬性選擇加入的開發人員可以使用。穩定屬性表示元素穩定的 Rust 版本。不過,這僅供文件用途,不會控管可見度。
Protobuf、gRPC
通訊協定緩衝區未提供版本控管工具。而是更注重向前和向後相容性,而非 FIDL。舉例來說,沒有結構體 (只有訊息,類似於 FIDL 資料表)、沒有嚴格型別 (所有型別都有彈性行為),且列舉不支援詳盡比對 (截至 proto3)。
Google Cloud API 會搭配 gRPC 使用通訊協定緩衝區,並提供版本管理和相容性相關指南。版本控管策略是根據慣例,而非系統內建功能。API 會在 protobuf 套件結尾編碼主要版本號碼,並將其納入 URI 路徑。這樣一來,服務就能同時支援多個主要版本,而用戶端也會收到回溯相容的更新,也就是說,不必採取任何行動即可完成遷移。
-
在實作期間,這項規則放寬,允許導入和淘汰作業同時進行。因此,您可以在任何版本界線手動分解 FIDL 宣告,方法是交換。 ↩
-
本文採用 RFC-0086:RFC-0050 更新:FIDL 屬性語法中導入的語法。 ↩
-
在實作期間,為了簡化與建構系統的整合,我們省略了這些規則。如要選取版本,fidlc 預設會使用
HEAD,並忽略未使用的平台。對於程式庫宣告,除了繼承之外,可用性不會有任何影響,因此缺少的程式庫等同於空白程式庫,且不會顯示淘汰警告。 ↩