言語ガイド(エディション)

プロジェクトで Protocol Buffers 言語の edition 2023 リビジョンを使用する方法について説明します。

このガイドでは、protocol buffer 言語を使用して protocol buffer データを構造化する方法について説明します。これには、.proto ファイルの構文と、.proto ファイルからデータ アクセス クラスを生成する方法が含まれます。このガイドは、protocol buffer 言語の edition 2023 を対象としています。エディションが proto2 および proto3 と概念的にどのように異なるかについては、Protobuf エディションの概要 を参照してください。

proto2 構文の詳細については、Proto2 言語ガイド を参照してください。

proto3 構文の詳細については、Proto3 言語ガイド を参照してください。

これはリファレンス ガイドです。このドキュメントで説明されている機能の多くを使用するステップバイステップの例については、選択した言語の チュートリアル を参照してください。

メッセージ型の定義

まず、非常に簡単な例を見てみましょう。検索リクエスト メッセージ形式を定義するとします。各検索リクエストには、クエリ文字列、関心のある結果の特定のページ、およびページあたりの結果数があります。メッセージ型を定義するために使用する .proto ファイルを次に示します。

edition = "2023";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 results_per_page = 3;
}
  • ファイルの最初の行は、protobuf 言語仕様の edition 2023 を使用していることを指定します。

    • edition(proto2/proto3 の場合は syntax)は、ファイルの最初の空でない、コメント以外の行である必要があります。
    • edition または syntax が指定されていない場合、protocol buffer コンパイラは proto2 を使用していると見なします。
  • SearchRequest メッセージ定義は、このタイプのメッセージに含めるデータごとに 3 つのフィールド(名前/値のペア)を指定します。各フィールドには、名前と型があります。

フィールド型の指定

前の例では、すべてのフィールドが スカラー型 です。2 つの整数 (page_numberresults_per_page) と文字列 (query) です。フィールドには、列挙型 や他のメッセージ型のような複合型も指定できます。

フィールド番号の割り当て

メッセージ定義の各フィールドには、1 から 536,870,911 までの番号を、次の制限付きで指定する必要があります。

  • 指定された番号は、そのメッセージのすべてのフィールドの中で一意である必要があります
  • フィールド番号 19,00019,999 は、Protocol Buffers の実装用に予約されています。protocol buffer コンパイラは、メッセージでこれらの予約済みフィールド番号のいずれかを使用するとエラーを報告します。
  • 以前に 予約済み のフィールド番号、または 拡張機能 に割り当てられているフィールド番号は使用できません。

この番号は、メッセージのワイヤ形式 でフィールドを識別するため、メッセージ型が使用された後は変更できません。フィールド番号を「変更」することは、そのフィールドを削除し、同じ型で新しい番号の新しいフィールドを作成することと同じです。適切な方法については、フィールドの削除 を参照してください。

フィールド番号は絶対に再利用しないでください。新しいフィールド定義で再利用するために、予約済み リストからフィールド番号を削除しないでください。フィールド番号の再利用による影響 を参照してください。

最も頻繁に設定されるフィールドには、1 ~ 15 のフィールド番号を使用する必要があります。フィールド番号の値が小さいほど、ワイヤ形式でのスペースが少なくなります。たとえば、1 ~ 15 の範囲のフィールド番号は、エンコードに 1 バイトかかります。16 ~ 2047 の範囲のフィールド番号は、2 バイトかかります。詳細については、Protocol Buffer エンコーディング を参照してください。

フィールド番号の再利用による影響

フィールド番号を再利用すると、ワイヤ形式のメッセージのデコードがあいまいになります。

protobuf ワイヤ形式は簡潔であり、ある定義を使用してエンコードされたフィールドを別の定義を使用してデコードする方法を提供しません。

ある定義を使用してフィールドをエンコードし、その同じフィールドを別の定義でデコードすると、次のことが起こる可能性があります。

  • デバッグに費やす開発者の時間の損失
  • パース/マージ エラー (最良のシナリオ)
  • PII/SPII のリーク
  • データの破損

フィールド番号の再利用の一般的な原因

  • フィールドの番号の振り直し (フィールドの番号順をより美しくするために行われる場合があります)。番号の振り直しは、事実上、番号の振り直しに関係するすべてのフィールドを削除して再度追加することになり、互換性のないワイヤ形式の変更が発生します。

  • フィールドを削除し、将来の再利用を防ぐために番号を 予約 しない。

    • これは、いくつかの理由から、拡張フィールド で非常に簡単に犯してしまう間違いでした。拡張宣言 は、拡張フィールドを予約するメカニズムを提供します。

フィールド番号は、フィールドのワイヤ形式を指定するために 3 ビットが使用されるため、32 ビットではなく 29 ビットに制限されています。詳細については、エンコーディングのトピック を参照してください。

フィールドのカーディナリティの指定

メッセージ フィールドは、次のいずれかになります。

  • 単数:

    単数フィールドには、明示的なカーディナリティ ラベルはありません。これには 2 つの状態が考えられます。

    • フィールドが設定され、明示的に設定された値、またはワイヤからパースされた値が含まれています。これはワイヤにシリアライズされます。
    • フィールドが設定されておらず、デフォルト値を返します。これはワイヤにシリアライズされません。

    値が明示的に設定されたかどうかを確認できます。

    エディションに移行された Proto3 暗黙的 フィールドは、field_presence 機能セットを IMPLICIT 値で使用します。

    エディションに移行された Proto2 required フィールドも field_presence 機能を使用しますが、LEGACY_REQUIRED に設定されます。

  • repeated: このフィールド型は、整形式メッセージで 0 回以上繰り返すことができます。繰り返される値の順序は保持されます。

  • map: これはペアのキー/値フィールド型です。このフィールド型の詳細については、マップ を参照してください。

繰り返されるフィールドはデフォルトでパックされます

proto エディションでは、スカラー数値型の repeated フィールドは、デフォルトで packed エンコーディングを使用します。

packed エンコーディングの詳細については、Protocol Buffer エンコーディング を参照してください。

整形式メッセージ

protobuf メッセージに適用される場合、「整形式」という用語は、シリアライズ/デシリアライズされたバイトを指します。protoc パーサーは、指定された proto 定義ファイルがパース可能であることを検証します。

単数フィールドは、ワイヤ形式バイトに複数回出現する可能性があります。パーサーは入力を受け入れますが、そのフィールドの最後のインスタンスのみが生成されたバインディングを介してアクセス可能になります。詳細については、Last One Wins を参照してください。

メッセージ型の追加

複数のメッセージ型を 1 つの .proto ファイルで定義できます。これは、複数の関連メッセージを定義する場合に役立ちます。たとえば、SearchResponse メッセージ型に対応する応答メッセージ形式を定義する場合は、それを同じ .proto に追加できます。

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 results_per_page = 3;
}

message SearchResponse {
 ...
}

メッセージの組み合わせは肥大化につながる 複数のメッセージ型(メッセージ、enum、サービスなど)を 1 つの .proto ファイルで定義できますが、依存関係が異なる多数のメッセージが 1 つのファイルで定義されている場合、依存関係の肥大化につながる可能性もあります。.proto ファイルあたりのメッセージ型をできるだけ少なくすることをお勧めします。

コメントの追加

.proto ファイルにコメントを追加するには

  • .proto コード要素の前の行に C/C++/Java 行末スタイル コメント「//」を使用することを推奨します。

  • C スタイル インライン/複数行コメント /* ... */ も使用できます。

    • 複数行コメントを使用する場合は、「*」のマージン行が推奨されます。
/**
 * SearchRequest represents a search query, with pagination options to
 * indicate which results to include in the response.
 */
message SearchRequest {
  string query = 1;

  // Which page number do we want?
  int32 page_number = 2;

  // Number of results to return per page.
  int32 results_per_page = 3;
}

フィールドの削除

フィールドを削除すると、適切に行わないと重大な問題が発生する可能性があります。

フィールドが不要になり、クライアント コードからすべての参照が削除されたら、メッセージからフィールド定義を削除できます。ただし、必ず 削除されたフィールド番号を予約 してください。フィールド番号を予約しないと、開発者が将来その番号を再利用する可能性があります。

メッセージの JSON および TextFormat エンコーディングが引き続きパースできるように、フィールド名も予約する必要があります。

予約済みフィールド番号

フィールドを完全に削除したり、コメントアウトしたりしてメッセージ型を 更新 すると、将来の開発者は、型に独自の更新を加えるときにフィールド番号を再利用できます。これにより、フィールド番号の再利用による影響 で説明されているように、重大な問題が発生する可能性があります。これが起こらないようにするには、削除されたフィールド番号を reserved リストに追加します。

protoc コンパイラは、将来の開発者がこれらの予約済みフィールド番号を使用しようとすると、エラー メッセージを生成します。

