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

このドキュメントでは、Protocol Buffers 言語の 2023 年版リビジョンをプロジェクトで使用する方法について説明します。

このガイドでは、プロトコルバッファ言語を使用してプロトコルバッファデータを構造化する方法について説明します。これには、.proto ファイルの構文や、.proto ファイルからデータアクセス クラスを生成する方法が含まれます。プロトコルバッファ言語の 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 言語仕様の 2023 年版エディションを使用していることを指定します。

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

フィールドタイプの指定

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

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

メッセージ定義内の各フィールドには、以下の制限事項に従い、1から536,870,911までの番号を割り当てる必要があります。

  • 与えられた番号は、そのメッセージのすべてのフィールド間で一意である必要があります
  • フィールド番号 19,000 から 19,999 は Protocol Buffers の実装用に予約されています。これらの予約されたフィールド番号のいずれかをメッセージで使用すると、プロトコルバッファコンパイラが警告します。
  • 以前に予約されたフィールド番号や、拡張に割り当てられたフィールド番号を使用することはできません。

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

フィールド番号は決して再利用すべきではありません。新しいフィールド定義で再利用するために、予約済みリストからフィールド番号を取り出すことは決してしないでください。フィールド番号を再利用することの影響を参照してください。

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

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

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

protobuf のワイヤーフォーマットは軽量であり、ある定義を使用してエンコードされ、別の定義を使用してデコードされたフィールドを検出する方法を提供しません。

ある定義を使用してフィールドをエンコードし、その同じフィールドを別の定義でデコードすると、以下の問題が発生する可能性があります。

  • デバッグによる開発時間の損失
  • 解析/マージエラー (最良のシナリオ)
  • PII/SPII の漏洩
  • データ破損

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

  • フィールドの再番号付け (フィールドの番号順をより見た目良くするために行われることがあります)。再番号付けは、実質的に再番号付けに関わるすべてのフィールドを削除し、再追加することになり、互換性のないワイヤーフォーマットの変更を引き起こします。

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

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

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

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

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

  • 単一:

    単一フィールドには明示的なカーディナリティラベルがありません。以下の 2 つの状態があります。

    • フィールドが設定され、明示的に設定された、またはワイヤーから解析された値を含んでいます。これはワイヤーにシリアル化されます。
    • フィールドが未設定であり、デフォルト値を返します。これはワイヤーにシリアル化されません。

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

    エディションに移行された Proto3 の暗黙的なフィールドは、field_presence 機能をIMPLICIT値に設定して使用します。

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

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

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

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

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

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

整形式メッセージ

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

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

追加のメッセージタイプの追加

単一の.protoファイル内で複数のメッセージタイプを定義できます。これは、複数の関連するメッセージを定義する場合に便利です。たとえば、SearchResponseメッセージタイプに対応する応答メッセージ形式を定義したい場合、同じ.protoファイルに追加できます。

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

message SearchResponse {
 ...
}

メッセージの結合は肥大化を招く メッセージ、enum、サービスなど、複数のメッセージタイプを単一の.protoファイル内で定義できますが、依存関係が異なる多数のメッセージが単一ファイルで定義されると、依存関係の肥大化を招く可能性もあります。.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 でのメッセージエンコーディングが引き続き解析できるように、フィールド名も予約する必要があります。

予約済みフィールド番号

フィールドを完全に削除するか、コメントアウトすることでメッセージタイプを更新した場合、将来の開発者はタイプに独自の更新を行う際に、そのフィールド番号を再利用する可能性があります。これは、後で同じ.protoの古いインスタンスをロードした場合に、データ破損、プライバシーバグなど、深刻な問題を引き起こす可能性があります。これを確実に防ぐ1つの方法は、削除されたエントリの数値値(および/またはJSONシリアル化の問題を引き起こす可能性のある名前)がreservedであることを指定することです。将来のどの開発者もこれらの識別子を使用しようとすると、プロトコルバッファコンパイラはエラーメッセージを生成します。

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

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

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

予約済みフィールド名

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

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

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

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

