RFC-0232:多個 API 級別的 FIDL 繫結

RFC-0232:適用於多個 API 級別的 FIDL 繫結
狀態已接受
區域
  • FIDL
說明

啟用 FIDL 繫結功能,以便與多個 API 級別相容

問題
Gerrit 變更
作者
審查人員
提交日期 (年-月-日)2023-09-27
審查日期 (年-月-日)2023 年 10 月 24 日

摘要

今天產生 FIDL 繫結時,我們會將樹狀結構中的 LEGACY 和樹狀結構外的編號 API 級別設為目標。本文件建議透過提供一次指定多個 API 級別的方式,將 LEGACY 泛用化並淘汰。這個功能一開始只會用於樹狀結構,但我們可能會發現在某些情況下,這個功能在樹狀結構外也相當實用。

背景

根據 FIDL 版本管理的原始設計,您無法移除元素,同時保留對該元素的 ABI 支援。舉例來說,如果 CL 將方法標示為 removed=5,就必須刪除該方法的實作。這是因為我們在 HEAD 上建構 Fuchsia 平台,而方法的伺服器繫結會因為大於 5 而不再存在於 HEAD

為解決這個問題,我們修正 RFC-0083,以導入 LEGACY 版本和 legacy 引數。LEGACY 版本與 HEAD 類似,但如果移除的元素已標示為 legacy=true,則會重新加入該元素。

提振精神

LEGACY 有幾個問題:

  • 這是一個不含任何資訊的偽版本。已凍結的 API 級別 N 的繫結包含 N 中的所有 API,但 LEGACY 的繫結則包含在建構時使用 legacy=true 標示為 removed 的所有項目 (除了 HEAD 中的所有項目)。

  • 舊版支援功能會根據個別 API 決定,且會隨著時間變更。因此,我們很難保證平台版本實際支援特定舊版 API 級別。

  • 您無法在納入舊版支援功能的情況下,指定特定 API 級別 (即 HEAD 以外)。

  • 它只解決部分相容性問題,其中 Fuchsia 平台是通訊的一端。但在產品元件之間,則有通訊協定可用於傳輸資料。

  • 它會賦予 Fuchsia monorepo 權限,因此更難將元件從單一資料庫和發布程序中分割。

利害關係人

協助人員:abarth@google.com

審查者:hjfreyer@google.com、wilkinsonclay@google.com、ddorwin@google.com

諮詢對象:wez@google.com、sethladd@google.com

社交化:在撰寫 RFC 之前,我已與 FIDL 團隊和平台版本工作群組討論過這個想法。

設計

我們建議在產生 FIDL 繫結時,允許一次指定多個 API 級別。舉例來說,使用 --available fuchsia:10,15 叫用 fidlc 會指定 API 級別 10 和 15,導致結合這兩個級別元素的繫結。如果具有特定名稱的元素在 10 級和 15 級有不同的定義,我們會使用 15 級的定義,因為它較新。

這會淘汰 LEGACY 版本。建構 Fuchsia 平台時,我們會以支援的 API 級別組合為目標,而非 LEGACY 繫結。這樣一來,您也不需要使用 legacy=true 標示個別 API。