message Foo {
  reserved 2, 15, 9 to 11;
}

予約済みフィールド番号範囲は包括的です(9 to 119, 10, 11 と同じです)。

予約済みフィールド名

古いフィールド名を後で再利用することは、TextProto または JSON エンコーディングを使用している場合を除き、一般的に安全です。TextProto または JSON エンコーディングでは、フィールド名がシリアライズされます。このリスクを回避するには、削除されたフィールド名を reserved リストに追加できます。

予約済み名は、protoc コンパイラの動作のみに影響し、ランタイムの動作には影響しません。ただし、1 つの例外があります。TextProto 実装は、パース時に予約済み名を持つ不明なフィールドを(他の不明なフィールドのようにエラーを発生させることなく)破棄する場合があります(現在、C++ および Go 実装のみがこれを行います)。ランタイム JSON パースは、予約済み名の影響を受けません。

message Foo {
  reserved 2, 15, 9 to 11;
  reserved foo, bar;
}

同じ reserved ステートメントでフィールド名とフィールド番号を混在させることはできないことに注意してください。

.proto から何が生成されるか?

protocol buffer コンパイラ.proto で実行すると、コンパイラは、ファイルで記述したメッセージ型を操作するために必要なコードを、選択した言語で生成します。これには、フィールド値の取得と設定、メッセージの出力ストリームへのシリアライズ、入力ストリームからのメッセージのパースが含まれます。

  • C++ の場合、コンパイラは各 .proto から .h ファイルと .cc ファイルを生成し、ファイルで記述されたメッセージ型ごとにクラスを生成します。
  • Java の場合、コンパイラは各メッセージ型のクラスと、メッセージ クラス インスタンスを作成するための特別な Builder クラスを含む .java ファイルを生成します。
  • Kotlin の場合、Java で生成されたコードに加えて、コンパイラは、改善された Kotlin API を持つ各メッセージ型の .kt ファイルを生成します。これには、メッセージ インスタンスの作成を簡素化する DSL、nullable フィールド アクセサー、およびコピー関数が含まれます。
  • Python は少し異なります。Python コンパイラは、.proto 内の各メッセージ型の静的記述子を含むモジュールを生成します。これは、メタクラス で使用され、ランタイム時に必要な Python データ アクセス クラスを作成します。
  • Go の場合、コンパイラは、ファイル内の各メッセージ型の型を含む .pb.go ファイルを生成します。
  • Ruby の場合、コンパイラは、メッセージ型を含む Ruby モジュールを含む .rb ファイルを生成します。
  • Objective-C の場合、コンパイラは各 .proto から pbobjc.h ファイルと pbobjc.m ファイルを生成し、ファイルで記述されたメッセージ型ごとにクラスを生成します。
  • C# の場合、コンパイラは各 .proto から .cs ファイルを生成し、ファイルで記述されたメッセージ型ごとにクラスを生成します。
  • PHP の場合、コンパイラは、ファイルで記述されたメッセージ型ごとに .php メッセージ ファイルを生成し、コンパイルする各 .proto ファイルに対して .php メタデータ ファイルを生成します。メタデータ ファイルは、有効なメッセージ型を記述子プールにロードするために使用されます。
  • Dart の場合、コンパイラは、ファイル内の各メッセージ型のクラスを含む .pb.dart ファイルを生成します。

各言語の API の使用方法の詳細については、選択した言語のチュートリアルに従ってください。API の詳細については、関連する API リファレンス を参照してください。

スカラー値型

スカラー メッセージ フィールドは、次のいずれかの型を持つことができます。次の表は、.proto ファイルで指定された型と、自動生成されたクラスの対応する型を示しています。

Proto 型
double
float
int32可変長エンコーディングを使用します。負の数をエンコードするには非効率的です。フィールドに負の値が含まれる可能性がある場合は、代わりに sint32 を使用してください。
int64可変長エンコーディングを使用します。負の数をエンコードするには非効率的です。フィールドに負の値が含まれる可能性がある場合は、代わりに sint64 を使用してください。
uint32可変長エンコーディングを使用します。
uint64可変長エンコーディングを使用します。
sint32可変長エンコーディングを使用します。符号付き int 値。これらは、通常の int32 よりも負の数をより効率的にエンコードします。
sint64可変長エンコーディングを使用します。符号付き int 値。これらは、通常の int64 よりも負の数をより効率的にエンコードします。
fixed32常に 4 バイトです。値が 228 より大きいことが多い場合は、uint32 よりも効率的です。
fixed64常に 8 バイトです。値が 256 より大きいことが多い場合は、uint64 よりも効率的です。
sfixed32常に 4 バイトです。
sfixed64常に 8 バイトです。
bool
string文字列には常に UTF-8 エンコードまたは 7 ビット ASCII テキストを含める必要があり、232 より長くすることはできません。
bytes232 を超えない任意のバイト シーケンスを含めることができます。
Proto 型C++ 型Java/Kotlin 型[1]Python 型[3]Go 型Ruby 型C# 型PHP 型Dart 型Rust 型
doubledoubledoublefloatfloat64Floatdoublefloatdoublef64
floatfloatfloatfloatfloat32Floatfloatfloatdoublef32
int32int32_tintintint32Fixnum または Bignum (必要に応じて)intintegerinti32
int64int64_tlongint/long[4]int64Bignumlonginteger/string[6]Int64i64
uint32uint32_tint[2]int/long[4]uint32Fixnum または Bignum (必要に応じて)uintintegerintu32
uint64uint64_tlong[2]int/long[4]uint64Bignumulonginteger/string[6]Int64u64
sint32int32_tintintint32Fixnum または Bignum (必要に応じて)intintegerinti32
sint64int64_tlongint/long[4]int64Bignumlonginteger/string[6]Int64i64
fixed32uint32_tint[2]int/long[4]uint32Fixnum または Bignum (必要に応じて)uintintegerintu32
fixed64uint64_tlong[2]int/long[4]uint64Bignumulonginteger/string[6]Int64u64
sfixed32int32_tintintint32Fixnum または Bignum (必要に応じて)intintegerinti32
sfixed64int64_tlongint/long[4]int64Bignumlonginteger/string[6]Int64i64
boolboolbooleanboolboolTrueClass/FalseClassboolbooleanboolbool
stringstringStringstr/unicode[5]stringString (UTF-8)stringstringStringProtoString
bytesstringByteStringstr (Python 2)、bytes (Python 3)[]byteString (ASCII-8BIT)ByteStringstringListProtoBytes

[1] Kotlin は、符号なし型の場合でも、Java と Kotlin が混在するコードベースでの互換性を確保するために、Java の対応する型を使用します。

[2] Java では、符号なし 32 ビットおよび 64 ビット整数は、符号付きの対応する型を使用して表され、上位ビットは単に符号ビットに格納されます。

[3] いずれの場合も、フィールドに値を設定すると、型チェックが実行され、有効であることが確認されます。

[4] 64 ビットまたは符号なし 32 ビット整数は、デコード時に常に long として表されますが、フィールドの設定時に int が指定されている場合は int にすることができます。いずれの場合も、値を設定するときは、表される型に収まる必要があります。[2] を参照してください。

[5] Python 文字列は、デコード時には unicode として表されますが、ASCII 文字列が指定されている場合は str にすることができます (これは変更される可能性があります)。

[6] 整数は 64 ビット マシンで使用され、文字列は 32 ビット マシンで使用されます。

これらの型をメッセージをシリアライズするときにどのようにエンコードするかについては、Protocol Buffer エンコーディング を参照してください。

フィールドのデフォルト値

メッセージがパースされるとき、エンコードされたメッセージ バイトに特定のフィールドが含まれていない場合、パースされたオブジェクト内のそのフィールドにアクセスすると、そのフィールドのデフォルト値が返されます。デフォルト値は型固有です。

  • 文字列の場合、デフォルト値は空の文字列です。
  • バイトの場合、デフォルト値は空のバイトです。
  • bool の場合、デフォルト値は false です。
  • 数値型の場合、デフォルト値はゼロです。
  • メッセージ フィールドの場合、フィールドは設定されていません。その正確な値は言語に依存します。詳細については、生成されたコード ガイド を参照してください。
  • enum の場合、デフォルト値は最初に定義された enum 値であり、0 である必要があります。Enum のデフォルト値 を参照してください。

繰り返されるフィールドのデフォルト値は空です(通常は適切な言語の空のリスト)。

マップ フィールドのデフォルト値は空です(通常は適切な言語の空のマップ)。

デフォルト スカラー値のオーバーライド

protobuf エディションでは、単数非メッセージ フィールドの明示的なデフォルト値を指定できます。たとえば、SearchRequest.result_per_page フィールドにデフォルト値 10 を指定するとします。

