拡張宣言

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

はじめに

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

拡張機能の概要が必要な場合は、こちらの拡張機能ガイドをお読みください

背景

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

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

使い方

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

syntax = "proto2";

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 を単一の拡張範囲で定義できます。
  • 拡張範囲の宣言が 1 つでもある場合、その範囲のすべての拡張機能も宣言する必要があります。これにより、宣言されていない拡張機能が追加されるのを防ぎ、新しい拡張機能は範囲の宣言を使用することを強制します。
  • 指定されたメッセージ型 (.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 キーワードとは別であり、拡張範囲を分割する必要はありません

syntax = "proto2";

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 チームは、すべての拡張可能なメッセージについて、誰かにそれを遵守させる能力も意思もありません。拡張可能な proto の所有者であるあなたが、拡張宣言を通じて拡張番号を調整したくない場合は、他の手段を通じて調整を提供することを選択できます。ただし、拡張番号の誤った再利用は深刻な問題を引き起こす可能性があるため、非常に注意してください。

問題を回避する 1 つの方法は、拡張機能を完全に避け、代わりに google.protobuf.Any を使用することです。これは、ストレージをフロントエンドとする API や、クライアントが proto の内容を気にするが、それを受信するシステムが気にしないパススルーシステムに適した選択肢となる可能性があります。

拡張番号の再利用による影響

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

拡張機能の定義を変更した場合の影響は、拡張機能と標準フィールドで同じです。フィールド番号を再利用すると、proto をワイヤ形式からデコードする方法に曖昧さが生じます。protobuf ワイヤ形式は簡素であり、ある定義を使用してエンコードされ、別の定義を使用してデコードされたフィールドを検出する適切な方法を提供しません。

この曖昧さは、クライアントがある拡張機能定義を使用し、サーバーが別の拡張機能定義を使用して通信するなど、短い時間枠で顕在化する可能性があります。

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

この結果として起こりうることは次のとおりです

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

データ定義の曖昧さは、ほぼ確実に誰かのデバッグ時間を少なくとも浪費することになります。また、データのリークや破損を引き起こし、クリーンアップに数か月かかる可能性もあります。

ヒント

拡張宣言を削除しないでください

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

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

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

textproto を使用する場合の曖昧さの可能性があるため、予約済みフィールドの full_name を使用することはお勧めしません。

既存の拡張宣言の型を変更しないでください

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

拡張フィールドが enum またはメッセージ型であり、その enum またはメッセージ型が名前変更されている場合、宣言名を更新する必要があります。これは安全です。破損を防ぐために、型、拡張フィールド定義、および拡張宣言の更新はすべて 1 回のコミットで行う必要があります。

拡張フィールドの名前を変更する際は注意してください

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