byte[]
配列からC#でC/C++データ構造を読み取るための包括的ガイド
言語間、特にC/C++からC#へのデータ構造を移行または操作する際、開発者はしばしばC/C++構造体のバイト表現を管理可能なC#の等価物に変換するという課題に直面します。このブログ投稿では、C/C++構造体データを含むbyte[]
配列をC#構造体に効率的に変換する方法について説明します。不要なデータコピーから生じる煩わしさを避けます。
問題の理解
以下のC構造体定義を考えてみましょう。
typedef OldStuff {
CHAR Name[8];
UInt32 User;
CHAR Location[8];
UInt32 TimeStamp;
UInt32 Sequence;
CHAR Tracking[16];
CHAR Filler[12];
}
この表現は、固定された順序の文字と整数から成っています。このようなバイナリデータをC#で扱う際、開発者は構造体を正確に配置し、管理コード用に変換するという課題に直面します。
対応するC#構造体は、StructLayout
を使用してデータのメモリ内での配置を制御します。
[StructLayout(LayoutKind.Explicit, Size = 56, Pack = 1)]
public struct NewStuff
{
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 8)]
[FieldOffset(0)]
public string Name;
[MarshalAs(UnmanagedType.U4)]
[FieldOffset(8)]
public uint User;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 8)]
[FieldOffset(12)]
public string Location;
[MarshalAs(UnmanagedType.U4)]
[FieldOffset(20)]
public uint TimeStamp;
[MarshalAs(UnmanagedType.U4)]
[FieldOffset(24)]
public uint Sequence;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 16)]
[FieldOffset(28)]
public string Tracking;
}
従来のアプローチ
開発者が取る典型的なアプローチは、byte[]
配列を別のバッファにコピーしてから、それを目的のC#構造体にマージするというものです。一般的に見られる方法は以下の通りです。
GCHandle handle;
NewStuff MyStuff;
int BufferSize = Marshal.SizeOf(typeof(NewStuff));
byte[] buff = new byte[BufferSize];
Array.Copy(SomeByteArray, 0, buff, 0, BufferSize);
handle = GCHandle.Alloc(buff, GCHandleType.Pinned);
MyStuff = (NewStuff)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(NewStuff));
handle.Free();
このアプローチは機能しますが、データの中間コピーを作成することによるメモリ使用量やパフォーマンスに関して余分なオーバーヘッドを発生させます。
より良い解決策
データを複製する代わりに、byte[]
配列を直接利用して構造体を抽出できます。より効率的な方法は以下の通りです。
GCHandleを直接使用する
NewStuff ByteArrayToNewStuff(byte[] bytes)
{
GCHandle handle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
try
{
NewStuff stuff = (NewStuff)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(NewStuff));
return stuff;
}
finally
{
handle.Free();
}
}
この方法では、元のバイト配列へのハンドルを割り当てるため、追加のバッファを作成する必要がありません。
柔軟性のためのジェネリックメソッド
バイト配列から任意の構造体に変換したい場合、ジェネリックメソッドを作成できます。
T ByteArrayToStructure<T>(byte[] bytes) where T : struct
{
T stuff;
GCHandle handle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
try
{
stuff = (T)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(T));
}
finally
{
handle.Free();
}
return stuff;
}
パフォーマンス向上のためのアンセーフアプローチ
上級ユーザー向けには、unsafe
コンテキストを利用すると、ポインタを直接扱うことができ、さらなるパフォーマンス向上が得られます。
unsafe T ByteArrayToStructure<T>(byte[] bytes) where T : struct
{
fixed (byte* ptr = &bytes[0])
{
return (T)Marshal.PtrToStructure((IntPtr)ptr, typeof(T));
}
}
結論
byte[]
配列からC#にC/C++データ構造を効果的に読み込むことは、面倒である必要はありません。GCHandle
をバイト配列に直接使用するか、unsafe
コードを利用することで、パフォーマンスを向上させ、コードを簡素化することができます。このアプローチを採用することで、不要なメモリオーバーヘッドを避けながら、よりクリーンで保守可能なコードを実現できます。
次回、異なる言語間のデータ変換を扱うことになったら、これらの方法を思い出してプロセスを簡略化しましょう!