int32 result_per_page = 3 [default = 10];

送信者が result_per_page を指定しない場合、受信者は次の状態を観察します。

  • result_per_page フィールドは存在しません。つまり、has_result_per_page() (hazzer メソッド) メソッドは false を返します。
  • result_per_page の値(「ゲッター」から返される)は 10 です。

送信者が result_per_page の値を送信した場合、デフォルト値 10 は無視され、送信者の値が「ゲッター」から返されます。

生成されたコードでのデフォルトの動作の詳細については、選択した言語の 生成されたコード ガイド を参照してください。

field_presence 機能が IMPLICIT に設定されているフィールドには、明示的なデフォルト値を指定できません。

列挙型

メッセージ型を定義するときに、そのフィールドの 1 つが定義済みの値リストの 1 つだけを持つようにしたい場合があります。たとえば、各 SearchRequestcorpus フィールドを追加するとします。コーパスは、UNIVERSALWEBIMAGESLOCALNEWSPRODUCTS、または VIDEO にすることができます。これは、可能な値ごとに定数を持つ enum をメッセージ定義に追加するだけで非常に簡単に行うことができます。

次の例では、すべての可能な値を持つ Corpus という名前の enum と、Corpus 型のフィールドを追加しました。

enum Corpus {
  CORPUS_UNSPECIFIED = 0;
  CORPUS_UNIVERSAL = 1;
  CORPUS_WEB = 2;
  CORPUS_IMAGES = 3;
  CORPUS_LOCAL = 4;
  CORPUS_NEWS = 5;
  CORPUS_PRODUCTS = 6;
  CORPUS_VIDEO = 7;
}

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 results_per_page = 3;
  Corpus corpus = 4;
}

Enum のデフォルト値

SearchRequest.corpus フィールドのデフォルト値は CORPUS_UNSPECIFIED です。これは、enum で定義された最初の値であるためです。

edition 2023 では、enum 定義で定義された最初の値は、必ず 値がゼロであり、ENUM_TYPE_NAME_UNSPECIFIED または ENUM_TYPE_NAME_UNKNOWN という名前を持つ必要があります。これは、次の理由によるものです。

  • ゼロ値は、proto2 セマンティクスとの互換性のために最初の要素である必要があります。proto2 セマンティクスでは、最初の enum 値は、別の値が明示的に指定されない限り、デフォルトになります。
  • ゼロ値は、proto3 セマンティクスとの互換性のためにも必要です。proto3 セマンティクスでは、ゼロ値は、この enum 型を使用するすべての暗黙的存在フィールドのデフォルト値として使用されます。

また、この最初のデフォルト値には、「この値は指定されていません」以外の意味を持たないことをお勧めします。

SearchRequest.corpus フィールドのような enum フィールドのデフォルト値は、次のように明示的にオーバーライドできます。

  Corpus corpus = 4 [default = CORPUS_UNIVERSAL];

enum 型が option features.enum_type = CLOSED; を使用して proto2 から移行された場合、enum の最初の値に制限はありません。これらの型の enum の最初の値を変更することはお勧めしません。これは、明示的なフィールドのデフォルトなしにその enum 型を使用するフィールドのデフォルト値を変更するためです。

Enum 値のエイリアス

異なる enum 定数に同じ値を割り当てることで、エイリアスを定義できます。これを行うには、allow_alias オプションを true に設定する必要があります。そうしないと、エイリアスが見つかったときに protocol buffer コンパイラが警告メッセージを生成します。すべてのエイリアス値はシリアライズには有効ですが、デシリアライズ時には最初の値のみが使用されます。

enum EnumAllowingAlias {
  option allow_alias = true;
  EAA_UNSPECIFIED = 0;
  EAA_STARTED = 1;
  EAA_RUNNING = 1;
  EAA_FINISHED = 2;
}

enum EnumNotAllowingAlias {
  ENAA_UNSPECIFIED = 0;
  ENAA_STARTED = 1;
  // ENAA_RUNNING = 1;  // Uncommenting this line will cause a warning message.
  ENAA_FINISHED = 2;
}

Enum 定数

列挙子定数は、32 ビット整数の範囲内である必要があります。enum 値はワイヤ上で varint エンコーディング を使用するため、負の値は非効率的であるため推奨されません。前の例のように、メッセージ定義内で enum を定義することも、外部で定義することもできます。これらの enum は、.proto ファイルの任意のメッセージ定義で再利用できます。また、_MessageType_._EnumType_ 構文を使用して、あるメッセージで宣言された enum 型を別のメッセージのフィールドの型として使用することもできます。

言語固有の Enum 実装

enum を使用する .proto で protocol buffer コンパイラを実行すると、生成されたコードには、Java、Kotlin、または C++ の場合は対応する enum が含まれます。または、Python の場合は、ランタイムで生成されたクラスに整数値を持つシンボリック定数のセットを作成するために使用される特別な EnumDescriptor クラスが含まれます。

デシリアライズ中に、認識されない enum 値はメッセージに保持されますが、メッセージがデシリアライズされるときにどのように表現されるかは言語に依存します。C++ や Go など、指定されたシンボルの範囲外の値を持つオープン enum 型をサポートする言語では、不明な enum 値は、単にその基になる整数表現として格納されます。Java などのクローズド enum 型を持つ言語では、enum のケースを使用して認識されない値を表し、特別なアクセサーで基になる整数にアクセスできます。いずれの場合も、メッセージがシリアライズされると、認識されない値はメッセージとともにシリアライズされます。

アプリケーションでメッセージ enum を操作する方法の詳細については、選択した言語の 生成されたコード ガイド を参照してください。

予約値

enum エントリを完全に削除したり、コメントアウトしたりして enum 型を 更新 すると、将来のユーザーは、型に独自の更新を加えるときに数値​​を再利用できます。これにより、後で同じ .proto の古いインスタンスをロードした場合、データの破損、プライバシー バグなど、重大な問題が発生する可能性があります。これが起こらないようにする方法の 1 つは、削除されたエントリの数値​​(および/または名前。JSON シリアライゼーションでも問題が発生する可能性があります)が reserved であることを指定することです。protocol buffer コンパイラは、将来のユーザーがこれらの識別子を使用しようとするとエラーを報告します。max キーワードを使用して、予約済みの数値範囲が可能な最大値までであることを指定できます。

enum Foo {
  reserved 2, 15, 9 to 11, 40 to max;
  reserved FOO, BAR;
}

同じ reserved ステートメントでフィールド名と数値を混在させることはできないことに注意してください。

他のメッセージ型の使用

他のメッセージ型をフィールド型として使用できます。たとえば、各 SearchResponse メッセージに Result メッセージを含めるとします。これを行うには、同じ .protoResult メッセージ型を定義し、SearchResponseResult 型のフィールドを指定できます。

message SearchResponse {
  repeated Result results = 1;
}

message Result {
  string url = 1;
  string title = 2;
  repeated string snippets = 3;
}

定義のインポート

前の例では、Result メッセージ型は SearchResponse と同じファイルで定義されています。フィールド型として使用するメッセージ型が別の .proto ファイルですでに定義されている場合はどうなるでしょうか?

他の .proto ファイルの定義を使用するには、それらをインポートします。別の .proto の定義をインポートするには、ファイルの先頭に import ステートメントを追加します。

import "myproject/other_protos.proto";

デフォルトでは、直接インポートされた .proto ファイルの定義のみを使用できます。ただし、.proto ファイルを新しい場所に移動する必要がある場合があります。.proto ファイルを直接移動し、1 回の変更ですべての呼び出しサイトを更新する代わりに、プレースホルダー .proto ファイルを古い場所に配置して、import public 概念を使用してすべてのインポートを新しい場所に転送できます。

パブリック インポート機能は、protobuf 静的リフレクションを使用する C++ ターゲットだけでなく、Java、Kotlin、TypeScript、JavaScript、GCL でも使用できないことに注意してください。

import public 依存関係は、import public ステートメントを含む proto をインポートするすべてのコードによって推移的に依存される可能性があります。例:

// new.proto
// All definitions are moved here
// old.proto
// This is the proto that all clients are importing.
import public "new.proto";
import "other.proto";
// client.proto
import "old.proto";
// You use definitions from old.proto and new.proto, but not other.proto

プロトコル コンパイラは、-I/--proto_path フラグを使用してプロトコル コンパイラ コマンド ラインで指定されたディレクトリ セットでインポートされたファイルを検索します。フラグが指定されていない場合は、コンパイラが呼び出されたディレクトリを検索します。一般に、--proto_path フラグをプロジェクトのルートに設定し、すべてのインポートに完全修飾名を使用する必要があります。

proto2 および proto3 メッセージ型の使用

proto2 および proto3 メッセージ型をインポートし、それらを edition 2023 メッセージで使用したり、その逆も可能です。

