はじめに
Protocol Buffersのコンパイラであるprotocですが、プラグインを作成することで.protoを解析して自由にコードなどのファイルを生成することができます。また標準入出力さえフォーマットを守っていればよいのでどの言語でもプラグインの作成が可能です。
protoc, the Protocol Buffers Compiler, can be extended to support new languages via plugins. A plugin is just a program which reads a CodeGeneratorRequest protocol buffer from standard input and then writes a CodeGeneratorResponse protocol buffer to standard output. These message types are defined in plugin.proto. We recommend that all third-party code generators be written as plugins, as this allows all generators to provide a consistent interface and share a single parser implementation.
// DeepL
プロトコルバッファコンパイラであるprotocは、プラグインによって新しい言語をサポートするように拡張することができる。プラグインは、標準入力からCodeGeneratorRequestプロトコルバッファを読み込み、標準出力にCodeGeneratorResponseプロトコルバッファを書き込む単なるプログラムである。これらのメッセージタイプはplugin.protoで定義される。私たちは、すべてのサードパーティのコードジェネレータがプラグインとして書かれることを推奨します。これは、すべてのジェネレータが一貫したインターフェイスを提供し、単一のパーサ実装を共有することを可能にするからです。
Other Languages | Protocol Buffers Documentation
私はC#を普段利用しているので、この記事では.protoからC#コードを生成するプラグインをC#で作成する方法を紹介したいと思います。
実装方針
最初に全体像を書くと.protoを入力として受取り、protocがpluginを呼び出してファイルを出力します。

