はじめに
Protocol Buffers(ProtoBuf)はGoogleが開発したスキーマ言語で、.proto
ファイルを記述してprotocコマンドを実行するだけで各種言語に合わせた定義ファイルを生成できるツールです。
この記事では、Protocol Buffersの基本的なおさらいから、独自のプラグインをC#で作成する方法までを解説します。
Protocol Buffersのおさらい
Protocol Buffersとは
Protocol Buffersは、Googleが開発しているスキーマ言語です。.proto
ファイルに定義を記述し、protocコマンドを実行することで、C#、Ruby、Pythonなど様々な言語向けのクラス定義ファイルを自動生成できます。
Protoファイルの例
Protoファイルでは、Message
が所謂クラス名に該当し、各種フィールドを定義します。フィールドには「フィールド番号」と呼ばれるユニークな数字が付与され、これはProtoをシリアライズ・デシリアライズする際に利用されます。
syntax = "proto3";
message Person {
string name = 1;
int32 age = 2;
string email = 3;
}
各言語向けの出力
protocコマンドに以下のようなオプションを付けることで、各言語向けのコードを生成できます。
-
--csharp_out
: C#コードを生成 -
--ruby_out
: Rubyコードを生成 -
--python_out
: Pythonコードを生成
生成されるコードの例
// 自動生成されたC#コード
namespace Example {
public sealed partial class Person : pb::IMessage<Person> {
private static readonly pb::MessageParser<Person> _parser = new pb::MessageParser<Person>(() => new Person());
private pb::UnknownFieldSet _unknownFields;
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
public static pb::MessageParser<Person> Parser { get { return _parser; } }
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
public string Name { get; set; }
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
public int Age { get; set; }
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
public string Email { get; set; }
// 以下シリアライズ、デシリアライズ関連のコードが続く...
}
}
# 自動生成されたPythonコード
class Person(message.Message):
__metaclass__ = reflection.GeneratedProtocolMessageType
DESCRIPTOR = _PERSON
# プロパティとメソッドが続く...
def __init__(self):
self.name = ""
self.age = 0
self.email = ""
# 自動生成されたRubyコード
module Example
class Person < ::Google::Protobuf::MessageExts
class << self
def descriptor
# 略...
end
end
attr_accessor :name
attr_accessor :age
attr_accessor :email
end
end
Protocol Buffersのプラグインとは?
プラグインの概要
Protocol Buffersにはプラグインと呼ばれる機能があり、.proto
ファイルの内容をもとに「自由に」ファイルを作ることができます。例えば以下のようなケースで活用できます:
- プロジェクト独自のコンテキストを持ったpartialクラスを作りたい
- Googleのクラスに依存していないピュアなクラス定義を出力させたい
プラグインの仕組み
Protocコマンドは元から外部プログラムを呼び出す、言わばランチャーのような機能しか持っていません。csharp_out
やpython_out
といったコマンド引数は、実際にはcsharp_out
という名前のプラグインを呼び出しているという実装になっています。そのため、独自にhogehoge_out
を定義し処理をさせることが可能です。
プラグインのプログラムは、OSで実行できるバイナリであり、標準入出力を用いてデータの受け渡しができれば何でも良いです。Protocコマンドは.proto
ファイルの解析結果を標準入出力でプログラムに渡すだけというシンプルな設計になっています。
C#でプラグインを書いてみよう
プロジェクトの作成
VisualStudioやRiderなどお好きなIDEでC#プロジェクトを作ります。
コンソールアプリケーションとして作成してください
必要なパッケージ
まずはパッケージを導入します。Protocコマンドが渡してくれるデータをパースして扱いやすい形で渡してくれる公式パッケージを2つ入れます:
- Google.Protobuf
- Google.ProtocolBuffers
コードの基本構造
標準入出力からのデータ受取と、protocに返すためのデータのインスタンスをまず作ります:
using Google.Protobuf;
using Google.Protobuf.Compiler;
namespace ProtocPlugin
{
internal class Program
{
static void Main(string[] args)
{
// 標準入力からデータを読み込む
var request = CodeGeneratorRequest.Parser.ParseFrom(Console.OpenStandardInput());
// 返却用のレスポンスを作成
var response = new CodeGeneratorResponse();
// ここにプラグインの処理を書く
// 標準出力に結果を書き込む
response.WriteTo(Console.OpenStandardOutput());
}
}
}
解析結果の出力例
簡単な例として、解析した情報をそのまま出力するだけのコードを作ってみましょう:
using Google.Protobuf;
using Google.Protobuf.Compiler;
using Google.Protobuf.Reflection;
namespace ProtocPlugin
{
internal class Program
{
static void Main(string[] args)
{
// 標準入力からデータを読み込む
var request = CodeGeneratorRequest.Parser.ParseFrom(Console.OpenStandardInput());
// 返却用のレスポンスを作成
var response = new CodeGeneratorResponse();
// リクエストからファイル情報を取得
foreach (var proto in request.ProtoFile)
{
// ファイル名を取得
var fileName = proto.Name.Replace(".proto", ".txt");
// 内容を作成
var content = $"Package: {proto.Package}\n";
content += "Messages:\n";
foreach (var message in proto.MessageType)
{
content += $" - {message.Name}\n";
content += " Fields:\n";
foreach (var field in message.Field)
{
content += $" {field.Name}: {field.Type} (#{field.Number})\n";
}
}
// レスポンスにファイルを追加
response.File.Add(new CodeGeneratorResponse.Types.File
{
Name = fileName,
Content = content
});
}
// 標準出力に結果を書き込む
response.WriteTo(Console.OpenStandardOutput());
}
}
}
ビルド方法
プロジェクトをdotnet buildなどでビルドしてバイナリを作成します。
大抵の場合、チームでプラグインを使うことが多いと思うのでランタイムの不足により動作しなくなる みたいな事が起きない自己完結型の単一バイナリとしてビルドするのがおすすめです
例
dotnet publish -c Release -r osx-arm64 --self-contained true -p:PublishSingleFile=true -o ../bin/osx-arm64 -p:DebugType=none
このコマンドにより:
-
-c Release
: リリースビルドを作成 -
-r osx-arm64
: AppleSiliconMac向けにビルド -
--self-contained true
: 依存するランタイムも含めて完全に自己完結したバイナリを作成 -
-p:PublishSingleFile=true
: 単一のexeファイルに出力 -
-o ../bin/win-x64
: 出力先ディレクトリを指定 -
-p:DebugType=none
: デバッグ情報を含めない(ファイルサイズ削減)
これにより、.NET Runtimeがインストールされていない環境でも実行可能な単一のexeファイルが生成されます。
バイナリサイズは多少大きくなりますが環境間でのトラブルが減るのでおすすめです。
プラグインの使用方法
ビルドに成功するとバイナリができあがります。これを使うようにprotocコマンドに設定します:
protoc ./proto/sample.proto \
--plugin=protoc-gen-custom=./path/to/plugin \
--custom_out=./
ここでのコマンドの各部分の説明:
-
./proto/sample.proto
: Protoファイルのパス -
--plugin=protoc-gen-custom=./path/to/plugin
: プラグインのバイナリへのパスを指定。protoc-gen-{プラグイン名}
でパスを指定する -
--custom_out=./
:--{プラグイン名}_out
でプラグインを利用し、出力先をセットする。protoc-gen-
で「custom」という名前で定義しているので、custom_out
というコマンドになる
プラグインの実行結果
実際に先ほどのプラグインを実行すると、どんな出力が得られるのか見てみましょう。例えば、以下のようなシンプルなprotoファイルを用意します:
syntax = "proto3";
package example;
message Person {
string name = 1;
int32 age = 2;
string email = 3;
}
message Address {
string street = 1;
string city = 2;
string country = 3;
int32 zip_code = 4;
}
このprotoファイルを今回作ったPluginを通すことで以下のような出力を得る事ができます。
Package: example
Messages:
- Person
Fields:
name: TYPE_STRING (#1)
age: TYPE_INT32 (#2)
email: TYPE_STRING (#3)
- Address
Fields:
street: TYPE_STRING (#1)
city: TYPE_STRING (#2)
country: TYPE_STRING (#3)
zip_code: TYPE_INT32 (#4)
実践例
ProtoBufプラグインは、C#の場合、T4テンプレートと組み合わせることで非常に強力になります。例えば、以下のような使い方が考えられます:
- データモデルからORMのマッピングクラスを自動生成
- APIのリクエスト/レスポンスからクライアントコードを自動生成
- ドメインモデルに基づいた独自のバリデーションコードの自動生成
まとめ
Protocol Buffersのプラグインは、情報が少ないですが意外と簡単に作ることができます。
自分のプロジェクトに最適化した独自の自動生成処理を作成することで、開発効率を大幅に向上させることができるでしょう。
C#になれてる人はC#で書いたほうが当然ですがメンテナンス性も良いですし、私のようにUnityエンジニアをメインでやってる人には特におすすめです!良き自動化ライフを!