ネストされた型

次の例のように、メッセージ型を他のメッセージ型内で定義して使用できます。ここでは、Result メッセージは SearchResponse メッセージ内で定義されています。

message SearchResponse {
  message Result {
    string url = 1;
    string title = 2;
    repeated string snippets = 3;
  }
  repeated Result results = 1;
}

このメッセージ型を親メッセージ型の外部で再利用する場合は、_Parent_._Type_ として参照します。

message SomeOtherMessage {
  SearchResponse.Result result = 1;
}

メッセージは好きなだけ深くネストできます。以下の例では、Inner という名前の 2 つのネストされた型は、異なるメッセージ内で定義されているため、完全に独立していることに注意してください。

message Outer {       // Level 0
  message MiddleAA {  // Level 1
    message Inner {   // Level 2
      int64 ival = 1;
      bool  booly = 2;
    }
  }
  message MiddleBB {  // Level 1
    message Inner {   // Level 2
      int32 ival = 1;
      bool  booly = 2;
    }
  }
}

メッセージ型の更新

既存のメッセージ型がすべてのニーズを満たさなくなった場合(たとえば、メッセージ形式に追加のフィールドが必要になった場合など)、古い形式で作成されたコードを引き続き使用したい場合でも、心配する必要はありません。バイナリ ワイヤ形式を使用すると、既存のコードを壊すことなくメッセージ型を非常に簡単に更新できます。

Proto ベストプラクティス と次のルールを確認してください。

  • 既存のフィールドのフィールド番号を変更しないでください。フィールド番号を「変更」することは、フィールドを削除し、同じ型の新しいフィールドを追加することと同じです。フィールドの番号を変更する場合は、フィールドの削除 の手順を参照してください。
  • 新しいフィールドを追加した場合でも、以前のメッセージ形式を使用するコードによってシリアライズされたメッセージは、新しく生成されたコードで解析できます。新しいコードが古いコードによって生成されたメッセージと適切に相互作用できるように、これらの要素のデフォルト値を念頭に置いてください。同様に、新しいコードによって作成されたメッセージは古いコードで解析できます。古いバイナリは、解析時に新しいフィールドを単に無視します。詳細については、不明なフィールドのセクションを参照してください。
  • フィールド番号が更新されたメッセージタイプで再利用されない限り、フィールドは削除できます。代わりにフィールドの名前を変更したり、おそらくプレフィックス「OBSOLETE_」を追加したり、フィールド番号を予約済みにしたりして、将来の.protoのユーザーが誤ってその番号を再利用できないようにすることができます。
  • int32uint32int64uint64、およびboolはすべて互換性があります。つまり、前方互換性または後方互換性を損なうことなく、これらのタイプのいずれかから別のタイプにフィールドを変更できます。ワイヤから解析された数値が対応する型に適合しない場合、C++で数値をその型にキャストした場合と同じ効果が得られます(たとえば、64ビット数値がint32として読み取られる場合、32ビットに切り捨てられます)。
  • sint32sint64は互いに互換性がありますが、他の整数型とは互換性がありません。
  • stringbytesは、バイトが有効なUTF-8である限り互換性があります。
  • 埋め込みメッセージは、バイトにメッセージのエンコードされたインスタンスが含まれている場合、bytesと互換性があります。
  • fixed32sfixed32と、fixed64sfixed64と互換性があります。
  • stringbytes、およびメッセージフィールドの場合、singularはrepeatedと互換性があります。repeatedフィールドのシリアライズされたデータが入力として与えられた場合、このフィールドがsingularであると予想するクライアントは、それがプリミティブ型フィールドである場合は最後の入力値を、メッセージ型フィールドである場合はすべての入力要素をマージします。これは、boolやenumを含む数値型に対しては一般的に安全ではないことに注意してください。数値型のrepeatedフィールドは、デフォルトでpacked形式でシリアライズされますが、singularフィールドが予期される場合には正しく解析されません。
  • enumは、ワイヤ形式の点でint32uint32int64、およびuint64と互換性があります(値が適合しない場合は切り捨てられることに注意してください)。ただし、メッセージがデシリアライズされるときにクライアントコードがそれらを異なる方法で処理する可能性があることに注意してください。たとえば、認識されないenum値はメッセージに保持されますが、メッセージがデシリアライズされるときにどのように表現されるかは言語に依存します。Intフィールドは常にその値を保持するだけです。
  • 単一のoptionalフィールドまたは拡張機能を新しいoneofのメンバーに変更することはバイナリ互換性がありますが、一部の言語(特にGo)では、生成されたコードのAPIは互換性のない方法で変更されます。このため、GoogleはAIP-180で文書化されているように、パブリックAPIでそのような変更を行いません。ソース互換性に関する同じ注意点がありますが、複数のフィールドを新しいoneofに移動することは、一度に複数のフィールドを設定するコードがないことを確認している場合は安全かもしれません。フィールドを既存のoneofに移動することは安全ではありません。同様に、単一のフィールドoneofoptionalフィールドまたは拡張機能に変更することは安全です。
  • map<K, V>と対応するrepeatedメッセージフィールドの間でフィールドを変更することはバイナリ互換性があります(メッセージレイアウトおよびその他の制限については、以下のマップを参照してください)。ただし、変更の安全性はアプリケーションに依存します。メッセージをデシリアライズおよび再シリアライズするとき、repeatedフィールド定義を使用するクライアントは意味的に同一の結果を生成します。ただし、mapフィールド定義を使用するクライアントは、エントリを並べ替えたり、重複するキーを持つエントリを削除したりする可能性があります。

不明なフィールド

不明なフィールドは、パーサーが認識しないフィールドを表す整形式のプロトコルバッファシリアライズされたデータです。たとえば、古いバイナリが新しいフィールドを持つ新しいバイナリによって送信されたデータを解析する場合、それらの新しいフィールドは古いバイナリで不明なフィールドになります。

Editionsメッセージは不明なフィールドを保持し、解析中およびシリアライズされた出力に含めます。これはproto2およびproto3の動作と一致します。

不明なフィールドの保持

一部のアクションは、不明なフィールドが失われる原因となる可能性があります。たとえば、次のいずれかを行うと、不明なフィールドが失われます

  • プロトをJSONにシリアライズします。
  • メッセージ内のすべてのフィールドを反復処理して、新しいメッセージを移入します。

不明なフィールドの損失を防ぐには、次の手順を実行します

  • バイナリを使用します。データ交換にはテキスト形式を使用しないでください。
  • フィールドごとではなく、データコピーにCopyFrom()MergeFrom()などのメッセージ指向APIを使用します

TextFormatは少し特殊なケースです。TextFormatへのシリアライズは、フィールド番号を使用して不明なフィールドを出力します。しかし、フィールド番号を使用するエントリがある場合、TextFormatデータをバイナリプロトに解析し直すと失敗します。

拡張機能

拡張機能は、コンテナメッセージの外部で定義されたフィールドです。通常は、コンテナメッセージの.protoファイルとは別の.protoファイルにあります。

拡張機能を使用する理由

拡張機能を使用する主な理由は2つあります

  • コンテナメッセージの.protoファイルのインポート/依存関係が少なくなります。これにより、ビルド時間が改善され、循環依存関係が解消され、それ以外の場合は疎結合が促進されます。拡張機能はこれに非常に適しています。
  • システムが最小限の依存関係と調整でコンテナメッセージにデータを添付できるようにします。拡張機能は、フィールド番号のスペースが限られていることと、フィールド番号の再利用の結果のために、これには最適なソリューションではありません。ユースケースで多数の拡張機能に対して非常に低い調整が必要な場合は、代わりにAnyメッセージタイプの使用を検討してください。

拡張機能の例

拡張機能の例を見てみましょう

// file kittens/video_ext.proto

import "kittens/video.proto";
import "media/user_content.proto";

package kittens;

// This extension allows kitten videos in a media.UserContent message.
extend media.UserContent {
  // Video is a message imported from kittens/video.proto
  repeated Video kitten_videos = 126;
}

拡張機能を定義するファイル(kittens/video_ext.proto)は、コンテナメッセージのファイル(media/user_content.proto)をインポートすることに注意してください。

コンテナメッセージは、拡張機能用にフィールド番号のサブセットを予約する必要があります。

// file media/user_content.proto

package media;

// A container message to hold stuff that a user has created.
message UserContent {
  // Set verification to `DECLARATION` to enforce extension declarations for all
  // extensions in this range.
  extensions 100 to 199 [verification = DECLARATION];
}

コンテナメッセージのファイル(media/user_content.proto)は、拡張機能用にフィールド番号[100〜199]を予約するメッセージUserContentを定義します。すべての拡張機能の宣言を必須にするには、範囲にverification = DECLARATIONを設定することをお勧めします。

