RFC-0097:FIDL 工具鍊

RFC-0097:FIDL 工具鍊
狀態已接受
區域
  • FIDL
說明

標準 FIDL 工具鍊的說明。

Gerrit 變更
作者
審查人員
提交日期 (年-月-日)2021-04-27
審查日期 (年-月-日)2021-05-26

摘要

我們說明 FIDL 工具鍊必須符合的要求,並提供問題分解方式的指引。

雖然具體的實作計畫超出本 RFC 的範圍,但預期工具 (例如 fidlcfidlgen_gobanjo) 和 Fuchsia 來源樹狀結構中的建構規則 (例如 fidl_library.gni) 會演進,以符合此處列出的需求。

此外,Fuchsia 來源樹狀結構外部的 FIDL 工具鍊應符合此處列出的需求。(本 RFC 在 Fuchsia 以外的環境不具效力,因此我們無法強制遵守,但強烈建議您遵守。)

術語

開始之前,我們先定義幾個術語。FIDL 工具鍊的簡化檢視畫面可歸納如下:

簡化工具鍊

FIDL 語言是由 fidlc 體現,代表前端編譯器 (或簡稱前端)。所有語言驗證都會在此進行。

前端會為編譯的每個 FIDL 程式庫產生中繼表示法 (稱為 JSON IR)。雖然名稱如此,但中介表示法不一定需要以 JSON 檔案表示。

接著,一或多個後端會處理 JSON IR,以產生輸出內容。請注意,從 FIDL 工具鍊的角度來看,JSON IR 的任何消費者都是後端。

最常見的生成輸出內容是目標語言 (例如 C++ 或 Rust) 的程式碼,可讓您操控型別、與通訊協定互動、開啟服務及使用常數。這類後端稱為 FIDL 繫結1,產生的程式碼應遵循繫結規格。我們通常會使用簡寫 fidlgen (或 fidlgen_<suffix>,例如 fidlgen_rustfidlgen_dart) 指稱產生 FIDL 繫結的後端。我們將網域物件稱為目標語言中的類別和型別集,用於表示 FIDL 型別。舉例來說,FIDL 列舉 fuchsia.fonts/Slant 會在 C++ 中有對應的網域物件 (以 enum class 形式),或在 Go 中有對應的網域物件 (以 type Slant uint32 形式)。

還有許多其他後端,各有需求和特質。舉例來說:fidldoc 會產生 FIDL 文件,例如 fuchsia.fonts 頁面;fidl_api_summarize 會產生 FIDL 程式庫的 API 摘要;fidlcat 會使用 JSON IR,以便提供執行階段內省功能。從 FIDL 工具鍊的角度來看,fidlcat 工具是後端,即使這只是該工具實際執行的極小一部分。

提振精神

FIDL 持續成長,已測試工具鍊的表達能力。 無法充分滿足新規定。需要新的擴充工具鍊。

我們首先會說明幾項新規定,然後接著介紹支援所有這些規定的方法。

查看整個節目

目前 FIDL 工具鍊會假設後端是依程式庫運作,因此只需要這個程式庫的 JSON IR 即可運作。

後端越來越需要存取多個 JSON IR,才能滿足需求。

舉例來說,fidldoc 需要一次記錄所有程式庫的 JSON IR,才能產生全域索引。fidlcat 也需要所有程式庫的檢視畫面才能運作。measure-tape 需要透過目標型別以遞移方式可觸及的程式庫 JSON IR,並為該型別產生捲尺。

Percolating metadata

部分後端需要有關程式庫的特殊中繼資料才能運作。 通常需要從程式庫依附元件樹狀結構的葉節點 (「基礎程式庫」) 開始,以疊代方式計算這項中繼資料,並將中繼資料滲透至根節點 (正在編譯的程式庫)。

舉例來說,[fidlgen_rust] 想知道某個型別是否可能包含浮點數,藉此判斷可安全衍生哪些特徵。不含任何浮點數的 struct 可能會有 Eq,其中沒有任何變體含有浮點數的 strict union 可能會有 Eq,但目前沒有浮點數欄位的 flexible table 不會有 Eq,因為提供這類欄位會違反來源相容性規則。

