拡張宣言

拡張宣言とは何か、なぜ必要か、どのように使用するかを詳細に説明します。

はじめに

このページでは、拡張宣言とは何か、なぜ必要か、どのように使用するかを詳細に説明します。

拡張について入門が必要な場合は、この拡張ガイドを読んでください。

動機

拡張宣言は、通常のフィールドと拡張の間で最適なバランスを取ることを目指します。拡張と同様に、フィールドのメッセージ型への依存関係を作成することを避けるため、未使用のメッセージを削除するのが難しい、または不可能な環境では、ビルドグラフがスリムになり、バイナリが小さくなります。通常のフィールドと同様に、フィールド名/番号は囲んでいるメッセージに表示されるため、競合を回避し、宣言されているフィールドの便利なリストを確認しやすくなります。

拡張宣言で占有されている拡張番号をリスト化することで、ユーザーは利用可能な拡張番号を選択し、競合を回避しやすくなります。

使用法

拡張宣言は拡張範囲のオプションです。C++ の前方宣言と同様に、完全な拡張定義を含む `.proto` ファイルをインポートすることなく、拡張フィールドのフィールド型、フィールド名、およびカーディナリティ (単数または繰り返し) を宣言できます。

edition = "2023";

message Foo {
  extensions 4 to 1000 [
    declaration = {
      number: 4,
      full_name: ".my.package.event_annotations",
      type: ".logs.proto.ValidationAnnotations",
      repeated: true },
    declaration = {
      number: 999,
      full_name: ".foo.package.bar",
      type: "int32"}];
}

この構文には次のセマンティクスがあります

  • 範囲のサイズが許す場合、異なる拡張番号を持つ複数の `declaration` を単一の拡張範囲で定義できます。
  • 拡張範囲に宣言がある場合、その範囲の*すべて*の拡張も宣言されなければなりません。これにより、未宣言の拡張が追加されるのを防ぎ、新しい拡張がその範囲の宣言を使用することを強制します。
  • 指定されたメッセージ型 (`.logs.proto.ValidationAnnotations`) は、以前に定義またはインポートされている必要はありません。我々は、それが別の `.proto` ファイルで定義される可能性のある有効な名前であることのみをチェックします。
  • この、または別の `.proto` ファイルが、この名前または番号でこのメッセージ (`Foo`) の拡張を定義する場合、我々は、拡張の番号、型、および完全な名前がここで前方宣言されているものと一致することを強制します。

拡張宣言は、異なるパッケージを持つ2つの拡張フィールドを期待します

package my.package;
extend Foo {
  repeated logs.proto.ValidationAnnotations event_annotations = 4;
}
package foo.package;
extend Foo {
  optional int32 bar = 999;
}

予約済み宣言

拡張宣言は、もはや積極的に使用されておらず、拡張定義が削除されたことを示すために `reserved: true` とマークできます。拡張宣言を削除したり、その `type` や `full_name` の値を編集したりしないでください

この `reserved` タグは、通常のフィールドの `reserved` キーワードとは別個のものであり、拡張範囲を分割する必要はありません

edition = "2023";

message Foo {
  extensions 4 to 1000 [
    declaration = {
      number: 500,
      full_name: ".my.package.event_annotations",
      type: ".logs.proto.ValidationAnnotations",
      reserved: true }];
}

宣言で `reserved` となっている番号を使用する拡張フィールド定義は、コンパイルに失敗します。

descriptor.proto での表現

拡張宣言は descriptor.proto において `proto2.ExtensionRangeOptions` のフィールドとして表現されます

message ExtensionRangeOptions {
  message Declaration {
    optional int32 number = 1;
    optional string full_name = 2;
    optional string type = 3;
    optional bool reserved = 5;
    optional bool repeated = 6;
  }
  repeated Declaration declaration = 2;
}

リフレクションフィールドのルックアップ

拡張宣言は、`Descriptor::FindFieldByName()` や `Descriptor::FindFieldByNumber()` のような通常のフィールドルックアップ関数からは*返されません*。拡張と同様に、`DescriptorPool::FindExtensionByName()` のような拡張ルックアップルーチンによって検出可能です。これは、宣言が定義ではなく、完全な `FieldDescriptor` を返すのに十分な情報を持たないという事実を反映した明示的な選択です。

