先日、C++からC#へ配列型と文字列型を含んだ構造体を渡すようなアプリを実装する機会があった。
異なる基盤の間でデータをやり取りする時にデータの変換が必要になる場合があるが、これをマーシャリングという。
配列型や文字列型は非Blittable型に分類されており、そのままではC++からC#へデータを渡すことはできない。
そのため、非Blittable型はC++からC#へ直接受け渡し可能なBlittable型へ変換する必要がある。
マーシャリングの基本知識についてはこのページが分かりやすかった。
データの受け渡し部分の実装はアプリの構成によって様々ある。
今回はC++からC#へ配列型と文字列型を含んだ構造体を渡す場合のデータ変換の一例を紹介する。
なお、C++とC#のコード間で型の比較をしやすくするため、16bit符号なし整数と32bit符号なし整数の型名は以下のように読み替えている。
- 16bit符号なし整数:WORD
- 32bit符号なし整数:DWORD
C++側のデータ変換
例えば以下のような構造体があったとする。
struct CPP_STRUCT
{
WORD WordData;
DWORD DwordData;
std::vector<WORD> WordVectorData;
std::wstring StringData;
};
構造体自体はポインタで受け渡しすれば良いが、構造体に定義されている配列型と文字列型はBlittable型へ変換しなければならない。
従って、CPP_STRUCTを以下のような構造体に変換すればC#側に直接データを受け渡しできる。
配列型と文字列型は先頭アドレスとデータサイズへの変換となる。
C#へ受け渡し後、先頭アドレスとデータサイズを使ってデータを復元することになる。
#pragma pack(push, 1)
typedef struct
{
WORD WordData;
DWORD DwordData;
WORD* WordVectorData;
DWORD SizeOfWordVectorData;
WCHAR* StringData;
DWORD SizeOfStringData;
} CPP_STRUCT_EXPORT;
#pragma pack(pop)
pragmaについては今回割愛するが、簡単にいうとC++とC#で同じようにメモリを取るようにするためのおまじない。
CPP_STRUCTのデータを、受け渡し用のCPP_STRUCT_EXPORTに詰め替えるコードは以下。
void FillExportValues(CPP_STRUCT_EXPORT* p, CPP_STRUCT& in)
{
// WORD, DWORD
p->WordData = in.WordData;
p->DwordData = in.DwordData;
// Array
p->WordVectorData = new WORD[in.WordVectorData.size()];
memcpy(p->WordVectorData, in.WordVectorData.data(), sizeof(WORD) * in.WordVectorData.size());
p->SizeOfWordVectorData = in.WordVectorData.size();
// String
p->StringData = new WCHAR[in.StringData.length() + 1];
memcpy(p->StringData, in.StringData.c_str(), sizeof(WCHAR) * (in.StringData.length() + 1));
p->SizeOfStringData = in.StringData.length();
}
C#側のデータ変換
C++側から渡されるCPP_STRUCT_EXPORTは以下の構造体で受け取ることができる。
StructLayoutやMarshalAsはC++のpragmaと同様、メモリの取り方を指定するおまじない。
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode, Pack = 1)]
public class CSHARP_STRUCT_EXPORT
{
[MarshalAs(UnmanagedType.U2)]
public WORD WordData;
[MarshalAs(UnmanagedType.U4)]
public DWORD DwordData;
public IntPtr WordVectorData;
[MarshalAs(UnmanagedType.U4)]
public DWORD SizeOfWordVectorData;
public IntPtr StringData;
[MarshalAs(UnmanagedType.U4)]
public DWORD SizeOfStringData;
}
このままでは配列型と文字列型が先頭ポインタとデータサイズで表現されており扱いにくい。
以下のような構造体に詰め替えればC#側で扱いやすい形になるだろう。
public class CSHARP_STRUCT
{
public WORD WordData;
public DWORD DwordData;
public WORD[] WordVectorData;
public string StringData;
}
CSHARP_STRUCT_EXPORTからCSHARP_STRUCTへの詰め替えは以下のようにする。
public static void RepackValues(CSHARP_STRUCT values, CSHARP_STRUCT_EXPORT exported)
{
// WORD, DWORD
values.WordData = exported.WordData;
values.DwordData = exported.DwordData;
// Array
values.WordVectorData = new WORD[(int)exported.SizeOfWordVectorData];
var gch = GCHandle.Alloc(values.WordVectorData, GCHandleType.Pinned);
try
{
var targetPtr = Marshal.UnsafeAddrOfPinnedArrayElement(values.WordVectorData, 0);
CopyMemory(targetPtr, exported.WordVectorData, (uint)Marshal.SizeOf<WORD>() * exported.SizeOfWordVectorData);
}
finally
{
gch.Free();
}
// String
values.StringData = Marshal.PtrToStringUni(exported.StringData, (int)exported.SizeOfStringData);
}
まとめ
C++からC#へ配列型と文字列型を含んだ構造体を渡したい場合はマーシャリングが必要になる。
非Blittable型は直接受け渡しできないので、非Blittable型をBlittable型へ変換する必要がある。