pluginにだけフォーカスすると、
- 標準入力で
CodeGeneratorRequestを受取り - 標準出力で
CodeGeneratorResponseを書き出す
を満たすコンソールアプリケーションを作成すればOKです。
CodeGeneratorRequest・CodeGeneratorResponseについては.protoでスキーマが記述されています。初見だと難しそうに感じられるかもしれませんが、C#ではGoogleが出しているGoogle.ProtoBufというライブラリを使用すれば型が用意されているのでそこまで迷うことなく利用できるかと思います。このpluginの中に.protoのどの情報を用いて、どのようなファイルを生成するかを記述します。
plugin.pb.h | Protocol Buffers Documentation
https://github.com/protocolbuffers/protobuf/blob/main/src/%C3%A5google/protobuf/compiler/plugin.proto
自作したpluginを呼び出すのがprotocです。例えば以下のようなコマンドを用いることでpluginを使用することができます。
# mypluginという自作プラグインを使用 # ./sample.protoを入力とし、./outputにファイル出力を行う $ protoc --plugin=protoc-gen-myplugin=./publish/MyPlugin --myplugin_out=./output ./sample.proto
ちょっと記法に癖があるので後ほど詳しく書きますが、基本はこれだけです。
プラグインの作成
C#でコンソールアプリケーションを作成する
dotnet cli(コマンド)でもVisualStudioやRiderでも何でも良いのですが、コンソールアプリケーションを作成してください。
# CustomProtocPluginというプロジェクト名のコンソールアプリ用プロジェクト作成 $ dotnet new console -n CustomProtocPlugin
NuGetパッケージの追加
先程も触れたのですが、Google.Protobufを導入すると型を用意してくれているのでかなり便利です。先程同様コマンドでもGUI上でもいいですがNuGetでインストールしてください。
www.nuget.org
$ dotnet add package Google.Protobuf
ちなみに使い方について本当に簡単にですが昔書いていました。
www.hanachiru-blog.com
コード実装
何度も同じことを言ってしまっていますが、標準入力からCodeGeneratorRequestを読み込み、標準出力としてCodeGeneratorResponseを書き込みます。割と型を見れば直感的に書けます。
using Google.Protobuf; using Google.Protobuf.Compiler; // 標準入力から CodeGeneratorRequest を読み込む using var stdin = Console.OpenStandardInput(); var request = CodeGeneratorRequest.Parser.ParseFrom(stdin); // リクエスト内の各 .proto ファイルを処理する var response = new CodeGeneratorResponse(); foreach (var protoFile in request.ProtoFile) { // CodeGeneratorRequest.FileToGenerateに含まれるファイル(明示的に指定されたファイル)のみを処理対象とする if (!request.FileToGenerate.Contains(protoFile.Name)) { continue; } // 生成するコードを指定 response.File.Add(new CodeGeneratorResponse.Types.File { Name = $"{Path.GetFileNameWithoutExtension(protoFile.Name)}.cs", Content = $$$""" public class {{{Path.GetFileNameWithoutExtension(protoFile.Name)}}}Sandbox { public string[] GetMessages() => [ {{{string.Join(", ", protoFile.MessageType.Select(x => $"\"{x.Name}\""))}}} ]; public string GetPackageName() => "{{{protoFile.Package}}}"; } """ }); } // 標準出力に CodeGeneratorResponse を書き込む using var stdout = Console.OpenStandardOutput(); response.WriteTo(stdout);
このプロジェクトをビルドして実行バイナリを吐き出させます。
# ./publishの中にCustomProtocPluginという実行バイナリが吐き出される $ dotnet publish -o ./publish
CodeGeneratorRequestについて
protocからpluginに渡される情報はすべてCodeGeneratorRequestに集約されています。
| .protoの定義 | Google.Protobufでの定義 | 説明 |
|---|---|---|
file_to_generate |
FileToGenerate(RepeatedField<string>) |
コマンドラインで直接指定された.protoファイル名のリスト |
parameter |
Parameter(string) |
コマンドラインで渡されるパラメーター |
proto_file |
ProtoFile(RepeatedField<FileDescriptorProto>) |
解析された全ての.protoファイル(依存関係も含む)の内容 |
compiler_version |
CompilerVersion(Version) |
protocのバージョン |
parameterについては後ほど渡し方を載せておきます。
CodeGeneratorResponseについて
| .protoの定義 | Google.Protobufでの定義 | 説明 |
|---|---|---|
error |
Error(string) |
エラーメッセージ。空でない場合はコード生成の失敗を示す |
supported_features |
SupportedFeatures(ulong) |
プラグインがサポートする機能のビットマスク |
minimum_edition |
MinimumEdition(int) |
対応する最小のエディション |
maximum_edition |
MaximumEdition(int |
対応する最大のエディション |
file |
File(RepeatedField) |
生成されるファイルのリスト |
CodeGeneratorRequestが解析不可能であるようなprotoc自体の問題を示すエラーは標準エラーを0以外にし、それ以外はErrorに値を入れてステータスコードを0で返します。
プラグインの利用方法
作成した自作pluginをprotocを通して利用します。
プラグインの名前
まずはプラグインの名前を決めておきます。これは必ずしも実行バイナリと名前が一致している必要はありません。
プラグイン名(NAMEを置換)・プラグインへのパス(path/to/mylibraryを置換)・出力先フォルダ(OUT_DIRを置換)を元に--plugin=protoc-gen-NAME=path/to/mybinary--NAME_out=OUT_DIRのようにprotocに渡してあげます。
Windowsの場合は必ず.exeを付与してください。
plugin.h | Protocol Buffers Documentation
# Windows protoc --plugin=protoc-gen-NAME=path/to/mybinary.exe --NAME_out=OUT_DIR # Otherwise $ protoc --plugin=protoc-gen-NAME=path/to/mybinary --NAME_out=OUT_DIR
正しく実行できるとprotocはプラグインを呼び出して出力を生成し、出力先フォルダに保存します。
動作例
sample.proto
syntax = "proto3"; package sample.package; option csharp_namespace = "Sample.Package"; message MyMessage1 { string name = 1; } message MyMessage2 { int32 id = 1; }
sample.cs
public class sampleSandbox { public string[] GetMessages() => [ "MyMessage1", "MyMessage2" ]; public string GetPackageName() => "sample.package"; }
より高度な利用
パラメータを渡す方法
--myplugin_opt=key1=val1,key2=val2のようにコマンドラインで指定すると、CodeGeneratorRequest.Parameterにstringでkey1=val1,key2=val2が渡ってきます。
$ protoc --plugin=protoc-gen-myplugin=./publish/MyPlugin --myplugin_out="key1=val1,key2=val2:./output" ./sample.proto