另一個例子來自 fidlgen_cpp,這個函式會產生非擁有網域物件。如果這些網域物件的內嵌部分是「值」(即非資源),即可安全複製。同樣地,計算這項中繼資料 (我們稱為「內嵌資源性」) 時,需要從葉節點到根節點,反覆計算這個值。

最近在討論為程式庫產生 ABI 指紋的新後端時,我們來回討論了這項功能應存在的位置。目前是基於實務考量,將這項功能託管在 fidlc 編譯器中,但這個答案並不令人滿意。

我們發現,需要中繼資料滲透的特徵不是暫緩處理、以變通方式解決 (通常是透過駭客手法),就是變成新的編譯器特徵,通常會過早強制執行一般化 (例如上述 ABI 指紋)。

此外,由於 Fuchsia FIDL 團隊可以輕鬆變更編譯器,因此我們在這方面比第三方更具優勢。因此,我們可以說,工具的狀態會損害我們的開放原始碼原則,因為該原則旨在讓所有後端處於公平的競爭環境。

每種目標語言和每個程式庫後端選項

FIDL 語言用於說明 Kernel API,且 Driver SDK 正在開發中,FIDL 的普及程度日益提高。

不過,在今天的工具鍊中,「FIDL 語言」和「後端」之間存在混淆,適合處理特定程式庫。當目標需要程式庫 fuchsia.fonts 的 Rust 程式碼時,我們會叫用 fidlgen_rust

這種做法過於簡單,無法說明特定程式庫需要專用後端。舉例來說,library zx; 是由 kazoo 處理。 這項每個程式庫的每個目標 fidlgen 選項,會進一步造成影響。以列舉 zx/clock 為例,我們的目標是有一天能產生目前手動編寫的 zx_clock_t typedef,以及各種 #define,實現列舉的成員。kazoo如果 fuchsia.fonts 程式庫依附於 zx/clock,這表示 fidlgen_cpp 需要瞭解 API 合約,才能正確產生連結程式碼,橋接其程式碼生成和 kazoo2

每個平台一個程式庫

目前,我們對多個同名 FIDL 程式庫的定義沒有意見。雖然不建議,但您可以在來源樹狀結構的各個位置定義多個程式庫 fuchsia.confusing,並獨立使用所有這些不同的程式庫。

利用平台 ID 概念會比較合理,在 Fuchsia 來源樹狀結構中,這項概念預設為 fuchsia。這樣一來,我們就能保證並強制執行,確保不會出現兩個名稱相似的程式庫定義。

考量到這項限制,我們將平台定義為共用相同平台 ID 的 FIDL 程式庫組合。

不得延遲驗證

目前後端無法選擇要成功或失敗的 FIDL 程式庫。後端應會處理任何有效的 JSON IR。這項限制表示我們不會在後端進行任何後期驗證。可以想像在後端新增驗證,以識別尚未實作的 FIDL 功能;另一個驗證範例是檢查 fidldoc 中的文件註解是否有效,並拒絕產生參考文件。(在這兩個範例中,預期都會發生正常降級。)

