Edited at

.Net(C# WPF)アプリとWin32アプリ(C++)でファイルマッピングを使って構造体データをプロセス間通信させる

More than 1 year has passed since last update.


前置き

 とあるアプリを作成するのに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)と出来るように組んでおくと良いかも


参考:

C#でレガシーな事をする方向けのまとめ