はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【C#】Google.ProtoBufを利用してProtocol Buffersを扱う方法

はじめに

先日protobuf-netの利用方法についての記事を書いたのですが、今回はGoogle製のライブラリGoogle.ProtoBugの利用方法について書きたいと思います。

www.nuget.org
github.com
protobuf.dev

概要

Google.ProtoBufC#用のProtocol Buffersランタイムライブラリです。

github.com

サポート

  • .NET 4.5+ (net45)
  • .NET Standard 1.1 and 2.0 (netstandard1.1 and netstandard2.0)
  • .NET 5+ (net50)

protobuf/csharp at main · protocolbuffers/protobuf · GitHub

環境

  • Rider 2023.1.3
  • Console Application
  • .net7.0
  • C#11

インストール方法

Rider上のエクスプローラーから.csprojを右クリックし、NuGetパッケージの管理を選択します。

NuGetパッケージの管理

あとはGoogle.ProtoBufと検索して右上の+ボタンを押せばインストール完了です。

Google.ProtoBufをインストール

おまけ : Riderプラグインの導入

JetBrains製のProtocol Buffersプラグインがあるので、これを導入すると便利です。proto2proto3に対応しています。

Provides editor support for Protocol Buffers files.

plugins.jetbrains.com

導入方法は設定画面を開き、プラグイン > Protocol Buffers > インストールからインストールを行なってください。

Protocol Buffersプラグインをインストール

使い方

.protoの利用

Google.ProtoBugを利用するにあたって.protoファイルを用いる必要があります。詳細については前回の記事を参照してください。

// sample.proto
syntax = "proto3";

option csharp_namespace = "Sample.Messages";

message Person
{
  // Protobugスタイルガイドではフィールド名にunderscore_separated_namesを利用することが推奨
  int32 id = 1;
  string name = 2;
  Address address = 3;
}

message Address
{
  string line1 = 1;
  string line2 = 2;
}

.protoから.csを生成するツールを取得する必要があります。簡単な方法としてNuGetからGoogle.ProtoBuf.Toolをインストールすると、protoc.exeが手に入ります。ただ私の場合はMacなので、Release Pagesから最新のprotoc-25.1-osx-universal_binary.zipをダウンロードして利用します。

github.com

インストールした中身
$ protoc "対象.protoのパス" --csharp_out="生成先のディレクトリパス"
// サンプル
$ protoc "./sample.proto" --csharp_out="./"

Protocol Buffer Basics: C# | Protocol Buffers Documentation

正しく動作すると.csファイルが生成されるはずです。

// <auto-generated>
//     Generated by the protocol buffer compiler.  DO NOT EDIT!
//     source: sample.proto
// </auto-generated>
#pragma warning disable 1591, 0612, 3021, 8981
#region Designer generated code

using pb = global::Google.Protobuf;
using pbc = global::Google.Protobuf.Collections;
using pbr = global::Google.Protobuf.Reflection;
using scg = global::System.Collections.Generic;
namespace Sample.Messages {

  /// <summary>Holder for reflection information generated from sample.proto</summary>
  public static partial class SampleReflection {

    #region Descriptor
    /// <summary>File descriptor for sample.proto</summary>
    public static pbr::FileDescriptor Descriptor {
      get { return descriptor; }
    }
    private static pbr::FileDescriptor descriptor;

    static SampleReflection() {
      byte[] descriptorData = global::System.Convert.FromBase64String(
          string.Concat(
            "CgxzYW1wbGUucHJvdG8iPQoGUGVyc29uEgoKAmlkGAEgASgFEgwKBG5hbWUY",
            "AiABKAkSGQoHYWRkcmVzcxgDIAEoCzIILkFkZHJlc3MiJwoHQWRkcmVzcxIN",
            "CgVsaW5lMRgBIAEoCRINCgVsaW5lMhgCIAEoCUISqgIPU2FtcGxlLk1lc3Nh",
            "Z2VzYgZwcm90bzM="));
      descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData,
          new pbr::FileDescriptor[] { },
          new pbr::GeneratedClrTypeInfo(null, null, new pbr::GeneratedClrTypeInfo[] {
            new pbr::GeneratedClrTypeInfo(typeof(global::Sample.Messages.Person), global::Sample.Messages.Person.Parser, new[]{ "Id", "Name", "Address" }, null, null, null, null),
            new pbr::GeneratedClrTypeInfo(typeof(global::Sample.Messages.Address), global::Sample.Messages.Address.Parser, new[]{ "Line1", "Line2" }, null, null, null, null)
          }));
    }
    #endregion

  }
  #region Messages
  [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")]
  public sealed partial class Person : pb::IMessage<Person>
  #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
      , pb::IBufferMessage
  #endif
  {
    private static readonly pb::MessageParser<Person> _parser = new pb::MessageParser<Person>(() => new Person());
    private pb::UnknownFieldSet _unknownFields;
    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
    [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
    public static pb::MessageParser<Person> Parser { get { return _parser; } }

    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
    [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
    public static pbr::MessageDescriptor Descriptor {
      get { return global::Sample.Messages.SampleReflection.Descriptor.MessageTypes[0]; }
    }

    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
    [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
    pbr::MessageDescriptor pb::IMessage.Descriptor {
      get { return Descriptor; }
    }

    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
    [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
    public Person() {
      OnConstruction();
    }

    partial void OnConstruction();

    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
    [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
    public Person(Person other) : this() {
      id_ = other.id_;
      name_ = other.name_;
      address_ = other.address_ != null ? other.address_.Clone() : null;
      _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);
    }

    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
    [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
    public Person Clone() {
      return new Person(this);
    }

    /// <summary>Field number for the "id" field.</summary>
    public const int IdFieldNumber = 1;
    private int id_;
    /// <summary>
    /// Protobugスタイルガイドではフィールド名にunderscore_separated_namesを利用することが推奨
    /// .NET ツールでは自動で UnderScoreSeparatedNames のように変換してくれますs
    /// </summary>
    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
    [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
    public int Id {
      get { return id_; }
      set {
        id_ = value;
      }
    }

// 長いので省略...

  }

  #endregion

}

#endregion Designer generated code

シリアライズ・デシリアライズ

シリアライズ・デシリアライズするには、先ほど生成された定義を利用するだけなのでとても簡単です。

using Google.Protobuf;
using Sample.Messages;

internal class Program
{
    public static void Main()
    {
        var person = new Person
        {
            Id = 12345,
            Name = "Hanachiru",
            Address = new Address
            {
                Line1 = "Line 1",
                Line2 = "Line 2"
            }
        };

        using (var output = File.Create("Sample.dat"))
        {
            // 実際に書き込まれるデータ(バイナリ) : 08 B9 60 12 09 48 61 6E 61 63 68 69 72 75 1A 10 0A 06 4C 69 6E 65 20 31 12 06 4C 69 6E 65 20 32
            person.WriteTo(output);
        }

        Person newPerson;
        using (var input = File.OpenRead("Sample.dat"))
        {
            newPerson = Person.Parser.ParseFrom(input);
            
            // 12345
            Console.WriteLine(newPerson.Id);
            
            // Hanachiru
            Console.WriteLine(newPerson.Name);
            
            // Line 1
            Console.WriteLine(newPerson.Address.Line1);

            // Line 2
            Console.WriteLine(newPerson.Address.Line2);
        }
    }
}

ちなみにバイナリのデータはちゃんとprotobuf-netを利用した前回と全く同じになっていますね。(むしろなってくれてないと困りますが)

Google.ProtoBufの場合はシリアライズ・デシリアライズの処理もコード生成されているので、.protoがほぼ必須になっているといってもいいでしょう。