宣言された拡張は、TextFormat および JSON の観点からは、依然として通常の拡張のように動作します。また、既存のフィールドを宣言された拡張に移行するには、まずそのフィールドのリフレクティブな使用をすべて移行する必要があることを意味します。

拡張宣言を使用した番号割り当て

拡張は通常のフィールドと同様にフィールド番号を使用するため、各拡張が親メッセージ内で一意の番号を割り当てられることが重要です。親メッセージ内の各拡張のフィールド番号と型を宣言するために、拡張宣言を使用することをお勧めします。拡張宣言は、親メッセージのすべての拡張のレジストリとして機能し、protoc はフィールド番号の競合がないことを強制します。新しい拡張を追加する際は、通常、以前追加された拡張番号を1つ増やすことで、次に利用可能な番号を選択してください。

ヒント: `MessageSet` には、次に利用可能な番号を選択するのに役立つスクリプトを提供する特別なガイダンスがあります。

拡張を削除するたびに、誤って再利用するリスクを排除するために、フィールド番号を `reserved` とマークするようにしてください。

この慣習は単なる推奨事項であり、protobuf チームは、すべての拡張可能なメッセージに対して誰かにこれを遵守させる能力も意図もありません。拡張可能なプロトの所有者として、拡張宣言を通じて拡張番号を調整したくない場合、他の方法で調整を提供することを選択できます。ただし、拡張番号の誤った再利用は重大な問題を引き起こす可能性があるため、非常に注意してください。

この問題を回避する一つの方法は、拡張を完全に避け、代わりに`google.protobuf.Any`を使用することです。これは、ストレージを前面に出す API や、クライアントがプロトの内容を気にしても、それを受信するシステムが気にしないパススルーシステムにとって良い選択肢となりえます。

拡張番号を再利用した場合の結果

拡張は、コンテナメッセージの外部で定義されるフィールドであり、通常は別の .proto ファイルにあります。この定義の分散により、2人の開発者が誤って同じ拡張フィールド番号に対して異なる定義を作成することが容易になります。

拡張定義を変更することの結果は、拡張と標準フィールドで同じです。フィールド番号を再利用すると、プロトがワイヤー形式からどのようにデコードされるかについて曖昧さが生じます。Protobuf のワイヤー形式は軽量であり、ある定義を使用してエンコードされ、別の定義を使用してデコードされたフィールドを検出する良い方法を提供しません。

この曖昧さは、クライアントが1つの拡張定義を使用し、サーバーが別の拡張定義を使用して通信する場合など、短期間で現れることがあります。

この曖昧さは、ある拡張定義を使用してエンコードされたデータを保存し、後で2番目の拡張定義を使用して取得およびデコードする場合など、より長期間にわたって現れることもあります。この長期的なケースは、データがエンコードおよび保存された後に最初の拡張定義が削除された場合、診断が難しい場合があります。

これによる結果は次のとおりです

  1. パースエラー (最良のシナリオ)。
  2. PII / SPII の漏洩 – PII または SPII がある拡張定義を使用して書き込まれ、別の拡張定義を使用して読み取られる場合。
  3. データの破損 – データが「間違った」定義を使用して読み取られ、変更されて書き換えられた場合。

データ定義の曖昧さは、少なくとも誰かにデバッグ時間を要するでしょう。また、クリーンアップに数ヶ月かかるデータ漏洩や破損を引き起こす可能性もあります。

使用のヒント

拡張宣言を絶対に削除しない

拡張宣言を削除すると、将来の誤った再利用の可能性が開かれます。拡張がもはや処理されず、定義が削除された場合、拡張宣言は予約済みとしてマークできます。

新しい拡張宣言に `reserved` リストのフィールド名または番号を絶対に使用しない

予約済み番号は、過去にフィールドや他の拡張に使用された可能性があります。

予約済みフィールドの `full_name` を使用することは、textproto を使用する際に曖昧さの可能性があるため推奨されません。

既存の拡張宣言の型を絶対に変更しない

拡張フィールドの型を変更すると、データの破損につながる可能性があります。

拡張フィールドが enum またはメッセージ型であり、その enum またはメッセージ型が名前変更される場合、宣言名を更新することは必須であり、安全です。破損を避けるため、型、拡張フィールド定義、および拡張宣言の更新はすべて単一のコミットで行われるべきです。

拡張フィールドの名前を変更する際は注意する

拡張フィールドの名前を変更することはワイヤー形式では問題ありませんが、JSON および TextFormat の解析を壊す可能性があります。