前置き
とあるアプリを作成するのにWin32フックDLLと.Netのクライアントアプリ間でデータを連携させる必要があって、このデータ連携にファイルマッピングを採用することにしました。
ネットの断片的な資料を漁って、別の切り口の断片的な情報を浮かべようと思います(ぉぃ)
ファイルマッピング今昔
Win32APIだとCreateFileMapping / OpenFileMappingで得たハンドルからMapViewOfFileを使ってポインタを得て構造体にキャストして使えて簡単です。
.Net4.0未満の場合はそれらAPIをマーシャリングする所から始めるメンドクサーメソッドだったけど、.Net4.0からはMemoryMappedFileで多少イージーになったようでありがたいです。
サンプル
C#のアプリ(WPF)とCのアプリ(今回はWin32SDKをベタで使うタイプ)でサンプルを作ったものを抜粋します。
WPFアプリ
namespace IPCwithCSandCPPSample
{
/// <summary>
/// テストクラス:ファイルマッピングへマッピングするためのクラス
/// C++側で閲覧するためデータ構造が同一になるように配慮すること
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode, Pack = 4)]
public class Test1
{
public int A;
public bool B;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
public string C;
public Test2 D = new Test2();
}
/// <summary>
/// テストクラス2
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode, Pack = 4)]
public class Test2
{
public int AA;
}
/// <summary>
/// MainWindow.xaml の相互作用ロジック
/// </summary>
public partial class MainWindow : Window
{
private MemoryMappedFile mmf;
private MemoryMappedViewStream mmvs;
private Test1 testclass = new Test1();
public MainWindow()
{
InitializeComponent();
// 頻繁に開け閉めしなくてもよいので起動時に作成
mmf = MemoryMappedFile.CreateOrOpen("TestMap", (uint)Marshal.SizeOf(typeof(Test1)));
mmvs = mmf.CreateViewStream();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
// UIからデータを取り込む(右辺は全部WPFのテキストボックスです)
testclass.A = int.Parse(this.A.Text);
testclass.B = bool.Parse(this.B.Text);
testclass.C = this.C.Text;
testclass.D.AA = int.Parse(this.D.Text);
// 構造体の体裁を整えたデータクラスをファイルマッピングのストリームに流すためにバイト列に変換する
int size = Marshal.SizeOf(typeof(Test1));
byte[] bytes = new byte[size];
IntPtr ptr = Marshal.AllocCoTaskMem(size);
Marshal.StructureToPtr(testclass, ptr, false);
Marshal.Copy(ptr, bytes, 0, size);
Marshal.FreeCoTaskMem(ptr);
// ファイルマッピングに書く
mmvs.Write(bytes, 0, size);
mmvs.Seek(0, System.IO.SeekOrigin.Begin);
}
}
}
Win32APIアプリ
/// <summary>
/// テストクラス2
/// </summary>
typedef struct {
int AA;
}Test2;
/// <summary>
/// テストクラス:ファイルマッピングへマッピングされているクラス
/// </summary>
typedef struct {
int A;
BOOL B;
wchar_t C[256];
Test2 D;
}Test1;
HANDLE g_Handle = NULL;
Test1 *g_pTest = NULL;
/// <summary>
/// Opens the mapping.
/// </summary>
void OpenMapping()
{
if (g_Handle != NULL)
{
return;
}
// C#で作るのでこちらは開くだけ
g_Handle = OpenFileMapping(FILE_MAP_ALL_ACCESS, false, L"TestMap");
if (g_Handle == NULL)
{
return;
}
// マップファイルのアクセスはキャストして構造体から行える
g_pTest = (Test1*)MapViewOfFile(g_Handle, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(Test1));
if (g_pTest == NULL)
{
CloseHandle(g_Handle);
return;
}
}
/// <summary>
/// Closes the mapping.
/// </summary>
void CloseMapping()
{
if (g_Handle == NULL)
{
return;
}
CloseHandle(g_Handle);
}
/// <summary>
/// Gets the mapping structure to string.
/// </summary>
/// <param name="string">The string.</param>
void GetMappingStructToString(wchar_t * string)
{
if (g_pTest == NULL)
{
return;
}
wsprintf(string, L"%d %d %s %d\n", g_pTest->A, g_pTest->B, g_pTest->C, g_pTest->D.AA);
}
/// <summary>
/// 200msに一回共有メモリを読んで表示を更新するよウィンドウ
/// </summary>
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static int timerID;
static wchar_t string[256];
switch (message)
{
case WM_CREATE:
timerID = SetTimer(hwnd, -1, 200, NULL);
OpenMapping();
break;
case WM_TIMER:
OpenMapping();
GetMappingStructToString(string);
InvalidateRect(hwnd, NULL, true);
break;
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
// TODO: 描画コードをここに追加してください...
{
RECT temp;
GetClientRect(hwnd, &temp);
DrawText(hdc, string, lstrlen(string), &temp, DT_TOP | DT_LEFT | DT_WORDBREAK);
}
EndPaint(hwnd, &ps);
break;
case WM_DESTROY:
CloseMapping();
KillTimer(hwnd, timerID);
PostQuitMessage(0);
break;
default:
return DefWindowProc(hwnd, message, wParam, lParam);
}
return 0;
}
共有する構造体に関して
- ファイルマッピングの中身はバイト列だ、と言うことを頭に入れて構造体を構成する
- C#ではクラスでもStructLayout出来るが上記は考慮すること
一つ目は当然のことですね。アドレス渡されても渡された側は見られません。
typedef struct {
int A;
BOOL B;
wchar_t C[256];
Test2 D;
}Test1;
こんな感じの構造体にした場合は
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode, Pack = 4)]
public class Test1
{
public int A;
public bool B;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
public string C;
public Test2 D = new Test2();
}
こんな感じで大丈夫。
注意点
- C#のboolをStructureToPtrするとレイアウト情報にPack=1と設定しても4バイトになるので、C99のboolで受けるならアライメント調整が必要
- 多分2バイトや3バイトも強制4バイトアライメントになる予感(知らんけど)
- CharSet = CharSet.Unicode (ここ数年のWin32SDKベタアプリ側はデフォでUnicode)
- 配列、文字列なんかは上限値必須
C#側の書き込み側の肝
// 構造体の体裁を整えたデータクラスをファイルマッピングのストリームに流すためにバイト列に変換する
int size = Marshal.SizeOf(typeof(Test1));
byte[] bytes = new byte[size];
IntPtr ptr = Marshal.AllocCoTaskMem(size);
Marshal.StructureToPtr(testclass, ptr, false);
Marshal.Copy(ptr, bytes, 0, size);
Marshal.FreeCoTaskMem(ptr);
// ファイルマッピングに書く
mmvs.Write(bytes, 0, size);
mmvs.Seek(0, System.IO.SeekOrigin.Begin);
- 構造体→バイト列→ストリームWrite の手順が必要
- Marshal.AllocCoTaskMemはMarshal.AllocHGlobalでも速度的にはあまり変化がない模様
mmvs.Seek(testclass.ToBytes(), 0, testclass.Length)と出来るように組んでおくと良いかも