詳細資料

  • 從 FIDL 語言中移除 LEGACY 版本,以及 @available 屬性的 legacy 引數。

  • 將 fidlc 的 --available 指令列引數語法從 <platform>:<target_version> 變更為 <platform>:<target_versions>,其中 <target_versions> 是逗號分隔的版本清單。例如:

    • --available fuchsia:10
    • --available fuchsia:10,11
    • --available fuchsia:10,20,HEAD
  • <target_versions> 清單必須經過排序,且不得含有重複項目。這項做法旨在強調版本會建立線性記錄,且較新版本會優先處理。

  • <target_version> 清單會決定一組候選元素

    • 如果 <target_versions>{v | v >= A} 相交,則標示為 @available(added=A) 的元素即為候選元素。

    • 如果 <target_versions>{v | A <= v < R} 相交,則標示為 @available(added=A, removed=R) 的元素即為候選元素。

    • 請注意,此 RFC 取決於 RFC-0231:FIDL 版本替換語法。為了決定候選元素,replaced 會與 removed 相同。

  • 如果元素符合以下條件,就會納入繫結:(1) 該元素是候選元素,且 (2) 在所有同名候選項目中,其 added 版本最高。

  • 如果標示為 @available(..., deprecated=D, ...) 的元素根據上述規則納入繫結,當 <target_versions>{v | v >= D} 相交時,該元素就會視為已淘汰。這不會對目前產生的程式碼造成影響,但日後可能會 (https://fxbug.dev/42156877)。

  • 如前所述,--available 標記可用於多個平台,且可重複使用。這兩項功能 (多個平台和多個目標版本) 之間沒有顯著的互動。

  • 如同先前所述,編譯的成功或失敗必須與主程式庫平台的 --available 標記無關。(可能會依賴其他平台的依附元件 --available 標記)。舉例來說,如果編譯作業在 --available fuchsia:15,16 中成功,則保證在 --available fuchsia:10,100,HEAD 中也會成功。同樣地,如果前者失敗,後者也會失敗,並顯示相同的錯誤集。

  • 建構 Fuchsia 平台時,請將 --available fuchsia:LEGACY 替換為 --available fuchsia:<target_versions>,其中 <target_versions> 包含所有支援的執行階段 API 級別、開發中的 API 級別和 HEAD

影響

這項設計可為有效的 FIDL 程式庫產生繫結,無論該程式庫隨時間的推移而演變為何種版本,皆可產生繫結。這是重要的限制,因為 FIDL 版本資訊可以代表任何符合語法的變更。具體來說,fidlc 允許多個同名元素共存,前提是這些元素的版本範圍不得重疊。當 <target_versions> 包含多個這類元素時,我們只會納入最新的元素。這項功能支援三種一般演進模式:

  • 生命週期。元素為 added,可能為 removed。在生命週期內指定任何版本時,我們會將該版本納入繫結中。範例:

    @available(added=1, removed=5)
    flexible Method() -> ();
    
  • 更換。元素為 added,後來 replaced 則有不同的定義。從概念上來說,這代表單一元素隨時間變化,而非兩個不同的元素。我們假設替換項目的設計與原始元素相容,且只在繫結中加入替換元素。範例:

    @available(added=1, replaced=5)
    flexible Method() -> ();
    @available(added=5)
    flexible Method() -> () error uint32;
    
  • 名稱重複使用。元素 removed 之後,其名稱可用於之後的新元素 added。這類似於取代,但兩個元素在概念上有所不同,且生命週期之間存在差距。我們假設較新的元素是優先選項,因此只會將該元素納入繫結中。範例:

    @available(added=1, removed=5)
    flexible Method();
    @available(added=10)
    flexible Method() -> ();
    

    請注意,如果以這種方式重複使用元素名稱,則對該名稱的參照無法跨越兩個定義之間的差距。舉例來說,以下程式碼無法編譯:

    @available(added=1, removed=5)
    type Args = struct {};
    @available(added=10)
    type Args = table {};
    
    @available(added=2)
    protocol Foo {
        Method(Args); // ERROR: 'Method' exists at versions 5 to 10, but 'Args' does not
    };
    

範例

請參考下列 FIDL 程式庫:

@available(added=1)
library foo;

@available(replaced=2)
type E = strict enum { V = 1; }; // E1
@available(added=2)
type E = flexible enum { V = 1; }; // E2

@available(added=3, removed=6)
open protocol P {
    @available(removed=4)
    flexible M() -> (); // M1

    @available(added=5)
    flexible M(table {}) -> (); // M2
};

選取單一版本時,繫結會包含以下內容:

--available E1 E2 P M1 M2
foo:1 ✔︎
foo:2 ✔︎
foo:3 ✔︎ ✔︎ ✔︎
foo:4 ✔︎ ✔︎
foo:5 ✔︎ ✔︎ ✔︎
foo:6 ✔︎
foo:HEAD ✔︎

選取多個版本時,系統會納入以下內容:

--available E1 E2 P M1 M2
foo:1,2 ✔︎
foo:1,HEAD ✔︎
foo:1,3 ✔︎ ✔︎ ✔︎
foo:1,2,3 ✔︎ ✔︎ ✔︎
foo:3,6 ✔︎ ✔︎ ✔︎
foo:3,HEAD ✔︎ ✔︎ ✔︎
foo:2,4,6 ✔︎ ✔︎
foo:1,3,5 ✔︎ ✔︎ ✔︎
foo:1,2,3,4,5,6,HEAD ✔︎ ✔︎ ✔︎

實作

  1. 實作 RFC-0231:FIDL 版本替換語法

  2. 在 fidlc 中實作新的 --available 功能。另外,請變更 JSON IR 中的「available」屬性,以便使用版本的字串陣列。

  3. 請變更所有現有的 legacy 引數,以便與新系統保持一致 (如果是在最低支援的 API 級別之前移除,請使用 false;如果是在該級別或之後移除,請使用 true)。如果差異很大,請考慮使用替代方案:覆寫機制

  4. 變更樹狀結構內平台版本,產生針對所有支援的 API 級別、開發中的 API 級別和 HEAD 產生繫結。

  5. 移除 FIDL 檔案中的所有 legacy 引數。

  6. 從 fidlc 中移除 LEGACY 支援。

  7. 淘汰 fidlc 錯誤代碼 fi-0182fi-0183

成效

這項提案不會影響成效。

人體工學

這項提案可讓 FIDL 版本管理更容易正確使用,因為您不必再擔心 legacy 引數。

回溯相容性

這項提案可移除個別 FIDL 程式庫作者選擇 legacy=true 的負擔,有助於實現 ABI 回溯相容性。這也讓我們聲明的「支援的 API 級別」集合更具信服力,因為這些 API 級別會直接用於為平台產生繫結。(當然,為了確保這些功能確實受支援,我們也需要進行測試)。

安全性考量

這項提案不會影響安全性。

隱私權注意事項

這項提案不會影響隱私權。

測試

您必須更新下列檔案,才能測試新行為:

  • tools/fidl/fidlc/tests/availability_interleaving_tests.cc
  • tools/fidl/fidlc/tests/decomposition_tests.cc
  • tools/fidl/fidlc/tests/versioning_tests.cc
  • tools/fidl/fidlc/tests/versioning_types_tests.cc

說明文件

下列說明文件頁面必須更新:

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

非問題:減少遷移動機

這項提案可視為減少在 RFC-0002:平台版本管理中所述的誘因,以便遷移淘汰的 API,因為您可以透過指定多個級別來存取新舊 API。不過,您現在已可使用 LEGACY 執行這項操作。就像花瓣不應在今天鎖定 LEGACY 一樣,也應避免濫用這項新功能。

此外,由於花瓣會透過 SDK 使用 fidlc,而非直接叫用,因此我們可以透過 SDK 建構規則中的限制來緩解這個問題。舉例來說,它們可以斷言目標版本字串不含半形逗號。

替代做法:版本範圍

我們可以要求範圍由兩個端點指定,而不是允許任意版本組合。我拒絕這個替代方案的原因如下:

  • 如果我們決定提高 API 級別的發布頻率,那麼只為部分 API 級別提供長期支援,可能會比較容易。這會導致版本集出現空隙,而非範圍。

  • 我們可能會想支援以 API 級別 N 為目標的個別舊元件,而無須重新編譯。如果其他所有內容都已移出 API 等級 NM,我們可能會出現 {N+1, ..., M} 的空白。

  • 我們目前為平台版本建立的內容,都沒有假設支援的 API 級別是連續範圍。舉例來說,version_history.json 包含 API 級別清單,而非範圍。

  • 使用範圍而非集合,不會讓 fidlc 實作更容易。這可能會讓效能稍微提升,但在實際操作中不太可能有差異。如果 fidlc 效能出現問題,可以優化許多容易取得的效益。

替代做法:覆寫機制

這項提案的一個缺點是,當您停止支援某個 API 級別時,可能很難同時更新 fuchsia.git 中的所有程式碼。為了將這類變更分成多個步驟,我們可能需要更精細的方式來控管綁定項目中包含的內容。您可以採取以下幾種做法:

  1. 在個別 fidl GN 目標中覆寫 <target_versions>

  2. 新增 @available 引數 unsupported=true,即使該元素通常會納入,也要排除該元素。這與 legacy 類似,但只會暫時使用 (理想情況)。

  3. 變更 --available 引數,以便接受 JSON 檔案,除了 <target_versions> 之外,還可提供要納入或排除的完整元素名稱清單。

我拒絕這個替代方案,因為我們不確定是否需要這項機制。因此,我們應該先嘗試在單一 CL 中進行變更。如果這麼做無法順利進行,我們應該嘗試使用條件式編譯來分階段進行變更,這樣一來,只有在停止支援 API 級別之前,才會納入實作項目。如果這麼做無法解決問題,我們可以重新考慮上述的覆寫機制。

我們也可以透過增加 API 級別的週期來減緩這種情況,這樣一來,每個 API 級別的移除作業就會減少。不過,這會對平台版本控制產生許多其他影響,因此不在本提案的範圍內。

替代做法:讓 legacy 預設為 true

請參閱 RFC-0233:預設使用 FIDL 舊版

這個替代方案可改善現況。以 false 做為預設值時,如果忘記新增 legacy=true,可能會導致 ABI 中斷。以 true 為預設值時,如果忘記新增 legacy=false,只會導致 fidlc 編譯錯誤或繫結中未使用的 API,這並非嚴重問題。

不過,這只是次要變更,無法解決本 RFC 中提出的所有問題。legacy 狀態仍會依 API 控制,導致特定 API 級別的執行階段支援不一致,進而難以判斷特定版本是否完全支援 API 級別。

既有技術與參考資料

Android SDK 可讓您指定 compileSdkVersionminSdkVersion。請參閱「Android API 級別」和 <uses-sdk> 說明文件