新しい拡張機能(kittens/video_ext.proto)が追加されると、対応する宣言をUserContentに追加する必要があり、verificationを削除する必要があります。

// A container message to hold stuff that a user has created.
message UserContent {
  extensions 100 to 199 [
    declaration = {
      number: 126,
      full_name: ".kittens.kitten_videos",
      type: ".kittens.Video",
      repeated: true
    },
    // Ensures all field numbers in this extension range are declarations.
    verification = DECLARATION
  ];
}

UserContentは、フィールド番号126が完全修飾名.kittens.kitten_videosと完全修飾型.kittens.Videoを持つrepeated拡張フィールドによって使用されることを宣言します。拡張機能の宣言の詳細については、拡張機能の宣言を参照してください。

コンテナメッセージのファイル(media/user_content.proto)は、kitten_video拡張機能の定義(kittens/video_ext.proto)をインポートしないことに注意してください。

拡張フィールドのワイヤ形式エンコーディングは、同じフィールド番号、型、およびカーディナリティを持つ標準フィールドと比較して違いはありません。したがって、フィールド番号、型、およびカーディナリティが一定である限り、標準フィールドをコンテナから拡張機能として移動したり、拡張フィールドをコンテナメッセージに標準フィールドとして移動したりすることは安全です。

ただし、拡張機能はコンテナメッセージの外部で定義されているため、特定の拡張フィールドを取得および設定するための特殊なアクセサは生成されません。例として、protobufコンパイラはAddKittenVideos()またはGetKittenVideos()アクセサを生成しません。代わりに、拡張機能は、HasExtension()ClearExtension()GetExtension()MutableExtension()、およびAddExtension()のようなパラメータ化された関数を介してアクセスされます。

C++では、次のようになります

UserContent user_content;
user_content.AddExtension(kittens::kitten_videos, new kittens::Video());
assert(1 == user_content.GetExtensionCount(kittens::kitten_videos));
user_content.GetExtension(kittens::kitten_videos, 0);

拡張範囲の定義

コンテナメッセージの所有者である場合は、メッセージへの拡張機能の拡張範囲を定義する必要があります。

拡張フィールドに割り当てられたフィールド番号は、標準フィールドに再利用できません。

拡張範囲は、定義後に拡張しても安全です。適切なデフォルトは、1000個の比較的小さな数値を割り当て、拡張機能の宣言を使用してそのスペースを密に埋めることです

message ModernExtendableMessage {
  // All extensions in this range should use extension declarations.
  extensions 1000 to 2000 [verification = DECLARATION];
}

実際の拡張機能の前に拡張機能宣言の範囲を追加する場合は、この新しい範囲に宣言が使用されるように強制するためにverification = DECLARATIONを追加する必要があります。このプレースホルダーは、実際の宣言が追加されると削除できます。

既存の拡張範囲を同じ合計範囲をカバーする別々の範囲に分割しても安全です。これは、レガシーメッセージタイプを拡張機能の宣言に移行するために必要な場合があります。たとえば、移行前は、範囲は次のように定義されている場合があります

message LegacyMessage {
  extensions 1000 to max;
}

移行後(範囲の分割)は、次のようになります

message LegacyMessage {
  // Legacy range that was using an unverified allocation scheme.
  extensions 1000 to 524999999 [verification = UNVERIFIED];
  // Current range that uses extension declarations.
  extensions 525000000 to max  [verification = DECLARATION];
}

拡張範囲を移動または縮小するために、開始フィールド番号を増やしたり、終了フィールド番号を減らしたりすることは安全ではありません。これらの変更は、既存の拡張機能を無効にする可能性があります。

プロトのほとんどのインスタンスで移入される標準フィールドには、フィールド番号1〜15を使用することをお勧めします。これらの番号を拡張機能に使用することはお勧めしません。

番号付け規則に非常に大きなフィールド番号を持つ拡張機能が含まれる可能性がある場合は、拡張範囲がmaxキーワードを使用して可能な最大のフィールド番号まで拡張されるように指定できます

message Foo {
  extensions 1000 to max;
}

maxは229 - 1、つまり536,870,911です。

拡張番号の選択

拡張機能は、コンテナメッセージの外部で指定できるフィールドにすぎません。フィールド番号の割り当てに関するすべての同じルールが、拡張フィールド番号に適用されます。フィールド番号の再利用の結果も、拡張フィールド番号の再利用に適用されます。

コンテナメッセージが拡張機能の宣言を使用している場合、一意の拡張フィールド番号を選択するのは簡単です。新しい拡張機能を定義するときは、コンテナメッセージで定義された最大の拡張範囲からの他のすべての宣言よりも上の最小のフィールド番号を選択します。たとえば、コンテナメッセージが次のように定義されている場合

message Container {
  // Legacy range that was using an unverified allocation scheme
  extensions 1000 to 524999999;
  // Current range that uses extension declarations. (highest extension range)
  extensions 525000000 to max  [
    declaration = {
      number: 525000001,
      full_name: ".bar.baz_ext",
      type: ".bar.Baz"
    }
    // 525,000,002 is the lowest field number above all other declarations
  ];
}

Containerの次の拡張機能は、番号525000002で新しい宣言を追加する必要があります。

未検証の拡張機能番号割り当て(推奨されません)

コンテナメッセージの所有者は、拡張機能の宣言を放棄して、独自の未検証の拡張機能番号割り当て戦略を支持することを選択できます。

未検証の割り当てスキームは、選択された拡張範囲内で拡張フィールド番号を割り当てるために、protobufエコシステムの外部にあるメカニズムを使用します。1つの例として、モノレポのコミット番号を使用することが考えられます。このシステムは、protobufコンパイラの観点からは「未検証」です。拡張機能が適切に取得された拡張フィールド番号を使用しているかどうかを確認する方法がないためです。

検証済みシステムのような拡張機能の宣言に対する未検証システムの利点は、コンテナメッセージの所有者と調整せずに拡張機能を定義できることです。

未検証システムの欠点は、protobufコンパイラが参加者を拡張フィールド番号の再利用から保護できないことです。

未検証の拡張フィールド番号割り当て戦略は推奨されていませんフィールド番号の再利用の結果は、メッセージのすべてのエクステンダ(推奨事項に従わなかった開発者だけでなく)に当てはまるためです。ユースケースで非常に低い調整が必要な場合は、代わりにAnyメッセージの使用を検討してください。

未検証の拡張フィールド番号割り当て戦略は、範囲1〜524,999,999に制限されています。フィールド番号525,000,000以上は、拡張機能の宣言でのみ使用できます。

拡張型の指定

拡張機能は、oneofmapを除く任意のフィールド型にすることができます。

ネストされた拡張機能(非推奨)

別のメッセージのスコープで拡張機能を宣言できます

import "common/user_profile.proto";

package puppies;

message Photo {
  extend common.UserProfile {
    int32 likes_count = 111;
  }
  ...
}

この場合、この拡張機能にアクセスするためのC++コードは次のとおりです

UserProfile user_profile;
user_profile.SetExtension(puppies::Photo::likes_count, 42);

言い換えれば、唯一の効果は、likes_countpuppies.Photoのスコープ内で定義されていることです。

これは一般的な混乱の原因です。メッセージタイプ内でネストされたextendブロックを宣言しても、外部タイプと拡張タイプの間に関係があるとは限りません。特に、前の例は、PhotoUserProfileの何らかのサブクラスであることを意味するものではありません。それが意味するのは、シンボルlikes_countPhotoのスコープ内で宣言されているということです。それは単なる静的メンバーです。

一般的なパターンは、拡張機能のフィールドタイプのスコープ内で拡張機能を定義することです。たとえば、タイプpuppies.Photomedia.UserContentへの拡張機能を次に示します。拡張機能はPhotoの一部として定義されています

import "media/user_content.proto";

package puppies;

message Photo {
  extend media.UserContent {
    Photo puppy_photo = 127;
  }
  ...
}

ただし、メッセージ型を持つ拡張機能をその型内で定義する必要はありません。標準定義パターンも使用できます

import "media/user_content.proto";

package puppies;

message Photo {
  ...
}

// This can even be in a different file.
extend media.UserContent {
  Photo puppy_photo = 127;
}

この標準(ファイルレベル)構文が推奨されます。混乱を避けるためです。ネストされた構文は、拡張機能にまだ慣れていないユーザーによってサブクラス化と誤解されることがよくあります。

Any

Anyメッセージタイプを使用すると、.proto定義なしでメッセージを埋め込み型として使用できます。Anyには、bytesとしての任意のシリアライズされたメッセージと、そのメッセージのタイプに対してグローバルに一意の識別子として機能し、解決されるURLが含まれています。Anyタイプを使用するには、google/protobuf/any.protoインポートする必要があります。

