C#でProtocolBuffersのプラグインを書く


## はじめに

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をシリアライズ・デシリアライズする際に利用されます。

```protobuf
syntax = "proto3";

message Person {
  string name = 1;
  int32 age = 2;
  string email = 3;
}
```

### 各言語向けの出力

protocコマンドに以下のようなオプションを付けることで、各言語向けのコードを生成できます。

- `--csharp_out` : C#コードを生成
- `--ruby_out` : Rubyコードを生成
- `--python_out` : Pythonコードを生成

生成されるコードの例
```cs
// 自動生成された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
# 自動生成されたPythonコード
class Person(message.Message):
  __metaclass__ = reflection.GeneratedProtocolMessageType
  
  DESCRIPTOR = _PERSON
  
  # プロパティとメソッドが続く...
  def __init__(self):
    self.name = ""
    self.age = 0
    self.email = ""
```

```ruby
# 自動生成された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#プロジェクトを作ります。
コンソールアプリケーションとして作成してください

![CleanShot 2025-02-28 at 23.31.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159111/ff45326d-249f-4f49-9705-9b91a315356f.png)


### 必要なパッケージ
まずはパッケージを導入します。Protocコマンドが渡してくれるデータをパースして扱いやすい形で渡してくれる公式パッケージを2つ入れます：

- Google.Protobuf
    - https://www.nuget.org/packages/google.protobuf/
- Google.ProtocolBuffers
    - https://www.nuget.org/packages/Google.ProtocolBuffers
導入はNugetで

### コードの基本構造

標準入出力からのデータ受取と、protocに返すためのデータのインスタンスをまず作ります：

```csharp
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());
        }
    }
}
```

### 解析結果の出力例

簡単な例として、解析した情報をそのまま出力するだけのコードを作ってみましょう：

```csharp
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などでビルドしてバイナリを作成します。
大抵の場合、チームでプラグインを使うことが多いと思うのでランタイムの不足により動作しなくなる　みたいな事が起きない自己完結型の単一バイナリとしてビルドするのがおすすめです

例
```bash
  dotnet publish -c Release -r osx-arm64 --self-contained true -p:PublishSingleFile=true -o ../bin/osx-arm64 -p:DebugType=none
```

このコマンドにより.NET Runtimeがインストールされていない環境でも実行可能な単一の実行ファイルが生成されます。
バイナリサイズは多少大きくなりますが環境間でのトラブルが減るのでおすすめです。

- `-c Release`: リリースビルドを作成
- `-r osx-arm64`: AppleSiliconMac向けにビルド
- `--self-contained true`: 依存するランタイムも含めて完全に自己完結したバイナリを作成
- `-p:PublishSingleFile=true`: 単一のexeファイルに出力
- `-o ../bin/win-x64`: 出力先ディレクトリを指定
- `-p:DebugType=none`: デバッグ情報を含めない（ファイルサイズ削減）



### プラグインの使用方法

ビルドに成功するとバイナリがせいせいされｍ。これを使うようにprotocコマンドに設定します：

```bash
protoc ./proto/sample.proto \
--plugin=protoc-gen-custom=./path/to/plugin \
--custom_out=./
```

ここでのコマンドの各部分の説明：

1. `./proto/sample.proto` : Protoファイルのパス
2. `--plugin=protoc-gen-custom=./path/to/plugin` : プラグインのバイナリへのパスを指定。`protoc-gen-{プラグイン名}`でパスを指定する
3. `--custom_out=./` : `--{プラグイン名}_out`でプラグインを利用し、出力先をセットする。`protoc-gen-`で「custom」という名前で定義しているので、`custom_out`というコマンドになる

### プラグインの実行結果

実際に先ほどのプラグインを実行すると、どんな出力が得られるのか見てみましょう。例えば、以下のようなシンプルなprotoファイルを用意します：

```protobuf
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エンジニアをメインでやってる人には特におすすめです！良き自動化ライフを！