實作版本支援

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

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

概觀

2023 版本

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

功能定義

除了支援版本和我們定義的全域功能之外,您可能還想定義自己的功能以利用基礎架構。這可讓您定義任意功能,供您的產生器和執行階段用於閘控新行為。第一步是針對 descriptor.proto 中 9999 以上的 FeatureSet 訊息宣告一個擴充號碼。您可以向 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 將拒絕傳送給產生器的任何版本 proto,這些產生器的 CodeGeneratorResponsesupported_features 欄位中未包含 FEATURE_SUPPORTS_EDITIONS。此外,我們還有 minimum_editionmaximum_edition 欄位,用於指定您的精確支援視窗。一旦您定義了新版本的所有程式碼和功能變更,您就可以提高 maximum_edition 以宣傳此支援。

程式碼產生測試

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

此基礎架構尚不可重複使用,但計劃在未來的版本中提供。到那時,您將可以使用它們來驗證遷移到版本不會有任何非預期的程式碼產生變更。

執行階段

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

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

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

語法反射

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

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

  • FieldDescriptor::has_presence - 欄位是否具有明確的存在性
    • 重複欄位永遠沒有存在性
    • 訊息、擴充和 oneof 欄位總是具有明確的存在性
    • 其他所有項目都有存在性,當且僅當 field_presence 不是 IMPLICIT
  • FieldDescriptor::is_required - 欄位是否為必要欄位
  • FieldDescriptor::requires_utf8_validation - 是否應檢查欄位的 utf8 有效性
  • FieldDescriptor::is_packed - 重複欄位是否具有 packed 編碼
  • FieldDescriptor::is_delimited - 訊息欄位是否具有 delimited 編碼
  • EnumDescriptor::is_closed - 欄位是否為封閉式

下游使用者應遷移到這些新的輔助程式,而不是直接使用語法。以下類別的現有描述器 API 理想情況下應棄用並最終移除,因為它們會洩漏語法資訊

  • FileDescriptor 語法
  • Proto3 選用 API
    • FieldDescriptor::has_optional_keyword
    • OneofDescriptor::is_synthetic
    • Descriptor::*real_oneof* - 應重新命名為僅 "oneof",且現有的 "oneof" 輔助程式應移除,因為它們會洩漏關於合成 oneof 的資訊 (版本中不存在)。
  • 群組類型
    • TYPE_GROUP 列舉值應移除,並替換為 is_delimited 輔助程式。
  • 必要標籤
    • LABEL_REQUIRED 列舉值應移除,並替換為 is_required 輔助程式。

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

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

功能可見性

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

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

舊版版本

legacy-syntax-editions中更詳細的討論,取得版本實作早期涵蓋範圍的好方法是統一 proto2、proto3 和版本。這有效地將 proto2 和 proto3 在底層遷移到版本,並使 語法反射中實作的所有輔助程式專門使用功能 (而不是在語法上分支)。這可以透過在功能解析中插入功能推斷階段來完成,其中 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 本身處理,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 - 使用 C++ gencode 為 Java 功能 proto 啟動編譯器
  • #14377 - 在 Java、Kotlin 和 Java Lite 程式碼產生器中使用功能,包括程式碼產生測試
  • #15210 - 在 Java 完整執行階段中使用功能,涵蓋 Java 功能啟動、功能解析和舊版版本,以及單元測試和一致性測試

純 Python

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

𝛍pb

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

Ruby

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