エディションサポートの実装

ランタイムとプラグインでエディションサポートを実装するための手順。

このトピックでは、新しいランタイムとジェネレータでエディションを実装する方法について説明します。

概要

エディション 2023

最初にリリースされたエディションは Edition 2023 であり、proto2 と proto3 の構文を統合するように設計されています。動作の違いをカバーするために追加された機能については、「エディションの機能設定」で詳しく説明されています。

機能定義

エディションと私たちが定義したグローバル機能を*サポート*することに加えて、インフラストラクチャを活用するために独自の機能を定義したい場合があるかもしれません。これにより、ジェネレータとランタイムで新しい動作をゲートするために使用できる任意の機能を定義できます。最初のステップは、descriptor.proto 内の `FeatureSet` メッセージに 9999 を超える拡張番号を要求することです。GitHub でプルリクエストを送信いただければ、次回のリリースに含まれます(例については、#15439 を参照してください)。

拡張番号を取得したら、機能プロトを作成できます(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` を定義しました(現在、ブール型と列挙型のみがサポートされています)。取りうる値を定義することに加えて、それがどのように使用できるかも指定する必要があります。

  • ターゲット - この機能をアタッチできるプロト記述子のタイプを指定します。これにより、ユーザーが機能を明示的に指定できる場所が制御されます。すべてのタイプを明示的にリストする必要があります。
  • 機能サポート - この機能のエディションに対する有効期間を指定します。導入されたエディションを指定する必要があり、それ以前は許可されません。オプションで、後のエディションで機能を非推奨にしたり削除したりできます。
  • エディションのデフォルト - 機能のデフォルト値に対する変更を指定します。これは、サポートされているすべてのエディションをカバーする必要がありますが、デフォルトが変更されていないエディションは省略できます。`EDITION_PROTO2` と `EDITION_PROTO3` は、「レガシー」エディションのデフォルトを提供するためにここで指定できることに注意してください(レガシーエディションを参照)。

機能とは?

機能は、エディションの境界線上で、時間の経過とともに悪い動作を段階的に改善するためのメカニズムを提供するように設計されています。実際に機能を削除するまでの期間は何年も(あるいは何十年も)先かもしれませんが、あらゆる機能の望ましい目標は最終的な削除です。悪い動作が特定されたら、修正を保護する新しい機能を導入できます。次のエディション(またはそれ以降)では、ユーザーがアップグレード時に古い動作を維持できるようにしながら、デフォルト値を反転させます。将来のある時点で、その機能を非推奨としてマークし、それをオーバーライドしているユーザーに対してカスタム警告をトリガーします。その後のエディションでは、その機能を削除済みとしてマークし、ユーザーがそれをオーバーライドできなくします(ただし、デフォルト値は引き続き適用されます)。互換性のないリリースで最後のエディションのサポートが終了するまで、その機能は古いエディションに固定されているプロトに対して引き続き使用可能であり、移行する時間が与えられます。

削除する意図のないオプションの動作を制御するフラグは、カスタムオプションとして実装する方が適切です。これは、機能をブール型または列挙型に制限した理由に関連しています。比較的に無制限の数の値によって制御される動作は、それほど多くの異なる動作を最終的に廃止することは現実的ではないため、エディションフレームワークにはあまり適していません。

この例外の1つは、ワイヤ境界に関連する動作です。言語固有の機能を使用してシリアル化または解析動作を制御することは危険です。なぜなら、他の言語が反対側にある可能性があるからです。ワイヤフォーマットの変更は、常に`descriptor.proto`のグローバル機能によって制御されるべきであり、これによりすべてのランタイムで一様に尊重されます。

ジェネレータ

C++ で記述されたジェネレーターは、C++ ランタイムを使用するため、多くのものを無料で利用できます。彼らは 機能解決 を自分自身で処理する必要はなく、機能拡張が必要な場合は、CodeGenerator の `GetFeatureExtensions` に登録できます。通常、codegen で記述子の解決された機能にアクセスするには `GetResolvedSourceFeatures` を使用し、自身の未解決の機能にアクセスするには `GetUnresolvedSourceFeatures` を使用できます。

コードを生成するランタイムと同じ言語で書かれたプラグインは、その機能定義のためにカスタムのブートストラップが必要になる場合があります。

明示的なサポート

ジェネレーターは、サポートするエディションを正確に指定する必要があります。これにより、エディションがリリースされた後でも、独自のスケジュールで安全にサポートを追加できます。Protocは、`CodeGeneratorResponse`の`supported_features`フィールドに`FEATURE_SUPPORTS_EDITIONS`を含まないジェネレーターに送信されたエディションプロトを拒否します。さらに、正確なサポート期間を指定するための`minimum_edition`フィールドと`maximum_edition`フィールドがあります。新しいエディションのすべてのコードと機能変更を定義したら、`maximum_edition`を増やしてこのサポートを宣伝できます。

コード生成テスト

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 - 繰り返しフィールドがパックされたエンコーディングを持つかどうか
  • FieldDescriptor::is_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` ヘルパーに置き換えられるべきです。

これらのチェックが存在するが、エディションに**敵対的ではない**ユーザーコードのクラスは多数あります。例えば、proto3の`optional`をその合成oneof実装のために特別に処理する必要があるコードは、極性が`syntax == "proto3"`のようなものであれば(`syntax != "proto2"`をチェックするのではなく)、エディションに敵対的ではありません。

これらのAPIを完全に削除できない場合は、非推奨として推奨しないべきです。

機能の可視性

editions-feature-visibilityで議論されているように、機能プロトはProtobuf実装の内部詳細であり続けるべきです。それらが制御する*動作*は記述子メソッドを介して公開されるべきですが、プロト自体は公開されるべきではありません。特に、これはユーザーに公開されるすべてのオプションが`features`フィールドを削除する必要があることを意味します。

機能が漏洩することを許容する唯一のケースは、記述子をシリアル化するときです。結果の記述子プロトは、元のプロトファイルを忠実に表現したものであり、オプション内に*未解決の機能*を含むべきです。

レガシー・エディション

legacy-syntax-editions でさらに議論されているように、エディション実装の初期カバレッジを得る優れた方法は、proto2、proto3、およびエディションを統合することです。これにより、proto2 と proto3 が舞台裏でエディションに効果的に移行され、構文リフレクションで実装されたすべてのヘルパーが(構文による分岐ではなく)排他的に機能を使用するようになります。これは、プロトファイルのさまざまな側面が適切な機能を通知できる機能解決に*機能推論*フェーズを挿入することで行うことができます。これらの機能は、親の機能にマージされて、解決された機能セットを取得できます。

proto2/proto3 の合理的なデフォルトは既に提供されていますが、Edition 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 メッセージと同一ですが、Edition 2023 に変換されています。
  • protobuf_test_messages.editions.proto3.TestAllTypesProto3 - 古い proto3 メッセージと同一ですが、Edition 2023 に変換されています。
  • protobuf_test_messages.editions.TestAllTypesEdition2023 - Edition 2023 固有のテストケースをカバーするために使用されます。

機能解決

エディションは機能を定義するために語彙スコープを使用します。これは、エディションサポートを実装する必要がある C++ 以外のコードが、独自の*機能解決*アルゴリズムを再実装する必要があることを意味します。ただし、作業の大部分はプロトク自体によって処理され、プロトクは中間的な `FeatureSetDefaults` メッセージを出力するように設定できます。このメッセージには、一連の機能定義ファイルの「コンパイル」が含まれており、すべてのエディションにおけるデフォルトの機能値が記述されています。

例えば、上記の機能定義は、proto2とEdition 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...>

デフォルトメッセージがコードによってフックアップされ、解析されると、特定のEditionのファイル記述子の機能解決は、単純なアルゴリズムに従います。

  1. エディションが適切な範囲 [minimum_edition, maximum_edition] 内にあることを検証します。
  2. 順序付けられた`defaults`フィールドで、エディション以下で最も高いエントリを二分探索します。
  3. 選択されたデフォルトから、overridable_featuresfixed_featuresにマージします。
  4. 記述子に設定された明示的な機能(ファイルオプションの`features`フィールド)をマージします。

そこから、他のすべての記述子の機能を再帰的に解決できます。

  1. 親記述子の機能セットに初期化します。
  2. 記述子に設定された明示的な機能(オプションの `features` フィールド)をマージします。

「親」記述子を決定するには、C++ 実装を参照できます。ほとんどの場合、これは簡単ですが、拡張は親が拡張対象ではなく囲むスコープであるため、少し驚くべき点があります。Oneof もそのフィールドの親として考慮する必要があります。

適合性テスト

今後のリリースでは、機能解決の言語間検証のための適合性テストを追加する予定です。それまでは、通常の適合性テストによって部分的なカバレッジが得られており、継承の単体テスト例を移植することで、より包括的なカバレッジを提供できます。

以下に、ランタイムとプラグインでエディションサポートをどのように実装したかの実例をいくつか示します。

Java

  • #14138 - Java機能プロト用のC++ gencodeでコンパイラをブートストラップする
  • #14377 - コード生成テストを含むJava、Kotlin、Java Liteコードジェネレータで機能を使用する
  • #15210 - Javaフルランタイムで機能を使用する。Java機能のブートストラップ、機能解決、レガシーエディション、単体テスト、適合性テストを含む。

純粋なPython

  • #14546 - コード生成テストを事前に設定
  • #14547 - 単体テストと適合性テストを含むエディションを一度に完全に実装

𝛍pb

  • #14638 - 機能解決とレガシーエディションをカバーするエディション実装の初回パス
  • #14667 - フィールドラベル/タイプのより完全な処理、upbのコードジェネレータのサポート、およびいくつかのテストを追加しました。
  • #14678 - Pythonランタイムにupbをフックし、より多くの単体テストと適合性テストを追加。

Ruby

  • #16132 - upb/Javaを4つのRubyランタイムすべてに接続し、完全なエディションサポートを実現。