import "google/protobuf/any.proto";

message ErrorStatus {
  string message = 1;
  repeated google.protobuf.Any details = 2;
}

特定のメッセージタイプのデフォルトのタイプURLは、type.googleapis.com/_packagename_._messagename_です。

言語実装が異なれば、タイプセーフな方法でAny値をパックおよびアンパックするためのランタイムライブラリヘルパーがサポートされます。たとえば、Javaでは、Anyタイプには特別なpack()およびunpack()アクセサがあり、C++にはPackFrom()およびUnpackTo()メソッドがあります

// Storing an arbitrary message type in Any.
NetworkErrorDetails details = ...;
ErrorStatus status;
status.add_details()->PackFrom(details);

// Reading an arbitrary message from Any.
ErrorStatus status = ...;
for (const google::protobuf::Any& detail : status.details()) {
  if (detail.Is<NetworkErrorDetails>()) {
    NetworkErrorDetails network_error;
    detail.UnpackTo(&network_error);
    ... processing network_error ...
  }
}

含まれるメッセージを少数のタイプに制限し、リストに新しいタイプを追加する前に許可を要求する場合は、Anyメッセージタイプの代わりに、拡張機能の宣言を持つ拡張機能の使用を検討してください。

Oneof

多くのsingularフィールドがあり、一度に最大1つのフィールドが設定されるメッセージがある場合は、oneof機能を使用することでこの動作を強制し、メモリを節約できます。

Oneofフィールドはsingularフィールドに似ていますが、oneof内のすべてのフィールドがメモリを共有し、一度に最大1つのフィールドを設定できます。oneofのメンバーを設定すると、他のすべてのメンバーが自動的にクリアされます。選択した言語に応じて、特別なcase()またはWhichOneof()メソッドを使用して、oneofでどの値が設定されているか(ある場合)を確認できます。

複数の値が設定されている場合、プロトの順序によって決定される最後に設定された値は、以前のすべての値を上書きすることに注意してください。

oneofフィールドのフィールド番号は、囲みメッセージ内で一意である必要があります。

Oneof の使用

.protoでoneofを定義するには、oneofキーワードの後にoneof名を続けます。この場合はtest_oneof

message SampleMessage {
  oneof test_oneof {
    string name = 4;
    SubMessage sub_message = 9;
  }
}

次に、oneofフィールドをoneof定義に追加します。mapフィールドとrepeatedフィールドを除く、任意の型のフィールドを追加できます。repeatedフィールドをoneofに追加する必要がある場合は、repeatedフィールドを含むメッセージを使用できます。

生成されたコードでは、oneofフィールドは通常のフィールドと同じゲッターとセッターを持っています。また、oneofでどの値が設定されているか(ある場合)を確認するための特別なメソッドも取得します。選択した言語のoneof APIの詳細については、関連するAPIリファレンスを参照してください。

Oneof の機能

  • oneofフィールドを設定すると、oneofの他のすべてのメンバーが自動的にクリアされます。したがって、複数のoneofフィールドを設定すると、設定した最後のフィールドのみが値を持ち続けます。

    SampleMessage message;
    message.set_name("name");
    CHECK(message.has_name());
    // Calling mutable_sub_message() will clear the name field and will set
    // sub_message to a new instance of SubMessage with none of its fields set.
    message.mutable_sub_message();
    CHECK(!message.has_name());
    
  • パーサーがワイヤ上で同じoneofの複数のメンバーを検出した場合、最後に検出されたメンバーのみが解析されたメッセージで使用されます。ワイヤ上のデータを解析するとき、バイトの先頭から開始し、次の値を評価し、次の解析ルールを適用します

    • 最初に、同じoneof内の異なるフィールドが現在設定されているかどうかを確認し、設定されている場合はクリアします。

    • 次に、フィールドがoneofに含まれていないかのようにコンテンツを適用します

      • プリミティブは、すでに設定されている値を上書きします
      • メッセージは、すでに設定されている値にマージされます
  • 拡張機能はoneofではサポートされていません。

  • oneofをrepeatedにすることはできません。

  • リフレクションAPIはoneofフィールドで機能します。

  • oneofフィールドをデフォルト値(たとえば、int32 oneofフィールドを0に設定するなど)に設定すると、そのoneofフィールドの「ケース」が設定され、値がワイヤ上でシリアライズされます。

  • C++を使用している場合は、コードがメモリクラッシュを引き起こさないようにしてください。次のサンプルコードは、sub_messageset_name()メソッドを呼び出すことによって既に削除されているため、クラッシュします。

    SampleMessage message;
    SubMessage* sub_message = message.mutable_sub_message();
    message.set_name("name");      // Will delete sub_message
    sub_message->set_...            // Crashes here
    
  • C++でもう一度、oneofを持つ2つのメッセージをSwap()すると、各メッセージは他方のoneofケースで終わります。以下の例では、msg1sub_messageを持ち、msg2nameを持ちます。

    SampleMessage msg1;
    msg1.set_name("name");
    SampleMessage msg2;
    msg2.mutable_sub_message();
    msg1.swap(&msg2);
    CHECK(msg1.has_sub_message());
    CHECK(msg2.has_name());
    

下位互換性の問題

oneofフィールドを追加または削除するときは注意してください。oneofの値をチェックするとNone/NOT_SETが返される場合、oneofが設定されていないか、oneofの異なるバージョンのフィールドに設定されている可能性があります。ワイヤ上の不明なフィールドがoneofのメンバーであるかどうかを知る方法がないため、違いを区別する方法はありません。

タグの再利用の問題

  • singularフィールドをoneofの内外に移動する:メッセージがシリアライズおよび解析された後、情報の一部(一部のフィールドはクリアされます)が失われる可能性があります。ただし、単一のフィールドを新しいoneofに安全に移動でき、1つだけが設定されていることがわかっている場合は、複数のフィールドを移動できる場合があります。詳細については、メッセージタイプの更新を参照してください。
  • oneofフィールドを削除して再度追加する:これにより、メッセージがシリアライズおよび解析された後、現在設定されているoneofフィールドがクリアされる可能性があります。
  • oneofを分割またはマージする:これは、singularフィールドの移動と同様の問題があります。

マップ

データ定義の一部として連想マップを作成する場合は、プロトコルバッファは便利なショートカット構文を提供します

map<key_type, value_type> map_field = N;

…ここで、key_typeは任意の整数型または文字列型(したがって、浮動小数点型とbytesを除く任意のスカラー型)にすることができます。enumもprotoメッセージもkey_typeには有効でないことに注意してください。value_typeは、別のマップを除く任意の型にすることができます。

したがって、たとえば、各Projectメッセージが文字列キーに関連付けられているプロジェクトのマップを作成する場合は、次のように定義できます

map<string, Project> projects = 3;

マップ機能

  • 拡張機能はマップではサポートされていません。
  • マップフィールドをrepeatedにすることはできません。
  • マップ値のワイヤ形式の順序とマップ反復の順序は未定義であるため、マップアイテムが特定の順序になっていることを信頼することはできません。
  • .protoのテキスト形式を生成する場合、マップはキーでソートされます。数値キーは数値的にソートされます。
  • ワイヤから解析する場合、またはマージする場合、重複するマップキーがある場合は、最後に検出されたキーが使用されます。テキスト形式からマップを解析する場合、重複するキーがあると解析が失敗する可能性があります。
  • マップフィールドにキーを提供したが値を提供しない場合、フィールドがシリアライズされるときの動作は言語に依存します。C++、Java、Kotlin、およびPythonでは、型のデフォルト値がシリアライズされますが、他の言語では何もシリアライズされません。
  • マップfooと同じスコープにシンボルFooEntryが存在することはできません。FooEntryはマップの実装ですでに使用されているためです。

生成されたマップAPIは現在、サポートされているすべての言語で利用可能です。選択した言語のマップAPIの詳細については、関連するAPIリファレンスを参照してください。

下位互換性

マップ構文はワイヤ上で次と同等であるため、マップをサポートしていないプロトコルバッファ実装でもデータを処理できます

message MapFieldEntry {
  key_type key = 1;
  value_type value = 2;
}

repeated MapFieldEntry map_field = N;

マップをサポートするプロトコルバッファ実装は、以前の定義で受け入れられるデータを生成および受け入れる必要があります。

パッケージ

オプションのpackage指定子を.protoファイルに追加して、プロトコルメッセージタイプ間の名前の衝突を防ぐことができます。

package foo.bar;
message Open { ... }

次に、メッセージタイプのフィールドを定義するときにパッケージ指定子を使用できます

message Foo {
  ...
  foo.bar.Open open = 1;
  ...
}