あなたの.protoから何が生成されるか?

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

  • C++ の場合、コンパイラは各.protoファイルから.hおよび.ccファイルを生成し、ファイルで記述された各メッセージタイプに対してクラスを作成します。
  • Java の場合、コンパイラは各メッセージタイプのクラスを含む.javaファイルを生成し、さらにメッセージクラスのインスタンスを作成するための特別なBuilderクラスも生成します。
  • Kotlin の場合、Java の生成コードに加えて、コンパイラは各メッセージタイプに対応する.ktファイルを生成し、Kotlin API を改善します。これには、メッセージインスタンスの作成を簡素化する DSL、ヌル許容フィールドアクセサー、およびコピー関数が含まれます。
  • 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可変長エンコーディングを使用します。符号付き整数値。通常の int32 よりも効率的に負の数をエンコードします。
sint64可変長エンコーディングを使用します。符号付き整数値。通常の 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 の文字列は、デコード時にはユニコードとして表現されますが、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フィールドを追加したいとします。この corpus は、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 で定義されている最初の値であるためです。

2023 年版エディションでは、enum 定義で最初に定義される値は、値がゼロである必要があり、名前はENUM_TYPE_NAME_UNSPECIFIEDまたはENUM_TYPE_NAME_UNKNOWNであるべきです。これは次の理由によります。

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

また、この最初のデフォルト値は、「この値は未指定であった」という以外のセマンティックな意味を持たないことを推奨します。

SearchRequest.corpusフィールドのような enum フィールドのデフォルト値は、このように明示的に上書きできます。

  Corpus corpus = 4 [default = CORPUS_UNIVERSAL];

option features.enum_type = CLOSED;を使用して proto2 から移行された enum タイプの場合、enum の最初の値に制限はありません。これらのタイプの enum の最初の値を変更することは推奨されません。なぜなら、明示的なフィールドのデフォルトがない場合、その enum タイプを使用するすべてのフィールドのデフォルト値が変わってしまうからです。

Enum 値のエイリアス

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

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ファイル内の任意のメッセージ定義で再利用できます。また、あるメッセージ内で宣言されたenumタイプを、_MessageType_._EnumType_の構文を使用して、別のメッセージのフィールドのタイプとして使用することもできます。

言語固有の Enum 実装

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

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

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

予約値

enum エントリを完全に削除したり、コメントアウトしたりして enum タイプを更新した場合、将来のユーザーがタイプに独自の更新を行う際に、数値値を再利用する可能性があります。これは、後で同じ.protoの古いインスタンスをロードした場合に、データ破損、プライバシーバグなど、深刻な問題を引き起こす可能性があります。これを確実に防ぐ1つの方法は、削除されたエントリの数値値(および/またはJSONシリアル化の問題を引き起こす可能性のある名前)がreservedであることを指定することです。将来のユーザーがこれらの識別子を使用しようとすると、プロトコルバッファコンパイラが警告します。予約された数値範囲が可能な最大値まで拡張されることを、maxキーワードを使用して指定できます。

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

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

他のメッセージタイプの使用

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

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ファイルを直接移動してすべての呼び出しサイトを単一の変更で更新する代わりに、古い場所にプレースホルダーの.protoファイルを配置して、import publicの概念を使用してすべてのインポートを新しい場所に転送できます。

