Protocol Buffers を C# で遊んでみた

はじめに

Google 製のシリアライズツール「Protocol Buffers」を、今更ながら、C# で使ってみました。
「Protocol Buffers って何?」という人は、次の記事を読むといいです。

C# で ProtocolBuffers を使うには

C# で ProtocolBuffers を使うためのライブラリが、既にたくさん作られています。その中で今回は、「protobuf-net」というライブラリを使ってみました。

protobuf-net の使い方

使い方はすごく簡単。

  1. プロジェクトの参照設定で protobuf-net を追加
  2. シリアライズしたいクラスに ProtoContract 属性を付ける
  3. メンバに ProtoMember 属性を付ける
  4. Serializer クラスの Serialize メソッドでシリアライズ
  5. Serializer クラスの Deserialize メソッドでデシリアライズ

サンプルコードがこちら。

using System;
using System.IO;
using ProtoBuf;

namespace ProtoBufSample
{
    // ProtocolBuffers でシリアライズしたいクラス
    [ProtoContract]
    public class Item
    {
        [ProtoMember(1)]
        public string Key { get; set; }
        [ProtoMember(2)]
        public string Name { get; set; }
        [ProtoMember(3)]
        public DateTime CreatedAt { get; set; }
        [ProtoMember(4)]
        public bool Done { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Item item = new Item
            {
                Key = "Foo",
                Name = "Bar",
                CreatedAt = DateTime.Now,
                Done = true,
            };

            using (Stream stream = new MemoryStream())
            using (StreamReader reader = new StreamReader(stream))
            {
                // シリアライズ
                Serializer.Serialize(stream, item);

                // コンソールに出力する
                stream.Seek(0, SeekOrigin.Begin);
                Console.WriteLine(reader.ReadToEnd());

                // 元に戻す
                stream.Seek(0, SeekOrigin.Begin);
                Item result = Serializer.Deserialize<Item>(stream);
                Console.WriteLine(item.Key);
                Console.WriteLine(item.Name);
            }

            Console.ReadLine();
        }
    }
}

ProtoMember のコンストラクタは tag という引数を受け取ります。これは 1 から始まる整数で、重複不可。メンバを識別する ID みたいなものです。

このサンプルの実行結果がこちら。

f:id:griefworker:20100128121138p:image

クラスの属性を付けるだけでいいという、手軽さが魅力ですね。

.proto ファイルも使えます

ProtoBuf.NET には protogen というツールが付属してます。このツールを使えば、.proto ファイルをもとにクラスのソースコードを生成できます。

package protobufsample;

message Item {
  required string key = 1;
  required string name = 2;
  required bool done = 3;
  required string created_at = 4;
}

この .proto ファイルを protogen に渡して、C# 用のクラスを生成します。

protogen.exe -i:protoファイル名 -o:出力ファイル名

protobuf と使い方が違うので注意。先ほどの .proto ファイルを元に生成したクラスがこちら。

//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

// Generated from: models.proto
namespace protobufsample
{
  [global::System.Serializable, global::ProtoBuf.ProtoContract(Name=@"Item")]
  public partial class Item : global::ProtoBuf.IExtensible
  {
    public Item() {}
    
    private string _key;
    [global::ProtoBuf.ProtoMember(1, IsRequired = true, Name=@"key", DataFormat = global::ProtoBuf.DataFormat.Default)]
    public string key
    {
      get { return _key; }
      set { _key = value; }
    }
    private string _name;
    [global::ProtoBuf.ProtoMember(2, IsRequired = true, Name=@"name", DataFormat = global::ProtoBuf.DataFormat.Default)]
    public string name
    {
      get { return _name; }
      set { _name = value; }
    }
    private bool _done;
    [global::ProtoBuf.ProtoMember(3, IsRequired = true, Name=@"done", DataFormat = global::ProtoBuf.DataFormat.Default)]
    public bool done
    {
      get { return _done; }
      set { _done = value; }
    }
    private string _created_at;
    [global::ProtoBuf.ProtoMember(4, IsRequired = true, Name=@"created_at", DataFormat = global::ProtoBuf.DataFormat.Default)]
    public string created_at
    {
      get { return _created_at; }
      set { _created_at = value; }
    }
    private global::ProtoBuf.IExtension extensionObject;
    global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
      { return global::ProtoBuf.Extensible.GetExtensionObject(ref extensionObject, createIfMissing); }
  }
  
}

あとは属性のときと同じように、Serializer を使ってシリアライズします。

まとめ

同じ .proto ファイルから生成したクラスを使えば、たとえ言語が違っても、シリアライズ・デシリアライズできます。C# だけで利用するなら属性で十分だけど、他の言語も併用するなら、 .proto ファイルを使うことになりますね。

クライアントは C#、サービス側は JavaPython で開発するプロジェクトで真価を発揮しそう。そんなプロジェクト、私はまず出会わないけど。