拡張機能の宣言

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

はじめに

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

拡張機能の紹介が必要な場合は、この拡張ガイドを読んでください。

動機

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

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

使用方法

拡張宣言は拡張範囲のオプションです。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タグは、通常のフィールドの予約済みキーワードとは別であり、拡張範囲を分割する必要はありません

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

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

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

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

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

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

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

この結果は次のようになります。

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

データ定義の曖昧さは、最低でも誰かのデバッグ時間を犠牲にすることになります。また、数ヶ月かかるデータ漏洩や破損を引き起こす可能性もあります。

使用上のヒント

拡張宣言は決して削除しない

拡張宣言を削除すると、将来の偶発的な再利用への道が開かれます。拡張機能が処理されなくなり、定義が削除された場合、拡張宣言はreservedとマークできます。

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

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

テキストプロトを使用する際の曖昧さの可能性から、予約済みフィールドのfull_nameを使用することは推奨されません。

既存の拡張宣言のタイプは決して変更しない

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

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

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

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