パッケージ指定子が生成されたコードにどのように影響するかは、選択した言語によって異なります

  • C++では、生成されたクラスはC++名前空間内にラップされます。たとえば、Openは名前空間foo::barにあります。
  • JavaおよびKotlinでは、.protoファイルで明示的にoption java_packageを提供しない限り、パッケージはJavaパッケージとして使用されます。
  • Pythonでは、Pythonモジュールはファイルシステム内の場所に従って編成されているため、packageディレクティブは無視されます。
  • Goでは、packageディレクティブは無視され、生成された.pb.goファイルは、対応するgo_proto_library Bazelルールにちなんで名付けられたパッケージにあります。オープンソースプロジェクトの場合、go_packageオプションを提供するか、Bazel -Mフラグを設定する必要があります。
  • Rubyでは、生成されたクラスは、必要なRuby大文字と小文字スタイルに変換されたネストされたRuby名前空間内にラップされます(最初の文字は大文字。最初の文字が文字でない場合は、PB_が先頭に追加されます)。たとえば、Openは名前空間Foo::Barにあります。
  • PHPでは、.protoファイルで明示的にoption php_namespaceを提供しない限り、パッケージはPascalCaseに変換された後、名前空間として使用されます。たとえば、Openは名前空間Foo\Barにあります。
  • C#では、.protoファイルで明示的にoption csharp_namespaceを提供しない限り、パッケージはPascalCaseに変換された後、名前空間として使用されます。たとえば、Openは名前空間Foo.Barにあります。

たとえばPythonのように、packageディレクティブが生成されたコードに直接影響を与えない場合でも、.protoファイルのパッケージを指定することを強くお勧めします。そうしないと、記述子で名前の衝突が発生し、プロトが他の言語で移植できなくなる可能性があるためです。

パッケージと名前解決

プロトコルバッファ言語の型名解決はC++のように機能します。最初に最も内側のスコープが検索され、次に次の内側のスコープが検索され、各パッケージは親パッケージに対して「内側」と見なされます。先頭の「.」(たとえば、.foo.bar.Baz)は、代わりに最も外側のスコープから開始することを意味します。

プロトコルバッファコンパイラは、インポートされた.protoファイルを解析することにより、すべての型名を解決します。各言語のコードジェネレーターは、スコープ規則が異なっていても、その言語で各タイプを参照する方法を知っています。

サービスの定義

メッセージタイプをRPC(リモートプロシージャコール)システムで使用する場合は、.protoファイルでRPCサービスインターフェイスを定義できます。プロトコルバッファコンパイラは、選択した言語でサービスインターフェイスコードとスタブを生成します。たとえば、SearchRequestを受け取り、SearchResponseを返すメソッドを持つRPCサービスを定義する場合は、.protoファイルで次のように定義できます

service SearchService {
  rpc Search(SearchRequest) returns (SearchResponse);
}

プロトコルバッファで使用する最も簡単なRPCシステムは、gRPCです。Googleで開発された言語およびプラットフォームニュートラルなオープンソースRPCシステムです。gRPCはプロトコルバッファとうまく連携し、特別なプロトコルバッファコンパイラプラグインを使用して.protoファイルから直接関連するRPCコードを生成できます。

gRPCを使用しない場合は、独自のRPC実装でプロトコルバッファを使用することもできます。詳細については、Proto2言語ガイドを参照してください。

プロトコルバッファのRPC実装を開発するための進行中のサードパーティプロジェクトも多数あります。私たちが知っているプロジェクトへのリンクのリストについては、サードパーティアドオンwikiページを参照してください。

JSON マッピング

標準のprotobufバイナリワイヤ形式は、protobufを使用する2つのシステム間の通信に推奨されるシリアライズ形式です。protobufワイヤ形式ではなくJSONを使用するシステムとの通信の場合、ProtobufはProtoJSONでの正規エンコーディングをサポートしています。

オプション

.protoファイルの個々の宣言は、多数のオプションで注釈を付けることができます。オプションは宣言の全体的な意味を変更しませんが、特定のコンテキストでの処理方法に影響を与える可能性があります。使用可能なオプションの完全なリストは、/google/protobuf/descriptor.protoで定義されています。

一部のオプションはファイルレベルオプションであり、メッセージ、enum、またはサービス定義の内側ではなく、トップレベルスコープで記述する必要があります。一部のオプションはメッセージレベルオプションであり、メッセージ定義の内側に記述する必要があります。一部のオプションはフィールドレベルオプションであり、フィールド定義の内側に記述する必要があります。オプションは、enum型、enum値、oneofフィールド、サービス型、およびサービスメソッドにも記述できます。ただし、現在、これらのいずれにも有用なオプションは存在しません。

最も一般的に使用されるオプションを次に示します

  • java_package(ファイルオプション):生成されたJava/Kotlinクラスに使用するパッケージ。.protoファイルで明示的なjava_packageオプションが指定されていない場合、デフォルトではプロトパッケージ(.protoファイルで「package」キーワードを使用して指定)が使用されます。ただし、プロトパッケージは通常、リバースドメイン名で始まることが期待されていないため、適切なJavaパッケージではありません。JavaまたはKotlinコードを生成しない場合、このオプションは効果がありません。

    option java_package = "com.example.foo";
    
  • java_outer_classname(ファイルオプション):生成するラッパークラスJavaクラスのクラス名(したがってファイル名)。.protoファイルで明示的なjava_outer_classnameが指定されていない場合、クラス名は.protoファイル名をキャメルケースに変換することによって構築されます(したがって、foo_bar.protoFooBar.javaになります)。java_multiple_filesオプションが無効になっている場合、.protoファイル用に生成された他のすべてのクラス/enumなどは、ネストされたクラス/enumなどとして、この外部ラッパークラスに生成されます。Javaコードを生成しない場合、このオプションは効果がありません。

    option java_outer_classname = "Ponycopter";
    
  • java_multiple_files(ファイルオプション):falseの場合、この.protoファイルに対して単一の.javaファイルのみが生成され、トップレベルのメッセージ、サービス、および列挙型用に生成されたすべてのJavaクラス/enumなどは、外部クラス(java_outer_classnameを参照)の内部にネストされます。trueの場合、トップレベルのメッセージ、サービス、および列挙型用に生成された各Javaクラス/enumなどに対して個別の.javaファイルが生成され、この.protoファイル用に生成されたラッパークラスJavaクラスには、ネストされたクラス/enumなどは含まれません。これは、デフォルトでfalseになるBooleanオプションです。Javaコードを生成しない場合、このオプションは効果がありません。

    option java_multiple_files = true;
    
  • optimize_for(ファイルオプション):SPEEDCODE_SIZE、またはLITE_RUNTIMEに設定できます。これは、C++およびJavaコードジェネレーター(およびおそらくサードパーティジェネレーター)に次の方法で影響します

    • SPEED(デフォルト):プロトコルバッファコンパイラは、メッセージタイプのシリアライズ、解析、およびその他の一般的な操作を実行するためのコードを生成します。このコードは高度に最適化されています。
    • CODE_SIZE:プロトコルバッファコンパイラは、最小限のクラスを生成し、シリアライズ、解析、およびその他のさまざまな操作を実装するために、共有の反射ベースのコードに依存します。したがって、生成されたコードはSPEEDよりもはるかに小さくなりますが、操作は遅くなります。クラスは、SPEEDモードの場合とまったく同じパブリックAPIを実装します。このモードは、非常に多数の.protoファイルが含まれており、それらすべてを非常に高速にする必要がないアプリで最も役立ちます。
    • LITE_RUNTIME:プロトコルバッファコンパイラは、「lite」ランタイムライブラリ(libprotobufではなくlibprotobuf-lite)のみに依存するクラスを生成します。liteランタイムは、完全なライブラリよりもはるかに小さく(約1桁小さい)、記述子やリフレクションなどの特定の機能を省略します。これは、携帯電話などの制約のあるプラットフォームで実行されているアプリに特に役立ちます。コンパイラは、SPEEDモードの場合と同様に、すべてのメソッドの高速実装を生成します。生成されたクラスは、各言語のMessageLiteインターフェイスのみを実装します。これは、完全なMessageインターフェイスのメソッドのサブセットのみを提供します。
    option optimize_for = CODE_SIZE;
    
  • cc_generic_servicesjava_generic_servicespy_generic_services(ファイルオプション):汎用サービスは非推奨になりました。プロトコルバッファコンパイラが、それぞれC++、Java、およびPythonのサービス定義に基づいて抽象サービスコードを生成する必要があるかどうか。レガシー上の理由から、これらはデフォルトでtrueになります。ただし、バージョン2.3.0(2010年1月)以降、RPC実装が「抽象」サービスに依存するのではなく、各システムに固有のコードを生成するためにコードジェネレータープラグインを提供することが好ましいと考えられています。

    // This file relies on plugins to generate service code.
    option cc_generic_services = false;
    option java_generic_services = false;
    option py_generic_services = false;
    
  • cc_enable_arenas(ファイルオプション):C++生成コードのアリーナ割り当てを有効にします。

  • objc_class_prefix(ファイルオプション):この.protoから生成されたすべてのObjective-C生成クラスおよびenumにプレフィックスが付加されるObjective-Cクラスプレフィックスを設定します。デフォルトはありません。Appleが推奨するように、3〜5文字の大文字のプレフィックスを使用する必要があります。2文字のプレフィックスはすべてAppleによって予約されていることに注意してください。

  • packed(フィールドオプション):protobufエディションでは、このオプションはtrueにロックされています。アンパックされたワイヤ形式を使用するには、エディション機能を使用してこのオプションをオーバーライドできます。これは、次の例に示すように、バージョン2.3.0より前のパーサーとの互換性を提供します(まれに必要な場合)

    repeated int32 samples = 4 [features.repeated_field_encoding = EXPANDED];
    
  • deprecated(フィールドオプション):trueに設定すると、フィールドが非推奨であり、新しいコードで使用すべきではないことを示します。ほとんどの言語では、これは実際には効果がありません。Javaでは、これは@Deprecatedアノテーションになります。C++の場合、clang-tidyは非推奨フィールドが使用されるたびに警告を生成します。将来的には、他の言語固有のコードジェネレーターがフィールドのアクセサに非推奨アノテーションを生成する可能性があり、これにより、フィールドを使用しようとするコードをコンパイルするときに警告が発行されます。フィールドが誰にも使用されておらず、新しいユーザーが使用するのを防ぎたい場合は、フィールド宣言をreservedステートメントに置き換えることを検討してください。

    int32 old_field = 6 [deprecated = true];
    

