C#で実メモリデータイメージを扱う
周辺デバイスなどと通信するプログラムでは、送受信するバイナリデータイメージを扱う機会に良く出くわします。この手の処理が多い場合はVC++のNativeでのプログラミングが便利です。しかし、C#のプログラミング生産性を考えると、単にバイナリデータイメージを扱う必要があるからといってVC++ Nativeで全てコーディングするという選択肢は短絡的です。ここでは、C#コードの中で、バイナリデータ構造を直接アクセスするコードサンプルを紹介します。
C#のプログラムの中で図のようなデータストリームを扱うものとします。
小さな四角が1バイト、小さな四角2つが2バイト、4つが4バイトのデータであるとします。送信するデータストリームの構造が図の場合、先ず、このデータ構造を扱う為のstructを定義します。
using System.Runtime.InteropServices;
[StructLayout( LayoutKind.Sequential, Pack=1)]
public struct BinaryDataStructure
{
public uint command;
public byte flag0;
public byte flag1;
public byte flag2;
public byte flag3;
public ushort sup0;
public ushort sup1;
public uint data;
}
図と見比べればこれが同じデータ構造であると分かるでしょう。
次に、このデータ構造をデータ列に変換するコードを紹介します。適当なクラスに以下のメソッドを記述します。
public unsafe void SendBinaryData(BinaryDataStructure data)
{
byte[] sendBuf = new byte[sizeof(BinaryDataStructure)];
fixed( byte* basePtr = &sendBuf[0] )
{
BinaryDataStructure* bds = (BinaryDataStructure*)basePtr;
bds->command = data.command;
bds->flag0 = data.flag0;
bds->flag1 = data.flag1;
bds->flag2 = data.flag2;
bds->flag3 = data.flag3;
bds->sup0 = data.sup0;
bds->sup1 = data.sup1;
bds->data = data.data;
}
// sendBufにはデータ構造通りのデータ列が出来上がっているので送信処理へ
…
}
}
こんな感じ。逆に受信したデータから組み上げる場合は、
public unsafe BinaryDataStructure ResolveBinaryData(byte[] receivedData)
{
var resolved = new BinaryDataStructure();
fixed( byte* basePtr = &receivedData[0] )
{
BinaryDataStructure* bds = (BinaryDataStructure*)basePtr;
resolved.command = bds->command;
resolved.flag0 = bds->flag0;
resolved.flag1 = bds->flag1;
resolved.flag2 = bds->flag2;
resolved.flag3 = bds->flag3;
resolved.sup0 = bds->sup0;
resolved.sup1 = bds->sup1;
resolved.data = bds->data;
}
return resolved;
}
こういう感じになります。C#ではあまり*とか->は見かけませんが、ポインタも使えたりするわけです。それから、unsafeキーワード。こちらもあまり見かけないキーワードですが、データ構造を生で触るには、仮想マシンの管理外のメモリ領域をさわさわするので、こんなキーワードが必要になります。
…で、これだけでは駄目で、ソリューションエクスプローラーでプロジェクトを右クリック→プロパティを選択します。
で、ビルドタブの”アンセーフコードの許可”にチェックマークを入れます。これで、バイナリデータ列をC#で容易にアクセスできるようになります。アンセーフコードの領域が大きいのは品質上あまりよろしくないので、バイナリデータ構造を生触りするコードは、クラスライブラリプロジェクトにして一箇所に纏めて部品化して使う事をお勧めします。