public import 機能は、Java、Kotlin、TypeScript、JavaScript、GCL、および protobuf 静的リフレクションを使用する C++ ターゲットでは利用できません。

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 メッセージタイプをインポートし、それを 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は互いに互換性がありますが、他の整数型とは互換性がありません。書き込まれた値が INT_MIN から INT_MAX の範囲内(両端を含む)であった場合、どちらの型でも同じ値として解析されます。sint64 の値がその範囲外に書き込まれ、sint32 として解析された場合、varint は 32 ビットに切り捨てられ、その後ジグザグデコードが行われます(これにより、異なる値が観測されます)。
  • stringbytesは、バイトが有効な UTF-8 である限り互換性があります。
  • 組み込みメッセージは、バイトがメッセージのエンコードされたインスタンスを含んでいる場合、bytesと互換性があります。
  • fixed32sfixed32と互換性があり、fixed64sfixed64と互換性があります。
  • stringbytes、およびメッセージフィールドの場合、単一型はrepeatedと互換性があります。繰り返しフィールドのシリアル化されたデータが入力として与えられた場合、このフィールドが単一型であることを期待するクライアントは、それがプリミティブ型フィールドであれば最後の入力値を取り、メッセージ型フィールドであればすべての入力要素をマージします。これは、bool や enum を含む数値型には一般的に安全ではないことに注意してください。数値型の繰り返しフィールドは、デフォルトでpacked形式でシリアル化されるため、単一フィールドが期待される場合には正しく解析されません。
  • enumはワイヤーフォーマットの観点からint32uint32int64、およびuint64と互換性があります(値が収まらない場合は切り捨てられることに注意してください)。ただし、メッセージがデシリアル化される際にクライアントコードがそれらを異なる方法で扱う可能性があることに注意してください。たとえば、認識されないenum値はメッセージ内に保持されますが、メッセージがデシリアル化されたときにこれがどのように表現されるかは言語に依存します。int フィールドは常にその値を保持します。
  • 単一のoptionalフィールドまたは拡張を新しいoneofのメンバーに変更することはバイナリ互換性がありますが、一部の言語(特に Go)では、生成されたコードの API が互換性のない方法で変更されます。このため、Google はAIP-180に記載されているように、そのパブリック API でそのような変更を行いません。ソース互換性に関する同じ注意点として、同時に複数のフィールドを設定するコードがないと確信できる場合は、複数のフィールドを新しいoneofに移動することは安全かもしれません。既存のoneofにフィールドを移動することは安全ではありません。同様に、単一フィールドのoneofoptionalフィールドまたは拡張に変更することは安全です。
  • map<K, V>と対応するrepeatedメッセージフィールドの間でフィールドを変更することはバイナリ互換性があります(メッセージレイアウトおよびその他の制限については、後述のマップを参照)。ただし、変更の安全性はアプリケーションに依存します。メッセージをデシリアル化および再シリアル化する際、repeatedフィールド定義を使用するクライアントはセマンティックに同一の結果を生成しますが、mapフィールド定義を使用するクライアントはエントリを並べ替えたり、重複するキーを持つエントリを破棄したりする可能性があります。

不明なフィールド

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

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

不明なフィールドの保持

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

  • proto を JSON にシリアル化する。
  • メッセージ内のすべてのフィールドを反復処理して、新しいメッセージを生成する。

不明なフィールドの損失を避けるには、以下を実行します。

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

TextFormat はやや特殊なケースです。TextFormat にシリアル化すると、不明なフィールドがフィールド番号を使用して出力されます。しかし、フィールド番号を使用するエントリがある場合、TextFormat データをバイナリ proto に再解析すると失敗します。

拡張

拡張とは、そのコンテナメッセージの外で定義されるフィールドです。通常、コンテナメッセージの.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) は、UserContentメッセージを定義し、拡張用にフィールド番号 [100 から 199] を予約しています。この範囲に対して、すべての拡張に宣言を必須とするために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 エコシステム外のメカニズムを使用して、選択された拡張範囲内で拡張フィールド番号を割り当てます。一例として、モノレポのコミット番号を使用することが考えられます。このシステムは、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のスコープ内で宣言されているだけであり、単なる静的メンバーであることを意味します。

一般的なパターンは、拡張のフィールドタイプのスコープ内で拡張を定義することです。たとえば、media.UserContentに対するpuppies.Photo型の拡張があり、その拡張が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

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

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

複数の値が設定されている場合、プロト内の順序によって決定される最後の設定値が、それまでのすべての値を上書きします

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にできません。

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

  • oneof フィールドをデフォルト値(int32 oneof フィールドを0に設定するなど)に設定した場合、その oneof フィールドの「ケース」が設定され、値はワイヤーにシリアル化されます。

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

    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 のメンバーであるかどうかを知る方法がないため、その違いを区別する方法はありません。

