實作版本支援

在執行階段和外掛程式中實作版本支援的說明。

本主題說明如何在新的執行階段和產生器中實作版本。

概觀

2023 版本

發布的第一個版本是 2023 版本,其設計旨在統一 proto2 和 proto3 語法。我們為了涵蓋行為差異而新增的功能詳述於版本的特性設定中。

功能定義

除了支援版本和我們定義的全域功能之外,您可能還想定義自己的功能來利用基礎架構。這可讓您定義任意功能,您的產生器和執行階段可以使用這些功能來閘控新的行為。第一步是針對 descriptor.proto 中 FeatureSet 訊息宣告 9999 以上的擴充號碼。您可以在 GitHub 上傳送提取請求給我們,它將會包含在我們的下一個版本中 (例如,請參閱 #15439)。

一旦您有了擴充號碼,您可以建立您的功能 proto (類似於 cpp_features.proto)。這些通常會看起來像這樣

edition = "2023";

package foo;

import "google/protobuf/descriptor.proto";

extend google.protobuf.FeatureSet {
  MyFeatures features = <extension #>;
}

message MyFeatures {
  enum FeatureValue {
    FEATURE_VALUE_UNKNOWN = 0;
    VALUE1 = 1;
    VALUE2 = 2;
  }

  FeatureValue feature_value = 1 [
    targets = TARGET_TYPE_FIELD,
    targets = TARGET_TYPE_FILE,
    feature_support = {
      edition_introduced: EDITION_2023,
      edition_deprecated: EDITION_2024,
      deprecation_warning: "Feature will be removed in 2025",
      edition_removed: EDITION_2025,
    },
    edition_defaults = { edition: EDITION_LEGACY, value: "VALUE1" },
    edition_defaults = { edition: EDITION_2024, value: "VALUE2" }
  ];
}

在這裡,我們定義了一個新的列舉功能 foo.feature_value (目前僅支援布林和列舉型別)。除了定義它可以採用的值之外,您還需要指定如何使用它

  • 目標 - 指定此功能可以附加到的 proto 描述符的型別。這控制使用者可以在何處明確指定此功能。每個型別都必須明確列出。
  • 功能支援 - 指定此功能相對於版本的生命週期。您必須指定它引入的版本,而且在此之前不允許使用它。您可以選擇在後續版本中將該功能標示為已淘汰或移除。
  • 版本預設值 - 指定對功能預設值的任何變更。這必須涵蓋每個支援的版本,但您可以省略預設值未變更的任何版本。請注意,可以在這裡指定 EDITION_PROTO2EDITION_PROTO3,以提供「舊版」版本的預設值 (請參閱 舊版版本)。

什麼是功能?

功能旨在提供一種機制,以在版本邊界上逐漸減少不良行為。雖然實際移除功能的時程可能在未來數年 (或數十年),但任何功能期望的目標都應該是最終移除。當發現不良行為時,您可以引入一個新的功能來保護修復。在下一個版本 (或可能更晚) 中,您會翻轉預設值,同時仍然允許使用者在升級時保留其舊行為。在未來的某個時間點,您會將該功能標示為已淘汰,這會對任何覆寫它的使用者觸發自訂警告。在後續版本中,您會將其標示為已移除,防止使用者再覆寫它 (但預設值仍然會套用)。在破壞性版本中放棄對最後一個版本的支援之前,該功能將仍可供舊版版本上停滯的 proto 使用,給予它們遷移時間。

您無意移除的可選行為的控制旗標,最好實作為自訂選項。這與我們將功能限制為布林或列舉型別的原因有關。由 (相對) 無界限數量的值控制的任何行為,可能不適合版本框架,因為最終減少這麼多不同行為是不切實際的。

其中一個注意事項是與連線邊界相關的行為。使用語言特定的功能來控制序列化或剖析行為可能會很危險,因為任何其他語言都可能在另一端。連線格式變更應始終由 descriptor.proto 中的全域功能控制,每個執行階段都可以統一地遵守這些功能。

產生器

以 C++ 撰寫的產生器可以免費獲得許多好處,因為它們使用 C++ 執行階段。它們不需要自己處理功能解析,而且如果它們需要任何功能擴充,它們可以在其 CodeGenerator 中的 GetFeatureExtensions 中註冊它們。它們通常可以使用 GetResolvedSourceFeatures 來存取程式碼產生中描述符的已解析功能,並使用 GetUnresolvedSourceFeatures 來存取它們自己的未解析功能。

以與產生程式碼的執行階段相同語言撰寫的外掛程式,可能需要針對其功能定義進行一些自訂的引導。

明確支援

產生器必須明確指定它們支援的版本。這可讓您在版本發布後,按照自己的排程安全地新增對版本的支援。Protoc 會拒絕傳送給 CodeGeneratorResponsesupported_features 欄位中不包含 FEATURE_SUPPORTS_EDITIONS 的產生器的任何版本 proto。此外,我們有 minimum_editionmaximum_edition 欄位,可指定您的精確支援視窗。一旦您為新版本定義了所有程式碼和功能變更,您就可以提高 maximum_edition 來宣告此支援。

程式碼產生測試

我們有一組程式碼產生測試,可用於鎖定 2023 版本不會產生任何非預期的功能變更。這些在 C++ 和 Java 等語言中非常有用,其中大部分功能都在程式碼產生中。另一方面,在 Python 等語言中,程式碼產生基本上只是一個序列化描述符的集合,這些程式碼產生測試不太有用。

此基礎架構尚未可重複使用,但計劃在未來版本中推出。屆時,您可以使用它們來驗證遷移到版本是否沒有任何非預期的程式碼產生變更。

執行階段

沒有反射或動態訊息的執行階段不需要執行任何操作來實作版本。所有這些邏輯都應該由程式碼產生器處理。

具有反射但沒有動態訊息的語言需要解析的功能,但也可以選擇僅在其產生器中處理它。這可以透過在程式碼產生期間將已解析和未解析功能集都傳遞給執行階段來完成。這避免在執行階段重新實作功能解析,主要缺點是效率,因為它會為每個描述符建立一個獨特的功能集。

具有動態訊息的語言必須完整實作版本,因為它們需要在執行階段能夠建置描述符。

語法反射

在具有反射的執行階段中實作版本的第一步是移除對 syntax 關鍵字的所有直接檢查。所有這些都應該移到更精細的功能協助程式,如果需要,這些協助程式可以繼續使用 syntax

以下功能協助程式應該在描述符上實作,並使用適合語言的命名

  • FieldDescriptor::has_presence - 欄位是否具有明確的存在
    • 重複欄位永遠沒有存在
    • 訊息、擴充和 oneof 欄位總是具有明確的存在
    • 其他所有內容只有在 field_presence 不是 IMPLICIT 時才存在
  • FieldDescriptor::is_required - 欄位是否為必要
  • FieldDescriptor::requires_utf8_validation - 是否應檢查欄位是否為 utf8 有效性
  • FieldDescriptor::is_packed - 重複欄位是否具有壓縮編碼
  • FieldDescriptor::is_delimited - 判斷訊息欄位是否具有分隔編碼
  • EnumDescriptor::is_closed - 判斷欄位是否為封閉式

注意:在大多數語言中,訊息編碼功能目前仍然由 TYPE_GROUP 發出訊號,並且 required 欄位仍然設定為 LABEL_REQUIRED。這並非理想的做法,這樣做是為了簡化下游的遷移。最終,這些應遷移到適當的 helper 並改為使用 TYPE_MESSAGE/LABEL_OPTIONAL

下游使用者應遷移到這些新的 helper,而不是直接使用語法。以下現有的描述器 API 類別應該理想地被棄用並最終移除,因為它們洩漏了語法資訊

  • FileDescriptor 語法
  • Proto3 可選 API
    • FieldDescriptor::has_optional_keyword
    • OneofDescriptor::is_synthetic
    • Descriptor::*real_oneof* - 應該重新命名為「oneof」,並且現有的「oneof」helper 應該移除,因為它們洩漏了關於合成 oneof 的資訊(在版本中不存在)。
  • Group 類型
    • 應該移除 TYPE_GROUP 列舉值,並以 is_delimited helper 取代。
  • Required 標籤
    • 應該移除 LABEL_REQUIRED 列舉值,並以 is_required helper 取代。

在許多使用者程式碼類別中,這些檢查確實存在,但並非對版本不友善。例如,由於其合成 oneof 實作,需要特殊處理 proto3 optional 的程式碼,只要極性類似於 syntax == "proto3" (而不是檢查 syntax != "proto2"),就不會對版本不友善。

如果無法完全移除這些 API,則應將其棄用並加以勸阻。

功能可見性

editions-feature-visibility 中所述,功能 proto 應保留為任何 Protobuf 實作的內部細節。它們控制的行為應透過描述器方法公開,但不應公開 proto 本身。值得注意的是,這表示任何公開給使用者的選項都需要將其 features 欄位移除。

我們允許功能洩漏的唯一情況是序列化描述器時。產生的描述器 proto 應該是原始 proto 檔案的忠實表示,並且應該包含選項內未解析的功能

舊版

legacy-syntax-editions 中更詳細討論的,取得版本實作早期涵蓋範圍的好方法是統一 proto2、proto3 和版本。這實際上是在底層將 proto2 和 proto3 遷移到版本,並使 語法反射 中實作的所有 helper 專門使用功能(而不是分支於語法)。這可以透過在 功能解析 中插入功能推斷階段來完成,其中 proto 檔案的各種方面可以告知哪些功能適合。然後,這些功能可以合併到父層的功能中,以取得已解析的功能集。

雖然我們已經為 proto2/proto3 提供了合理的預設值,但對於 2023 版,需要以下額外的推斷

  • required - 當欄位具有 LABEL_REQUIRED 時,我們推斷 LEGACY_REQUIRED 存在
  • groups - 當欄位具有 TYPE_GROUP 時,我們推斷 DELIMITED 訊息編碼
  • packed - 當 packed 選項為 true 時,我們推斷 PACKED 編碼
  • expanded - 當 proto3 欄位的 packed 明確設定為 false 時,我們推斷 EXPANDED 編碼

一致性測試

已新增特定於版本的一致性測試,但需要選擇啟用。可以將 --maximum_edition 2023 標誌傳遞給執行器以啟用這些測試。您需要配置您的被測二進位檔案來處理以下新的訊息類型

  • protobuf_test_messages.editions.proto2.TestAllTypesProto2 - 與舊的 proto2 訊息相同,但已轉換為 2023 版
  • protobuf_test_messages.editions.proto3.TestAllTypesProto3 - 與舊的 proto3 訊息相同,但已轉換為 2023 版
  • protobuf_test_messages.editions.TestAllTypesEdition2023 - 用於涵蓋特定於 2023 版的測試案例

功能解析

版本使用詞法範圍來定義功能,這表示任何需要實作版本支援的非 C++ 程式碼都需要重新實作我們的功能解析演算法。但是,大部分工作由 protoc 本身處理,可以將其配置為輸出中間的 FeatureSetDefaults 訊息。此訊息包含一組功能定義檔案的「編譯」,佈局每個版本中的預設功能值。

例如,上面的功能定義將編譯為 proto2 和 2025 版之間的以下預設值(以文字格式表示)

defaults {
  edition: EDITION_PROTO2
  overridable_features { [foo.features] {} }
  fixed_features {
    // Global feature defaults…
    [foo.features] { feature_value: VALUE1 }
  }
}
defaults {
  edition: EDITION_PROTO3
  overridable_features { [foo.features] {} }
  fixed_features {
    // Global feature defaults…
    [foo.features] { feature_value: VALUE1 }
  }
}
defaults {
  edition: EDITION_2023
  overridable_features {
    // Global feature defaults…
    [foo.features] { feature_value: VALUE1 }
  }
}
defaults {
  edition: EDITION_2024
  overridable_features {
    // Global feature defaults…
    [foo.features] { feature_value: VALUE2 }
  }
}
defaults {
  edition: EDITION_2025
  overridable_features {
    // Global feature defaults…
  }
  fixed_features { [foo.features] { feature_value: VALUE2 } }
}
minimum_edition: EDITION_PROTO2
maximum_edition: EDITION_2025

為了簡潔起見,省略了全域功能預設值,但它們也會存在。此物件包含每個具有唯一預設值集合的版本的有序列表(某些版本可能最終不存在)在指定範圍內。每個預設值集合都分為可覆寫固定功能。前者是該版本支援的功能,使用者可以自由覆寫。固定功能是尚未引入或已移除的功能,使用者無法覆寫。

我們提供一個 Bazel 規則來編譯這些中間物件

load("@com_google_protobuf//editions:defaults.bzl", "compile_edition_defaults")

compile_edition_defaults(
    name = "my_defaults",
    srcs = ["//some/path:lang_features_proto"],
    maximum_edition = "PROTO2",
    minimum_edition = "2024",
)

輸出 FeatureSetDefaults 可以嵌入到您需要在其中執行功能解析的任何語言的原始字串常值中。我們還提供一個 embed_edition_defaults 巨集來執行此操作

embed_edition_defaults(
    name = "embed_my_defaults",
    defaults = ":my_defaults",
    output = "my_defaults.h",
    placeholder = "DEFAULTS_DATA",
    template = "my_defaults.h.template",
)

或者,您可以直接(在 Bazel 之外)調用 protoc 來產生此資料

protoc --edition_defaults_out=defaults.binpb --edition_defaults_minimum=PROTO2 --edition_defaults_maximum=2023 <feature files...>

一旦您的程式碼連結並解析了預設訊息,在給定版本中,檔案描述器的功能解析遵循簡單的演算法

  1. 驗證版本是否在適當的範圍內 [minimum_edition, maximum_edition]
  2. 在排序的 defaults 欄位中二元搜尋小於或等於該版本的最高條目
  3. 從選定的預設值將 overridable_features 合併到 fixed_features
  4. 合併在描述器上設定的任何明確功能(檔案選項中的 features 欄位)

從那裡,您可以遞迴地解析所有其他描述器的功能

  1. 初始化為父描述器的功能集
  2. 合併在描述器上設定的任何明確功能(選項中的 features 欄位)

為了判斷「父」描述器,您可以參考我們的 C++ 實作。這在大多數情況下很簡單,但擴充功能有點令人驚訝,因為它們的父項是封閉範圍,而不是被擴充者。Oneof 也需要被視為其欄位的父項。

一致性測試

在未來的版本中,我們計劃新增一致性測試,以驗證跨語言的功能解析。在此之前,我們常規的 一致性測試確實提供部分涵蓋範圍,並且我們的 繼承單元測試範例可以移植以提供更全面的涵蓋範圍。

範例

以下是一些我們在運行時和外掛程式中實作版本支援的真實範例。

Java

  • #14138 - 使用 Java 功能 proto 的 C++ gencode 引導編譯器
  • #14377 - 在 Java、Kotlin 和 Java Lite 程式碼產生器中使用功能,包括程式碼產生測試
  • #15210 - 在 Java 完整運行時中使用功能,涵蓋 Java 功能引導、功能解析和舊版,以及單元測試和一致性測試

純 Python

  • #14546 - 預先設定程式碼產生測試
  • #14547 - 一次性完整實作版本,以及單元測試和一致性測試

𝛍pb

  • #14638 - 版本實作的第一個步驟,涵蓋功能解析和舊版
  • #14667 - 新增更完整的欄位標籤/類型處理、對 upb 程式碼產生器的支援以及一些測試
  • #14678 - 將 upb 連接到 Python 運行時,並進行更多單元測試和一致性測試

Ruby

  • #16132 - 將 upb/Java 連接到所有四個 Ruby 運行時以獲得完整的版本支援