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

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

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

概要

エディション 2023

最初にリリースされたエディションは Edition 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" }
  ];
}

ここでは、新しい enum フィーチャー foo.feature_value を定義しました(現在、boolean と enum 型のみがサポートされています)。このフィーチャーが取り得る値を定義するだけでなく、どのように使用できるかも指定する必要があります。

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

フィーチャーとは何か?

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

削除する意図のないオプションの動作を制御するフラグは、カスタムオプションとして実装する方が適切です。これは、フィーチャーを boolean または enum 型に制限している理由と関連しています。(比較的)無制限の数の値によって制御される動作は、最終的に非常に多くの異なる動作を廃止することは現実的ではないため、エディションフレームワークには適していない可能性が高いです。

これには、ワイヤー境界に関連する動作という注意点があります。言語固有のフィーチャーを使用してシリアライズまたはパース動作を制御することは危険な場合があります。なぜなら、反対側には他の言語が存在する可能性があるからです。ワイヤーフォーマットの変更は常に descriptor.proto 内のグローバルフィーチャーによって制御されるべきであり、これによりすべてのランタイムが均一にこれを尊重できます。

ジェネレーター

C++ で書かれたジェネレーターは、C++ ランタイムを使用するため、多くのメリットを享受できます。フィーチャー解決を自分で処理する必要はなく、フィーチャー拡張が必要な場合は、CodeGenerator の GetFeatureExtensions に登録できます。一般的に、コード生成でディスクリプタの解決済みフィーチャーにアクセスするには GetResolvedSourceFeatures を、自身の未解決フィーチャーにアクセスするには GetUnresolvedSourceFeatures を使用できます。

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

明示的なサポート

ジェネレーターは、サポートするエディションを正確に指定する必要があります。これにより、エディションがリリースされた後でも、独自のスケジュールで安全にサポートを追加できます。Protoc は、CodeGeneratorResponsesupported_features フィールドに FEATURE_SUPPORTS_EDITIONS を含まないジェネレーターに送信されたエディションの proto を拒否します。さらに、正確なサポート期間を指定するための minimum_edition および maximum_edition フィールドがあります。新しいエディションのすべてのコードとフィーチャーの変更を定義したら、maximum_edition を上げてこのサポートを告知できます。

コード生成テスト

Edition 2023 が予期せぬ機能変更を生成しないことを保証するために使用できるコード生成テストのセットがあります。これらは、機能の大部分が生成されたコードにある C++ や Java のような言語で非常に役立っています。一方、生成されたコードが基本的にシリアライズされたディスクリプタのコレクションにすぎない Python のような言語では、それほど有用ではありません。

このインフラストラクチャはまだ再利用できませんが、将来のリリースで利用可能になる予定です。その時点で、これらを使用してエディションへの移行が予期せぬコード生成の変更を伴わないことを検証できるようになります。

ランタイム

リフレクションや動的メッセージを持たないランタイムは、エディションを実装するために何もする必要はありません。そのロジックはすべてコードジェネレーターによって処理されるべきです。

リフレクションはあるが動的メッセージはない言語は、解決済みフィーチャーを必要としますが、オプションでジェネレーターのみで処理することもできます。これは、コード生成中に解決済みおよび未解決のフィーチャーセットの両方をランタイムに渡すことで実現できます。これにより、ランタイムでのフィーチャー解決の再実装を回避できますが、すべてのディスクリプタに対して一意のフィーチャーセットが作成されるため、効率が主な欠点となります。

動的メッセージを持つ言語は、実行時にディスクリプタを構築できる必要があるため、エディションを完全に実装する必要があります。

構文リフレクション

リフレクションを持つランタイムでエディションを実装する最初のステップは、syntax キーワードのすべての直接チェックを削除することです。これらはすべて、よりきめ細かなフィーチャーヘルパーに移動させるべきであり、必要であれば引き続き syntax を使用できます。