タグ再利用の問題

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

マップ

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

map<key_type, value_type> map_field = N;

…ここで、key_typeは任意の整数型または文字列型(つまり、浮動小数点型とbytesを除く、任意のスカラ型)にすることができます。key_typeには enum も proto メッセージも有効ではありません。value_typeは、別のマップを除く任意の型にすることができます。

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

map<string, Project> projects = 3;

マップの機能

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

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

後方互換性

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

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

repeated MapFieldEntry map_field = N;

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

パッケージ

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

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

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

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

パッケージ指定子が生成コードに影響を与える方法は、選択した言語によって異なります。

  • C++ では、生成されたクラスは C++ 名前空間内にラップされます。たとえば、Openfoo::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_が前置される)に変換されます。たとえば、OpenFoo::Bar名前空間に属します。
  • PHP では、.protoファイルでoption php_namespaceを明示的に指定しない限り、パッケージは PascalCase に変換された後に名前空間として使用されます。たとえば、OpenFoo\Bar名前空間に属します。
  • C# では、.protoファイルでoption csharp_namespaceを明示的に指定しない限り、パッケージは PascalCase に変換された後に名前空間として使用されます。たとえば、OpenFoo.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 言語ガイドで確認できます。

Protocol Buffers 用の 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 パッケージ(.protoファイル内の「package」キーワードを使用して指定)が使用されます。ただし、proto パッケージは通常、逆ドメイン名で始まることが期待されていないため、良い 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 などは、この外側のラッパー Java クラス内にネストされたクラス/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のブールオプションです。Java コードを生成しない場合、このオプションは効果がありません。

    option java_multiple_files = true;
    
  • optimize_for (ファイルオプション): SPEEDCODE_SIZELITE_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_services, java_generic_services, py_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クラスと列挙型に付加される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 値オプション

列挙値オプションがサポートされています。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);

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

カスタムオプション

プロトコルバッファでは、独自のオプションを定義して使用することもできます。これは、ほとんどの人が必要としない高度な機能であることに注意してください。もし独自のオプションを作成する必要があると思われる場合は、詳細について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を持つ場合、そのフィールドは列挙型(またはその他の非メッセージエンティティ)のカスタムオプションとして設定できません。Protocはこれを強制し、ターゲット制約に違反があった場合はエラーを発生させます。

一見すると、この機能は不要に見えるかもしれません。なぜなら、すべてのカスタムオプションは特定のエンティティのオプションメッセージの拡張であり、すでにそのオプションをその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ファイルに対してプロトコルバッファコンパイラprotocを実行する必要があります。コンパイラをインストールしていない場合は、パッケージをダウンロードし、READMEの指示に従ってください。Goの場合、コンパイラ用の特別なコードジェネレータプラグインもインストールする必要があります。これはGitHubのgolang/protobufリポジトリで見つけることができ、インストール手順もそこにあります。

プロトコルコンパイラは次のように呼び出されます。

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の短縮形として使用できます。

注意: proto_pathに対する相対ファイルパスは、所与のバイナリ内でグローバルに一意でなければなりません。たとえば、proto/lib1/data.protoproto/lib2/data.protoがある場合、-I=proto/lib1 -I=proto/lib2と一緒に使用することはできません。なぜなら、import "data.proto"がどのファイルを意味するかが曖昧になるからです。代わりに-Iproto/を使用し、グローバル名はlib1/data.protolib2/data.protoになります。

ライブラリを公開しており、他のユーザーがあなたのメッセージを直接使用する可能性がある場合は、ファイル名の衝突を避けるために、使用が期待されるパスに一意のライブラリ名を含めるべきです。1つのプロジェクトに複数のディレクトリがある場合、プロジェクトのトップレベルディレクトリに1つの-Iを設定するのがベストプラクティスです。

  • 1つまたは複数の出力ディレクティブを指定できます。

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

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

ファイルの場所

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

場所は言語に依存しないようにすべき

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

このルールの例外は、テスト用など、プロトがJavaコンテキストでのみ使用されることが明確な場合です。

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

以下に関する情報については、

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