允許延後驗證會導致令人不快的遠端中斷 (例如 https://fxbug.dev/42144169):在 FIDL 程式庫以 SDK 成品形式提供,並整合至下游存放區的世界中,執行後端的開發人員可能與 FIDL 程式庫作者不同。因此,當修正問題的能力掌握在 FIDL 程式庫作者手中時,使用 FIDL 程式庫向開發人員提供警告或錯誤,充其量只會令人沮喪,最糟的情況是成為使用 FIDL 程式庫的阻礙因素。

因此,禁止延遲驗證的政策對 fidlc 編譯器維持了健康的壓力,要求「驗證所有項目」,並對後端施壓,要求「支援所有項目」。這在很大程度上避免了遠距離中斷故障,但代價是缺乏細微差別的位置 (「後端沒有驗證」)。

限制來源存取權

我們逐漸允許 JSON IR 複製部分 FIDL 來源,但並未多加思考長期後果。舉例來說,隨著更多複雜的運算式加入,我們公開了已解析的值,讓後端發出常數,同時保留 IR 中的運算式本身 (文字)。雖然運算式文字有助於在產生的程式碼中產生有意義的註解,但這會降低 SDK 發布者的隱私權,因為他們無法輕易選擇是否提供來源。

不難想像,如果繼續沿用這個路徑,會有更多 FIDL 來源進入 IR,這並非理想結果,因為這不僅會造成重複,還可能導致隱私權界線遭到侵犯。

我們設計 FIDL 工具鍊時,會盡量只納入必要的來源。對於少數需要存取來源的後端 (例如 fidl-lsp), 我們會依據參照範圍。詳情請參閱設計部分

縮放編譯

為求簡化,fidlc 編譯器最初的設計僅適用於來源,也就是 .fidl 檔案。如果程式庫有依附元件,編譯程式庫時需要連帶編譯所有依附元件。

舉例來說,編譯 fuchsia.fonts 程式庫時,我們也必須編譯 fuchsia.memfuchsia.intl 程式庫,依此類推。這表示今天的編譯完全沒有效率。fuchsia.mem 等核心程式庫會多次重新編譯。這種架構效率不彰的問題從未造成任何影響:SDK 中目前只有 64,000 多行 FIDL 來源,而且由於遞移依附元件相對較淺,因此這種效率不彰的情況並不明顯。

不過,在思考「理想」的 FIDL 工具鍊時,我們希望與編譯器設計的標準做法保持一致。傳統上,編譯器會接收來源檔案等輸入內容,並產生 x86 組語等輸出內容。隨著程式碼集不斷擴增,編譯器還必須能夠提供某種工作分割方式,這樣輸入內容的小幅更新就不需要重新編譯整個程式碼集。

舉例來說,假設您使用 javac 編譯器,並變更某個檔案 SomeCode.javafor 迴圈的條件,您應該不會希望為了再次執行程式,就必須重新編譯數千個檔案。您只需要重新編譯該單一檔案,並可重複使用所有其他預先編譯的來源 (做為 .class 檔案)。

如要順利分割工作,標準做法是定義編譯單元 (例如 FIDL 的程式庫),並產生中繼結果 (例如 JSON IR),這樣編譯程序的輸入內容就會是來源和直接依附元件的中繼結果。這樣一來,就能將總編譯時間 (假設為無限平行處理) 限制在最長的編譯依附元件鏈中。這也簡化了建構規則,這是du jour 的主題。

設計

我們將設計分為三個部分:

  1. 首先,指導原則可做為設計選擇的依據,並做為採用的方法和路徑的基礎;
  2. 標準 FIDL 工具鍊為例,說明如何分解建構 FIDL 的過程,以滿足所有需求;
  3. 最後,Fuchsia FIDL 團隊會進行一些特定清理作業,確保核心工具與本 RFC 的指引一致。

指導原則

IR 應能輕鬆支援常見後端

雖然複雜的後端應該可行 (例如整個程式檢視畫面),但 IR 的設計必須讓常見後端僅處理所處理程式庫的單一 IR,即可建構完成。

經驗顯示,大多數後端都相當簡單。我們致力於滿足簡單的用途 (而非專家用途),確保盡可能簡化 IR,並盡力確保後端生態系統蓬勃發展。

為說明這項原則,請考量在 fidlc 中完成的「型態形狀」計算。可以考慮改為將這項作業移至專用的「滲透」後端。不過,這會強制所有產生目標程式碼的後端 (主要用途) 同時依賴 IR 和這個「型別形狀」後端。

IR 應盡量減少

盡量減少依附元件是重要的因應措施,可輕鬆配合常見後端,因為「納入所有項目」並視為完成工作很容易 (或很誘人)。

為說明這項原則,請考慮目前在 fidlc 中計算「宣告順序」的反模式。只有少數後端依賴這個順序 (C 系列,甚至更少日常),而且這會為編譯器帶來不必要的複雜性。此外,這項規定也模糊了為何需要這類命令的理由,且經常造成混淆。此外,後端應獨立於核心編譯器演進,因此這種做法不夠彈性,阻礙了支援遞迴型別的進展

IR 不得包含來源

IR 不應包含任何來源,但必須嚴格遵守規定,以便輕鬆配合常見後端 (例如名稱)。IR 可能會視情況提供來源範圍參照。來源範圍參照是三元組:

(filename, start position, end position)

其中位置是元組 (line number, character number)

後端不應依賴存取來源來運作。如果後端必須存取來源才能運作 (例如 fidl-lsp),則必須清楚說明這項需求,並在無法存取來源時正常失敗。

選擇這種分解方式時,我們明確選擇讓 SDK 發布者 (即發布 FIDL 構件者) 決定是否要納入來源 FIDL。目前,由於部分來源最終會出現在 IR 中,因此這項選擇並非完全由他們決定。

除了名稱之外,IR 中還有一個值得注意的來源部分,那就是文件註解。這些註解依規格應屬於 API 的一部分,也就是 FIDL 程式庫作者明確選擇公開這些註解。此外,大多數後端都會使用這些說明文件註解 (例如在產生的程式碼中發出註解),因此符合輕鬆配合常見後端的原則。這些文件註解不會以原始來源的形式顯示在註解中,而是經過預先處理 (開頭縮排、/// 和左側空白字元會遭到修剪)。如簡要說明,我們打算在日後進一步處理文件註解。

後端一視同仁

FIDL 語言、以 fidlc 編譯器形式實作的 FIDL 語言,以及中介表示法的定義,都應設計成可支援包容性後端生態系統,讓所有後端 (無論是否建構為 Fuchsia 專案的一部分) 都能享有同等地位。

選擇這條分界線時,我們明確選擇避免為 Fuchsia FIDL 團隊擁有的後端短期需求提供便利性,而是專注於 FIDL 生態系統的長期可行性。

後端不會出錯

處理有效的 IR 時,後端必須成功。如果後端在環境中遇到問題 (例如檔案系統存取錯誤) 或 IR 無效,後端可能會失敗。如果後端無法處理符合 IR 結構定義的 IR,則不得因錯誤而失敗。

選擇這條分界線時,我們明確強制所有驗證都發生在前端,也就是驗證必須提升為 FIDL 語言限制。這項做法有兩個重要原因:

  1. 這項規則的推論是,只要有有效的 IR,就能使用與該 IR 相容的所有後端。也就是說,身為 SDK 發布商,只要確保 FIDL 程式庫編譯成功,所有使用與發布商相容的 FIDL 工具鍊版本的使用者,都能使用這些程式庫。
  2. 從語言設計的角度來看,這項嚴格要求是很有益的強制功能,可確保語言設計符合後端需求。舉例來說,如果後端擁有者基於某種原因需要驗證,就會提出這個問題 (透過 fidl-dev@fuchsia.dev 或 RFC),請 Fuchsia FIDL 團隊考慮將其納入語言規格。這可能促成語言改良,讓所有人都受益,或重新設計後端,以更符合 FIDL 工具鍊原則。

為說明這項原則,請考慮 Rust 中的特徵衍生:Eq 特徵無法為包含浮點數的型別衍生。您可能會想在包含浮點數 (float32float64) 的 FIDL 類型中新增屬性 @for_rust_has_floats,然後在 fidlgen_rust 中運用這項屬性,有條件地發出 Eq 特徵,並驗證屬性是否正確使用 (與值資源區別類似)。但這項誘惑違反了原則,因為這表示 fidlgen_rust 可能會出錯。在 fidlc 中驗證這類利基屬性也不理想,因為這會導致 FIDL 因各種目標語言的特定問題而變得複雜。3

標準 FIDL 工具鍊

標準 FIDL 工具鍊以程式庫分解為中心,並有兩種建構節點。

Percolating 建構節點

滲透節點會將程式庫的來源和程式庫直接依附元件的物件檔案提供給工具,並產生最終結果和目標物件檔案。

可滲透節點

舉例來說,目前大多數 fidlgen 後端都遵循這個模式:來源是 JSON IR,最終結果是產生的程式碼。這些工具沒有相依的物件檔案 (DOF),也不會產生目標物件檔案 (TOF)。

另一個例子是預計推出的 ABI 指紋工具,這項工具需要計算型別的結構屬性。這項工具會使用 JSON IR (來源),並產生 ABI 摘要 (最終結果) 和隨附的目標物件檔案 (TOF)。對有依附元件的程式庫進行作業時,系統會連同 JSON IR 一併使用這些程式庫的 TOF (即 DOF),產生下一個最終結果。最終結果和 TOF 可能只有格式不同,因為前者是供人閱讀,後者則供工具剖析。

建構整個檢視區塊的節點

系統會為整個檢視區塊節點提供來源,包括所有可遞移存取的依附程式庫,並產生最終結果。

全景檢視節點

舉例來說,measure-tape 需要所有可遞移存取的程式庫的 IR,才能定義要編譯的型別,自然會以整個檢視區塊節點表示。目前 fidlc 節點會以整個檢視區塊節點的形式運作,因為需要存取所有來源才能運作 (詳情請參閱縮放編譯)。fidlcatfidldoc 都需要完整檢視畫面,並依附於編譯的整個 Fuchsia 平台

雖然整個檢視區塊節點的效率確實不如滲透節點,但我們可能不想重組所有工具,改為以滲透方式運作,而是選擇將部分複雜性推送到建構系統。

在 Fuchsia 來源樹狀結構建構作業中,我們會產生 all_fidl_json.txt 檔案。清楚瞭解整個檢視區塊節點的需求後,我們就能更妥善地建構這個匯總。舉例來說,如果依平台整理這個匯總資料,並記錄每個程式庫的來源、JSON IR 和直接依附元件,我們就能輕鬆運用這個匯總資料,快速產生整個檢視畫面工具所需的輸入內容。開發人員工具 (例如 fidl-lspfidlbolt) 也會運用這項匯總資料。

選取工具

特定建構節點中的工具選取作業,應取決於目標 (例如「產生低階 C++ 程式碼」),以及編譯的程式庫 (例如「程式庫 zx」)。我們定義的總函式會採用元組 (目標產生器、程式庫),並傳回工具 (例如 kazoofidlgen_cpp),做為工具鍊的全域設定。

舉例來說,在 Fuchsia 來源樹狀結構中,我們預期會有下列設定:

(*, library zx)  kazoo
(low_level_cpp, not library zx)  fidlgen_llcpp
(high_level_cpp, not library zx)  fidlgen_hlcpp
(rust, not library zx)  fidlgen_rust
(docs, *)  fidldoc

使用統一 C++ 繫結時,這項設定會變更為:

(*, library zx)  kazoo
(cpp, not library zx)  fidlgen_cpp
(rust, not library zx)  fidlgen_rust
(docs, *)  fidldoc

對遞增編譯的影響

查看增量編譯時 (也就是結合現有編譯構件與新編譯構件,盡可能減少因應來源變更而執行的工作),本文所述的兩種節點表現差異很大。

一般來說,當編譯圖中的一或多個來源 (也稱為「來源集」) 變更時,就需要叫用節點。

相較於整個檢視區塊節點,Percolating 節點的來源集小得多,來源集是直接「來源」和目標物件檔案 (TOF)。也就是說,如果使用節點的遞移行為,來源變更會傳播至 TOF 變更,進而變更依附遞移節點的來源集。舉例來說,假設 fidlgen_rust 後端經過擴增,也會產生 TOF fuchsia.some.library.fidlgen_rust.tof。如果變更某個程式庫,且該程式庫的 TOF 也隨之變更,則所有相依程式庫也必須變更,進而導致更多對 fidlgen_rust 後端的呼叫 (依此類推)。

相較於 percolating 節點,整個檢視區塊節點的來源集更廣泛。 全檢視節點大致可分為兩類:一類取決於所有遞移依附元件 (例如 measure-tape),另一類取決於平台中的所有程式庫 (例如 fidldoc)。因此,任何變更都可能導致需要叫用這些節點。

整個檢視區塊節點的增量編譯成本是雙重負擔,因為這些節點需要更常執行,而且由於會查看更多來源,因此需要完成更多工作。只要稍加努力 (有人曾說「這只是簡單的程式設計問題」),任何需要完整檢視畫面的後端都能演變為可滲透的節點。考慮這類演進的健康壓力 (通常會帶來許多複雜度和維護負擔),是考慮增量編譯的速度優勢,並在成本難以承受時踏上這條路。

除了工具鍊本身的累加編譯成本外,也必須考量後續影響。由於大部分工具鍊都會產生原始碼 (例如 C++、Rust、Dart),因此往往會更接近整體建構圖的根目錄,這樣一來,工具鍊輸出內容的任何變更 (例如產生的 C++ 標頭變更) 都會對下游編譯作業造成重大影響 (例如直接或遞移依附於這個產生的 C++ 標頭的所有程式碼)。因此,應盡量減少對產生的來源進行變更。舉例來說,您可以將產生器的輸出內容標準化 (避免變更無意義的空白字元),或將輸出內容與快取版本進行比較,避免以相同內容覆寫內容,進而避免僅變更時間戳記 (詳情請參閱 GN 輸出內容範例)。

清除舊技術債,並避免產生更多技術債

我們會遵循本文所述原則,將 C 繫結和程式碼表生成作業移出 fidlc。由於歷史建構複雜性,這兩代都嵌入了核心編譯器。

我們也計畫從 IR 中移除「宣告順序」,而是將任何特殊排序推送至特定後端。

擴大編譯規模所述,FIDL 編譯器 fidlc 會演進為劃分工作,只要求直接依附元件的輸出內容 (可能是 JSON IR 本身),而不是所有遞移依附元件的來源。

最後,我們會避免累積更多技術債,並專注於讓工作與本文所述方向一致。舉例來說,下一個考慮使用的後端 ABI 指紋辨識,將是滲透型後端,而不是嵌入核心編譯器。

先前技術

請與 C++ 編譯進行比較/對比:C++ 編譯器通常會接收一個 C++ 來源檔案,並產生一個物件檔案。在稱為「連結」的最後一個程式組裝階段,連結器會將所有物件檔案合併為一個二進位檔。這種做法之所以可行,是因為在單獨編譯一個 C++ 來源檔案時,編譯器會透過標頭,查看目前檔案所依附的其他 C++ 來源檔案中的外部函式宣告。同樣地,目前的 JSON IR 提供的外部程式庫型別資訊極少,類似於函式宣告。

不過,如果需要更深入的優化,這個 C++ 編譯模型的效果就不太理想:編譯器只能查看宣告時,必須非常保守地看待函式的實際行為 (例如,函式是否一律會終止?是否會變動指標 X?是否會保留指標 Y,因此允許指標逸出?)。同樣地,在 FIDL 中,如果程式碼產生後端能夠進一步瞭解參照的外來型別,或許就能產生更簡潔且更優質的程式碼。就資源性和來源相容性而言,我們的要求會導致後端無法產生正確的程式碼,除非後端知道所有參照的外來型別的資源性。

為解決這個問題,C++ 中各種編譯器實作項目開始將越來越多的輔助資料注入物件檔案。舉例來說,GCC 和 Clang 都開發了自己的可序列化 IR 格式,可更詳細地表達這些 C++ 函式的行為,並與組語一起封裝。連結器會同時使用組語和 IR,並產生更優質的程式碼 (稱為連結時間最佳化)。在 FIDL 中,由於各種後端可能需要不同的外來型別知識,因此將「輔助資料」與「物件檔案」分離可能很有利,也就是在主要 JSON IR 旁邊產生後端專屬的 Sidecar。許多後端都希望資源性是常見的屬性,但以 LLCPP 為例,未來在產生程式碼來關閉控制代碼時,最好也能知道型別是否遞移包含行外物件 (編碼和解碼也是如此);Rust 則希望判斷型別是否遞移包含浮點數,以便在更多情況下衍生 Eq (但需要編譯器保證,才能避免來源相容性問題)。

說明文件

這項 RFC 是改善 FIDL 工具鍊說明文件的基礎,建議工具鍊作者妥善記錄他們提供的建構規則。

實作

如附註所述。

效能

不會影響效能,這份 RFC 說明瞭需求和問題分解,雖然不夠乾淨,但已達成。

人體工學

不支援這項操作。

回溯相容性

不支援這項操作。

安全性考量

沒有安全考量。

隱私權注意事項

由於來源與 IR 分離更清楚,因此隱私權獲得提升。

測試

工具鍊的標準測試。

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

如內文所述。


  1. 從技術上來說,我們將FIDL 繫結稱為程式碼生成工具、使用生成程式碼所需的支援執行階段程式庫,以及生成的程式碼。 

  2. C 系列 fidlgen 不會想為 zx/clock 產生自己的網域物件,而是選擇 #include kazoo 產生的標頭。同樣地,Rust fidlgen 會匯入 kazoo 產生的 zx 繫結,而不是根據 zx 程式庫定義產生自己的網域物件。 

  3. 目前我們無法證明在 FIDL 中新增 @has_floats 屬性 (或 has_float 修飾符) 的必要性,因為唯一的使用案例是在 fidlgen_rust 中,而且即使在該處也不是重大問題。如果這些項目有所變更 (例如其他幾個後端都有類似的 PartialEq/Eq 問題),或許就合理。