本指南說明如何變更跨越 Zircon 系統呼叫邊界的 struct,而不破壞現有二進位檔。這項程序在過去稱為「結構版本管理」或「結構演進」。目標對象是核心開發人員,以及任何修改 Zircon 系統介面的人員或代理程式。
如果您要新增、移除、重新排序、調整大小、重新命名或變更 //zircon/system/public/zircon/ 下宣告的任何 struct 欄位 (或鏡像至 //sdk/rust/zx-types),請先閱讀本指南。
如果結構體僅由加註 @next 的系統呼叫使用 (在 zircon/syscalls-next.h 中宣告),則不在本指南的討論範圍內。這些系統呼叫不屬於穩定 ABI,因此其結構遵循不同的規則。
為什麼無法就地變更結構體
syscall ABI 是核心與每個針對支援的 API 級別建構的二進位檔之間的合約,包括未與核心一起重建的預先建構二進位檔 (例如 SDK 使用者、預先建構套件和 CTF 測試)。這些二進位檔內建每個 struct 的大小、版面配置和意義。
核心只需要支援針對支援的 API 級別建構的二進位檔。與平台其他部分一樣,核心可以在 API 級別停用後,停止支援該級別。這會造成兩種影響:
- API 級別淘汰後,核心可以停止支援僅限指定該級別的二進位檔使用的 struct 版本。請參閱「移除舊版本」。
- 目前的標頭會使用
availability.h中的 API 級別巨集 (例如ZX_AVAILABLE_SINCE和FUCHSIA_API_LEVEL_AT_LEAST),視需要描述每個支援的 API 級別。您不必深入研究標頭的歷史記錄,或考慮已淘汰的 API 級別,就能找出核心必須支援的 struct 版本。
如要進一步瞭解 API 級別和適用的相容性保證,請參閱「RFC-0239:平台版本管理實務」和「Fuchsia 平台 API 演進指南」。
以下任何變更都會在不發出通知的情況下,破壞現有二進位檔:
- 新增欄位,即使是在結尾處。舉例來說,如果是
zx_object_get_info(),核心會複製sizeof(struct)個位元組,並在呼叫端的緩衝區較小時傳回ZX_ERR_BUFFER_TOO_SMALL。如果舊二進位檔傳遞的緩衝區大小適用於舊結構體,就會開始失敗。 - 移除、重新排序或調整欄位大小。舊二進位檔讀取或寫入錯誤的位元組。
- 變更現有欄位的意義或單位。版面配置不變,但舊二進位檔會誤解該值。
重新命名欄位而不變更其版面配置或含義,可與 ABI 相容,但會破壞來源相容性。請參閱「來源相容性」。
設計原理
版本管理配置遵循下列原則:
- 保持呼叫端簡單。核心會負責支援多個版本。呼叫端不需要程式庫,就能編碼、解碼或轉譯結構體 (就像使用 protobuf 樣式的編碼一樣)。每種支援的語言都可以透過其原生 FFI 使用純 C 結構版面配置。
- 維持成效:如果單一 syscall 目前會取得或設定一組相關值,新版本仍須允許在單一 syscall 中執行這項操作。
- 允許柔和轉場。介面會逐步演進:新增版本與舊版本並存、遷移呼叫端,然後移除舊版本。
考慮過的替代方案
- 只能在 struct 結尾附加新欄位。如果 syscall 讀取或寫入 struct 陣列 (例如
ZX_INFO_PROCESS_MAPS或ZX_INFO_PROCESS_VMOS),則不適用這種做法,因為元素步幅會改變。此外,這也會強制將新欄位放在可能浪費對齊邊框空間的位置。有了版本化結構體,每個新版本都能將欄位放在邏輯上所屬的位置。 - 在每個 struct 中新增
version欄位。這對 struct 陣列來說很麻煩,且需要更多核心工作才能輸出 struct。由於幾乎所有採用或傳回結構體的系統呼叫都已有topic或options引數,因此版本會改為在該處編碼。請勿在 syscall 結構中新增版本欄位。
版本管理配置
請勿直接修改 struct,而是導入新版 struct,並繼續提供舊版,直到沒有二進位檔依賴舊版為止:
- 使用版本化名稱 (例如
zx_info_vmo_v3_t) 建立現有結構的快照,並在核心中繼續支援該結構。 - 將未版本化的名稱 (
zx_info_vmo_t) 參照至新版面配置。 - 當所有呼叫端都已針對新版本重新編譯,且沒有針對支援的 API 級別建構的二進位檔使用舊版本,請刪除舊版本。
只有在呼叫端能告知核心其使用的版本時,這個配置才有效。每個支援版本管理功能的系統呼叫,其引數中都有版本管道:
| 系統呼叫 | 版本管道 | 編碼 | 範例 |
|---|---|---|---|
zx_object_get_info() |
topic 引數 |
主題的前 4 個位元,使用 object.h 中的 __ZX_INFO_TOPIC(topic, version) |
ZX_INFO_VMO_V3 = __ZX_INFO_TOPIC(23u, 2),ZX_INFO_VMO = __ZX_INFO_TOPIC(23u, 3) |
zx_clock_create()、zx_clock_get_details()、zx_clock_update() |
options 引數 |
clock.h 中的 ZX_CLOCK_ARGS_VERSION(n) (第 58 位元的 6 個位元) |
zx_clock_update_args_v1_t和zx_clock_update_args_v2_t |
zx_job_set_policy() |
topic 引數 |
policy.h 中的不重複主題常數 |
ZX_JOB_POL_BASIC_V1和ZX_JOB_POL_BASIC_V2 |
__ZX_INFO_TOPIC 編碼有 4 位元的版本,因此單一 zx_object_get_info() 主題在生命週期內最多可有 16 個版本。
沒有版本管道的系統呼叫
有幾個系統呼叫會採用或傳回結構,但沒有版本管道:
- 結構體視為最終版本的系統呼叫。例如,
zx_object_wait_many()會採用zx_wait_item_t陣列。struct 已包含等待信號所需的所有內容,因此變更會暗示不同的作業 (例如等待信號以外的內容)。在這種情況下,請新增 syscall。 - 結構格式由其他系統呼叫決定的系統呼叫。舉例來說,
zx_port_queue()和zx_port_wait()沒有版本管道,但會交換zx_port_packet_t(標記聯集)。相同大小的新封包類型可以新增至聯集,而zx_port_create()的options引數可以為通訊埠選取不同的封包格式。
否則,如果使用或產生 struct 的 syscall 沒有主題、選項位元或其他方式可讓呼叫端選取版本,就無法使用這個配置。您可以選擇新增系統呼叫,或是在新的系統呼叫中新增版本管道,並將呼叫端遷移至該管道。無論選擇哪一種,都會變更 Zircon 系統介面,請參閱「程序規定」。
設計會採用或傳回 struct 的新 syscall 時,請從一開始就加入版本管道 (例如 options 中的保留版本位元),以便日後更新 struct。系統 API 評分標準要求這麼做。
命名慣例
請遵循 object.h 中的現有慣例:
- 未版本化的名稱 (
zx_info_vmo_t、ZX_INFO_VMO) 一律參照最新版本。大多數程式碼都應使用這些。 - 舊版的 struct 會加上
_vN後置字串 (zx_info_vmo_v1_t、zx_info_vmo_v2_t、...),主題或常數則會加上_VN後置字串 (ZX_INFO_VMO_V1、...)。 - 變更的巢狀結構會與其父項一起建立版本 (例如
zx_info_maps_v2_t包含zx_info_maps_mapping_v2_t)。請參閱「巢狀和共用結構」。 - 版本號碼一律不會重複使用。移除舊版本後,無版本主題會保留其版本位元。例如,即使
ZX_INFO_PROCESS_V1已移除,ZX_INFO_PROCESS仍為__ZX_INFO_TOPIC(3u, 1u)。
巢狀和共用結構體
版本號碼會識別核心針對該呼叫讀取或寫入的所有內容,而不只是最外層的 struct。請將版本管道選取的 struct 視為「根」。根目錄版本涵蓋的 ABI 包括:
- 根目錄依值遞迴嵌入的每個 struct、陣列和 union。
- 核心透過根指標讀取或寫入的任何資料。
因此,當您變更 struct 時,版本升級會向上傳播到每個可觸及的根:
- 依值嵌入:如果巢狀結構的大小變更,則包含該結構的所有結構版面配置都會變更,這些結構的任何陣列步幅也會變更。即使大小不變,舊二進位檔也會誤讀含義已變更的欄位。
- 透過指標存取:外部 struct 的版面配置不會變更,但舊二進位檔仍會傳遞或預期指向資料的舊格式。核心只能依據根目錄的版本區分格式,因此根目錄需要新版本。
由於核心會繼續提供舊根,因此變更後的結構體和根之間的每個結構體也需要舊副本。舉例來說,當 zx_arm64_exc_data_t 成長時,舊版面配置會保留為 ZX_INFO_THREAD_EXCEPTION_REPORT_V1 下提供的版本化結構鏈:
typedef struct zx_exception_report_v1 {
zx_exception_header_t header;
zx_exception_context_v1_t context; // contains zx_arm64_exc_data_v1_t
} zx_exception_report_v1_t;
版本管理只會向上傳播至根目錄。變更外部 struct 時,不需要為其包含的 struct 建立版本。
共用結構體
由於未版本化的名稱一律參照最新版本,因此變更未版本化的結構體會變更「每個」嵌入該結構體的結構體,包括您不打算變更的系統呼叫所使用的結構體。如果其中任何系統呼叫未取得版本更新,就會導致 ABI 中斷。
舉例來說,zx_exception_context_t 會透過多個路徑到達核心:
- 在
zx_exception_report_t內,由zx_object_get_info()傳回ZX_INFO_THREAD_EXCEPTION_REPORT。 - 做為
zx_thread_raise_exception()的context引數。 - 在
zx_restricted_exception_t內 (透過zx_exception_report_t),嚴格篩選模式寫入時不含任何版本管道。這個結構體目前之所以豁免,只是因為它是在不穩定的zircon/syscalls-next.h中宣告。
變更結構前,請找出可從中連到的每個根:每個 zx_object_get_info() 主題、透過 options 版本控制的每個引數,以及核心遵循的每個指標。針對包含已變更 struct 的每個 struct,執行下列其中一項操作:
- 也為其建立版本,並與根層級代理一起建立。這需要根目錄具有版本管道。
- 將其固定在舊版本,方法是將其變更為明確嵌入
_vNstruct,使其版面配置保持不變。
如果任何受影響的根目錄沒有版本管道,請參閱「沒有版本管道的 Syscall」。
結構體版面配置規則
新的 struct 版本必須遵循 syscall struct 的一般規則:
- 使用固定寬度類型 (
uint32_t、uint64_t、zx_koid_t等)。 - 使用
uint8_t padding1[4];等具名欄位明確指定所有邊框間距。請勿依賴隱含編譯器邊框間距。 - 建議使用
uint32_t或uint64_t中的位元旗標,而非bool欄位。 - 記錄每個欄位,包括是否僅在某些情況下有效。
逐步說明:新增版本
下列步驟說明近期變更所使用的常見單一 CL 方法,例如導入目前 ZX_INFO_KMEM_STATS (RFC-0262) 的變更,以及 RFC-0254 的 zx_info_vmo_t、zx_info_maps_t 和 zx_info_task_stats_t 版本。
1. 更新 C 標頭
在 //zircon/system/public/zircon/syscalls/ 下方的適當標頭中:
- 將目前的結構定義逐字複製到具有下一個
_vN名稱的新結構 (例如zx_info_kmem_stats_v1_t)。版面配置必須與現有結構完全相同。 - 為該舊結構體新增
_VN主題或常數,並使用無版本主題的現有數值。舊二進位檔會繼續傳送這個值。 - 將未建立版本的 struct (
zx_info_kmem_stats_t) 修改為新的版面配置。 - 提高無版本主題的版本位元 (例如從
__ZX_INFO_TOPIC(17u, 0)提高至__ZX_INFO_TOPIC(17u, 1))。 - 如果變更的 struct 嵌入其他 syscall struct 或由其他 syscall struct 指向,請按照「巢狀和共用 struct」一節所述,為每個 struct 及其根目錄建立版本或固定。
舉例來說,ZX_INFO_KMEM_STATS 的版本編號如下:
-#define ZX_INFO_KMEM_STATS ((zx_object_info_topic_t) 17u) // zx_info_kmem_stats_t[1]
+#define ZX_INFO_KMEM_STATS __ZX_INFO_TOPIC(17u, 1) // zx_info_kmem_stats_t[1]
+#define ZX_INFO_KMEM_STATS_V1 __ZX_INFO_TOPIC(17u, 0) // zx_info_kmem_stats_v1_t[1]
2. 更新核心
核心必須接受所有支援的版本。
- 輸出結構體 (例如
zx_object_get_info()主題):先以最新的內部形式收集資訊,然後轉換為要求的版本,再複製出來。請參閱object_info.rs中ZX_INFO_VMO_V1、ZX_INFO_VMO_V2和ZX_INFO_VMO_V3的處理方式,以及memory_stats.rs中的vmo_info_to_version和MapsInfoVersion輔助程式。 - 輸入結構體 (例如
zx_clock_update()引數或zx_job_set_policy()政策):從版本管道解碼版本、驗證「該」版本的緩衝區空間,並將舊版本轉譯為內部表示法,並為新欄位定義明確的預設值。使用ZX_ERR_INVALID_ARGS拒絕不明版本。
3. 更新繫結、說明文件和驗證工具
在樹狀結構中搜尋現有的主題和結構名稱,並更新所有對應的位置。通常包括:
//sdk/rust/zx-types/src/lib.rs:新增_VN主題和_vN_tstruct、更新未版本化的 struct,並確保每個版本的const_assert_eq!大小和對齊檢查準確無誤。//sdk/rust/zx和//zircon/system/ulib/zx:更新公開結構的高階包裝函式。//zircon/vdso/*.fidl:更新文件註解中的 syscall 參考文件,包括舊版說明文件。//zircon/kernel/tests/abi_type_validator.cc:為新結構體新增大小、對齊和欄位偏移檢查,並保留舊版本的檢查。- 解碼 syscall 結構的工具 (如果參照主題),例如
//tools/fidlcat、//src/lib/fidl_codec和//src/lib/zxdump。 //zircon/public/sysroot_sdk/sysroot.api:建構作業失敗,並顯示在公開 sysroot 標頭變更時更新這個檔案的操作說明。
4. 新增測試
新增或更新 Zircon 核心測試 (例如在 //zircon/system/utest/core/ 下),測試「每個」支援的版本,而不只是最新版本。舉例來說,請參閱 vmo.cc 如何查詢 ZX_INFO_VMO_V1、ZX_INFO_VMO_V2、ZX_INFO_VMO_V3 和 ZX_INFO_VMO,並檢查共用欄位是否一致。
使用 fx core-tests 執行核心測試,並使用 fx build 建構。
來源相容性
上述配置可為所有現有二進位檔保留 ABI 相容性。來源相容性取決於變更類型:
- 僅新增欄位:使用未版本化名稱的程式碼會在重新編譯時採用新版本,且通常會繼續編譯。如果程式碼仰賴確切大小或每個欄位的匯總初始化,可能需要更新。
- 移除或重新命名欄位:這會破壞使用這些欄位的程式碼來源相容性。在同一個 CL 中更新所有樹狀結構內使用者。樹狀結構外使用者可以固定舊的
_vN名稱,直到遷移為止。
如果來源破壞性變更會影響許多樹狀結構外程式碼,請改用 RFC-0074 中所述、以別名為基礎的軟轉換:
- 同時導入
_V1和_V2,並將未版本化的名稱暫時別名為_V1。 - 將所有呼叫端遷移至
_V2。 - 將未建立版本的別名重新指向
_V2。 - 將呼叫端遷移回未版本化的名稱。
- 移除
_V1。
移除舊版本
只有在沒有任何二進位檔 (針對支援的 API 級別建構) 仍可使用舊版本時,才能移除舊版本。指定已淘汰 API 級別的二進位檔則不需考慮。移除前,請確認下列事項:
- 沒有任何樹狀結構內程式碼參照
_vN_t結構體或_VN常數。 - 沒有預先建構的二進位檔、支援 API 級別的 SDK 使用者,或 CTF 測試依附於此。如有疑問,請洽詢 Zircon 核心擁有者。
移除 CL 應從 C 標頭、核心處理、Rust 繫結、ABI 驗證工具項目、工具解碼器、vDSO 說明文件和該版本的測試中,刪除舊的 struct 和常數。舉例來說,請參閱移除 ZX_INFO_PROCESS_V1 的 CL (修訂版本 934bcf504d3)。
請勿重新編號其餘版本,也不要重複使用已移除的版本編號。
程序需求
- 變更 syscall 結構體會影響 Zircon 系統介面,因此需要 API Council 成員的
API-Review+1。 - 新增或移除 Zircon 系統介面,或大幅變更其語意,可能需要 RFC。請參閱「RFC-0006:Zircon RFC 程序附加條款」。
- 在所有版本中變更現有欄位的含義是例外狀況,需要明確理由和 API 審查,如 RFC-0254 所示。
既有技術
以下 RFC 採用這項配置:
- RFC-0074:公開程序啟動時間 (
zx_info_process_v2_t) - RFC-0084:更多個別工作執行階段資訊 (
zx_info_task_runtime_v1_t) - RFC-0254:變更寫入時複製頁面的歸因 (
zx_info_vmo_t、zx_info_maps_t、zx_info_task_stats_t) - RFC-0262:展開 kmem 統計資料 (
zx_info_kmem_stats_v1_t)
如要瞭解新增系統呼叫的機制,請參閱「新增系統呼叫」。