Enum 値オプション

Enum値オプションがサポートされています。deprecatedオプションを使用して、値がもう使用すべきではないことを示すことができます。拡張機能を使用してカスタムオプションを作成することもできます。

次の例は、これらのオプションを追加するための構文を示しています

import "google/protobuf/descriptor.proto";

extend google.protobuf.EnumValueOptions {
  string string_name = 123456789;
}

enum Data {
  DATA_UNSPECIFIED = 0;
  DATA_SEARCH = 1 [deprecated = true];
  DATA_DISPLAY = 2 [
    (string_name) = "display_value"
  ];
}

string_nameオプションを読み取るためのC++コードは、次のようになります

const absl::string_view foo = proto2::GetEnumDescriptor<Data>()
    ->FindValueByName("DATA_DISPLAY")->options().GetExtension(string_name);

enum値とフィールドにカスタムオプションを適用する方法については、カスタムオプションを参照してください。

カスタム オプション

プロトコルバッファでは、独自のオプションを定義して使用することもできます。これは、ほとんどの人が必要としない高度な機能であることに注意してください。独自のオプションを作成する必要があると思われる場合は、詳細についてProto2言語ガイドを参照してください。カスタムオプションの作成には拡張機能を使用することに注意してください。

オプションの保持

オプションには、オプションが生成されたコードに保持されるかどうかを制御する保持の概念があります。オプションはデフォルトでランタイム保持を持ちます。つまり、オプションは生成されたコードに保持され、生成された記述子プールでランタイムに表示されます。ただし、retention = RETENTION_SOURCEを設定して、オプション(またはオプション内のフィールド)をランタイムで保持しないように指定できます。これはソース保持と呼ばれます。

オプションの保持は高度な機能であり、ほとんどのユーザーは気にする必要はありませんが、バイナリにオプションを保持するためのコードサイズコストをかけずに特定のオプションを使用したい場合には役立ちます。ソース保持を持つオプションは、protoc および protoc プラグインからは引き続き可視であるため、コードジェネレーターはそれらを使用して動作をカスタマイズできます。

保持は、オプションに直接設定できます。例えば、このようにです。

extend google.protobuf.FileOptions {
  int32 source_retention_option = 1234
      [retention = RETENTION_SOURCE];
}

また、プレーンフィールドに設定することもできます。その場合、そのフィールドがオプション内に現れる場合にのみ有効になります。

message OptionsMessage {
  int32 source_retention_field = 1 [retention = RETENTION_SOURCE];
}

必要であれば retention = RETENTION_RUNTIME を設定できますが、これはデフォルトの動作であるため、効果はありません。メッセージフィールドが RETENTION_SOURCE とマークされている場合、その内容全体が削除されます。内部のフィールドが RETENTION_RUNTIME を設定しようとしても、それをオーバーライドすることはできません。

オプションのターゲット

フィールドには targets オプションがあり、オプションとして使用される場合にフィールドが適用されるエンティティのタイプを制御します。たとえば、フィールドに targets = TARGET_TYPE_MESSAGE がある場合、そのフィールドは enum(または他の非メッセージエンティティ)のカスタムオプションには設定できません。Protoc はこれを強制し、ターゲット制約に違反がある場合はエラーを発生させます。

一見すると、この機能は不要に思えるかもしれません。なぜなら、すべてのカスタムオプションは特定のエンティティの options メッセージの拡張であり、すでにオプションをその1つのエンティティに制約しているからです。しかし、オプションターゲットは、複数のエンティティタイプに適用される共有のオプションメッセージがあり、そのメッセージ内の個々のフィールドの使用を制御したい場合に役立ちます。例えば

message MyOptions {
  string file_only_option = 1 [targets = TARGET_TYPE_FILE];
  int32 message_and_enum_option = 2 [targets = TARGET_TYPE_MESSAGE,
                                     targets = TARGET_TYPE_ENUM];
}

extend google.protobuf.FileOptions {
  MyOptions file_options = 50000;
}

extend google.protobuf.MessageOptions {
  MyOptions message_options = 50000;
}

extend google.protobuf.EnumOptions {
  MyOptions enum_options = 50000;
}

// OK: this field is allowed on file options
option (file_options).file_only_option = "abc";

message MyMessage {
  // OK: this field is allowed on both message and enum options
  option (message_options).message_and_enum_option = 42;
}

enum MyEnum {
  MY_ENUM_UNSPECIFIED = 0;
  // Error: file_only_option cannot be set on an enum.
  option (enum_options).file_only_option = "xyz";
}

クラスの生成

.proto ファイルで定義されたメッセージタイプを操作するために必要な Java、Kotlin、Python、C++、Go、Ruby、Objective-C、または C# コードを生成するには、.proto ファイルに対して protocol buffer コンパイラ protoc を実行する必要があります。コンパイラをインストールしていない場合は、パッケージをダウンロード し、README の指示に従ってください。Go の場合は、コンパイラ用の特別なコードジェネレータープラグインもインストールする必要があります。これとインストール手順は、GitHub の golang/protobuf リポジトリにあります。

Protocol Compiler は次のように呼び出されます。

protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
  • IMPORT_PATH は、import ディレクティブを解決する際に .proto ファイルを探すディレクトリを指定します。省略した場合、現在のディレクトリが使用されます。複数のインポートディレクトリは、--proto_path オプションを複数回渡すことで指定できます。それらは順番に検索されます。-I=_IMPORT_PATH_--proto_path の短縮形として使用できます。

  • 1つ以上の 出力ディレクティブ を指定できます。

    さらに便利な機能として、DST_DIR.zip または .jar で終わる場合、コンパイラは指定された名前の単一の ZIP 形式のアーカイブファイルに出力を書き込みます。.jar 出力には、Java JAR 仕様で要求されるマニフェストファイルも付与されます。出力アーカイブが既に存在する場合、上書きされることに注意してください。

  • 入力として1つ以上の .proto ファイルを提供する必要があります。複数の .proto ファイルを一度に指定できます。ファイルは現在のディレクトリからの相対パスで指定されますが、各ファイルはコンパイラがその正規名を決定できるように、いずれかの IMPORT_PATH に存在する必要があります。

ファイルの場所

.proto ファイルを他の言語のソースと同じディレクトリに配置しないことをお勧めします。プロジェクトのルートパッケージの下に、.proto ファイル用のサブパッケージ proto を作成することを検討してください。

場所は言語非依存であるべき

Java コードを操作する場合、関連する .proto ファイルを Java ソースと同じディレクトリに配置すると便利です。ただし、Java 以外のコードが同じ proto を使用する場合、パスプレフィックスは意味をなさなくなります。したがって、一般的には、//myteam/mypackage のような関連する言語に依存しないディレクトリに proto を配置してください。

この規則の例外は、proto がテストなど、Java コンテキストでのみ使用されることが明らかな場合です。

サポートされているプラットフォーム

に関する情報

  • サポートされているオペレーティングシステム、コンパイラ、ビルドシステム、および C++ バージョンについては、Foundational C++ Support Policy を参照してください。
  • サポートされている PHP バージョンについては、Supported PHP versions を参照してください。