以下のフィーチャーヘルパーは、言語に応じた命名でディスクリプタ上に実装されるべきです。

  • FieldDescriptor::has_presence - フィールドに明示的な存在があるかどうか
    • 繰り返しフィールドには決して存在がない
    • メッセージ、拡張、および oneof フィールドには常に明示的な存在がある
    • それ以外は、field_presenceIMPLICIT でない場合にのみ存在がある
  • FieldDescriptor::is_required - フィールドが必須であるかどうか
  • FieldDescriptor::requires_utf8_validation - フィールドが UTF-8 の有効性についてチェックされるべきかどうか
  • 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」ヘルパーは削除すべきです。なぜなら、それらは合成 oneofs(エディションには存在しない)に関する情報を漏洩させるためです。
  • グループ型
    • TYPE_GROUP enum 値は削除され、is_delimited ヘルパーに置き換えられるべきです。
  • 必須ラベル
    • LABEL_REQUIRED enum 値は削除され、is_required ヘルパーに置き換えられるべきです。

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

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

フィーチャーの可視性

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

フィーチャーが漏れ出ることを許可する唯一のケースは、ディスクリプタをシリアライズする場合です。結果として生成されるディスクリプタ proto は、元の proto ファイルの忠実な表現であるべきであり、オプション内に未解決のフィーチャーを含むべきです。

レガシーエディション

legacy-syntax-editionsでより詳しく議論されているように、editions実装の早期カバレッジを得る優れた方法は、proto2、proto3、およびeditionsを統合することです。これにより、proto2とproto3は内部的にeditionsに効果的に移行され、Syntax Reflectionで実装されたすべてのヘルパーが(構文による分岐ではなく)フィーチャーを排他的に使用するようになります。これは、Feature Resolutionフィーチャー推論フェーズを挿入することで実現できます。このフェーズでは、プロトファイルの様々な側面が適切なフィーチャーを通知できます。これらのフィーチャーは、その後親のフィーチャーにマージされ、解決されたフィーチャーセットを得ることができます。

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++ 以外のコードは、弊社のフィーチャー解決アルゴリズムを再実装する必要があることを意味します。ただし、作業の大部分は protoc 自体によって処理され、中間的な 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",
)

あるいは、protoc を直接(Bazel の外で)呼び出してこのデータを生成することもできます。

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_featuresfixed_features にマージします。
  4. ディスクリプタに設定されている明示的なフィーチャー(ファイルオプションの features フィールド)をマージします。

そこから、他のすべてのディスクリプタのフィーチャーを再帰的に解決できます。

  1. 親ディスクリプタのフィーチャーセットで初期化します。
  2. ディスクリプタに設定されている明示的なフィーチャー(オプションの features フィールド)をマージします。

「親」記述子を決定するには、弊社のC++実装を参照できます。これはほとんどの場合において単純ですが、拡張機能(extensions)はその親が拡張される側(extendee)ではなく、囲むスコープ(enclosing scope)であるため、少々驚くかもしれません。また、Oneofはそのフィールドの親として考慮される必要があります。

コンフォーマンス テスト

将来のリリースでは、機能解決の言語横断的な検証のために、準拠性テストを追加する予定です。それまでは、通常の準拠性テストである程度のカバレッジが得られます。また、弊社の継承の単体テスト例を移植することで、より包括的なカバレッジを提供できます。

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

Java

  • #14138 - Java フィーチャー proto のための C++ gencode を使用したコンパイラのブートストラップ
  • #14377 - コード生成テストを含む、Java、Kotlin、および Java Lite のコードジェネレーターでフィーチャーを使用
  • #15210 - Java フィーチャーのブートストラップ、フィーチャー解決、レガシーエディションをカバーするJavaフルランタイムでフィーチャーを使用。単体テストとコンフォーマンス テストも含む。

Pure Python

  • #14546 - 事前コード生成テストのセットアップ
  • #14547 - 単体テストとコンフォーマンス テストを含む、エディションの完全な一括実装

𝛍pb

  • #14638 - フィーチャー解決とレガシーエディションをカバーするエディション実装の初回パス
  • #14667 - フィールドラベル/型のより完全な処理、upb のコードジェネレーターのサポート、およびいくつかのテストを追加
  • #14678 - upb を Python ランタイムに接続、さらなる単体テストとコンフォーマンス テストを含む

Ruby

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