C#の共有メモリで簡単にオブジェクトを共有する方法

C#を使ってプロセス間でデータ共有をする際にオブジェクトを共有する方法です。

プロセス間でオブジェクトを共有したい場合、大抵の場合構造体を定義してメモリに書き込めばすれば良いとネットに書いてあります。ですが、普段クラスで扱っているデータをその時だけ構造体にするのは結構手間です。

また、オブジェクトを構造体で定義するのも手間です。定義の方法もC#ではすごくマイナーな相互運用で構造体を記述する知識が必要なのでもっと簡単にオブジェクトを共有できる方法を紹介したいと思います。

考え方

考え方は以下の通りです。

// ◆サーバー側
//
// C#のオブジェクトを作成
//   ↓
// オブジェクトをJsonにシリアライズ
//   ↓
// Jsonを共有メモリに展開

// ◆クライアント側
//
// Jsonを共有メモリから読み取る
//   ↓
// Jsonをオブジェクトにデシリアライズ
//   ↓
// C#のオブジェクトの使用

昨今のWebのプロトコル風にJSONを利用します。この方法だとバイナリ配列のやり取りと違ってオーバーヘッドが発生します。その代わり非常に扱いやすいです。

C++と連携してネイティブから読み出した場合も割と簡単に読み出せると思います。

準備

データを共有するための実装を作先に準備します。

共有するクラスの準備

まず共有したい型の定義です。後でシリアライズするのでクラスに以下のように[DataContract]および、[DataMember]属性を目印として付与します。既存クラスの場合も属性の追加だけで大丈夫です。

using System.Runtime.Serialization;

[DataContract]
public class Item
{
    [DataMember(Name = "id")]
    public int ID { get; set; }

    [DataMember(Name = "rate")]
    public double Rate { get; set; }

    [DataMember(Name = "name")]
    public string Name { get; set; }

    public override string ToString()
    {
        return $"{nameof(this.ID)}={ID}, {nameof(this.Rate)}={this.Rate}, {nameof(this.Name)}={this.Name}";
    }
}

JSONを読み書きするクラス

C#のライブラリにJSONを扱えるクラスがあるのでそれを使用してオブジェクト ⇔ JSON文字列の相互変換を定義します。

public static class JsonUtility
{
    // 任意のオブジェクトを JSON メッセージへシリアライズします。
    public static string Serialize(object graph)
    {
        using (var stream = new MemoryStream())
        {
            var serializer = new DataContractJsonSerializer(graph.GetType());
            serializer.WriteObject(stream, graph);
            return Encoding.UTF8.GetString(stream.ToArray());
        }
    }

    // Jsonメッセージをオブジェクトへデシリアライズします。
    public static T Deserialize<T>(string message)
    {
        using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(message)))
        {
            //var setting = new DataContractJsonSerializerSettings()
            //{
            //    UseSimpleDictionaryFormat = true,
            //};
            var serializer = new DataContractJsonSerializer(typeof(T)/*, setting*/);
            return (T)serializer.ReadObject(stream);
        }
    }
}

使用方法

上記のクラスを使って実際にオブジェクトを共有します。

サーバー側

サーバー側ではItemオブジェクトを作成してJSONに変換後に共有メモリにデータを展開します。

// サーバー側実装
public static void Main(string[] args)
{
    // 共有するオブジェクト
    var item = new Item
    {
        ID = 1,
        Rate = 2.553,
        Name = "sample1234567890",
    };

    var sharedMemory = MemoryMappedFile.CreateNew("share", 1024 * 1024 * 5); // 5MB分領域を確保
    using (MemoryMappedViewAccessor view = sharedMemory.CreateViewAccessor())
    {
        string jsonStr = JsonUtility.Serialize(item);
        char[] jsonCharArray = jsonStr.ToCharArray(0, jsonStr.Length); // C#のcharは2byte

        // 受け取り手はサイズが分からないので先頭にサイズを書いておく
        view.Write(0, jsonCharArray.Length);

        // Json文字列を共有メモリに書き込み
        view.WriteArray(sizeof(int), jsonCharArray, 0, jsonCharArray.Length);
    }

    Console.ReadLine(); // プロセスが終了するとデータが消えるのでここで止めておく

    using (sharedMemory) { }
}

読み取り側は共有メモリに実際にどれくらいデータが書き込まれているかわかりません。

そのため、共有メモリの先頭4バイトの領域にデータサイズを書き込んでいます。

クライアント側

クライアント側の実装です。

共有メモリからサイズを読んでJSONの読み取り → オブジェクトにデシリアライズを行います。

public static void Main(string[] args)
{
    string json = ReadDataByJson();

    // 読み取ったデータをデシリアライズしてオブジェクトに戻す
    var item = JsonUtility.Deserialize<Item>(json);

    Console.WriteLine($"{item}");
    // > ID=1, Rate=2.553, Name=sample1234567890
}

// 共有メモリからデータを読み取る
public static string ReadDataByJson()
{
    using (var sharedMemory = MemoryMappedFile.OpenExisting("share"))
    using (MemoryMappedViewAccessor view = sharedMemory.CreateViewAccessor())
    {
        // 共有メモリに先頭に書き込まれている配列サイズを取得する
        int size = view.ReadInt32(0);
        char[] jsonCharArray = new char[size];

        // 共有メモリからデータを取得する
        view.ReadArray(sizeof(int), jsonCharArray, 0, jsonCharArray.Length);

        // 扱いやすいように文字列にして返す
        return new string(jsonCharArray);
    }
}

関連記事