在 C# 與非受控 DLL (C/C++) 交互時,透過 Windows 訊息機制 (SendMessage/PostMessage) 進行跨邊界通訊是經典且可靠的方式。本文示範自訂訊息定義、WM_COPYDATA 結構化資料傳遞、視窗程序重寫 (WndProc)、DLL 端發送訊息完整流程。
WM_USER (0x0400) 以上為自訂訊息範圍
using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;
public static class Win32Api
{
// 訊息常數
public const int WM_USER = 0x0400;
public const int WM_COPYDATA = 0x004A;
public const int WM_APP = 0x8000;
// 自訂訊息
public const int WM_MY_CUSTOM_MSG = WM_USER + 100;
public const int WM_DLL_NOTIFY = WM_USER + 101;
// COPYDATASTRUCT 結構
[StructLayout(LayoutKind.Sequential)]
public struct COPYDATASTRUCT
{
public IntPtr dwData; // 使用者定義資料
public int cbData; // 資料長度 (位元組)
public IntPtr lpData; // 資料指標
}
// SendMessage
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, ref COPYDATASTRUCT lParam);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);
// PostMessage
[DllImport("user32.dll", SetLastError = true)]
public static extern bool PostMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);
// 尋找視窗
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
}
public class MessageReceiverForm : Form
{
protected override void WndProc(ref Message m)
{
switch (m.Msg)
{
case Win32Api.WM_COPYDATA:
HandleCopyData(ref m);
break;
case Win32Api.WM_MY_CUSTOM_MSG:
HandleCustomMessage(m.WParam, m.LParam);
break;
case Win32Api.WM_DLL_NOTIFY:
HandleDllNotification(m.WParam, m.LParam);
break;
default:
base.WndProc(ref m); // 重要:未處理訊息傳給基底類別
break;
}
}
private void HandleCopyData(ref Message m)
{
var cds = Marshal.PtrToStructure(m.LParam);
if (cds.cbData > 0 && cds.lpData != IntPtr.Zero)
{
// 依據 dwData 判斷資料類型
string data = Marshal.PtrToStringAnsi(cds.lpData, cds.cbData);
Console.WriteLine($"收到 WM_COPYDATA: Type={cds.dwData}, Data={data}");
// 回應發送端 (透過 wParam 取得發送端視窗控制代碼)
// m.Result = (IntPtr)1; // 表示處理成功
}
}
private void HandleCustomMessage(IntPtr wParam, IntPtr lParam)
{
int param1 = wParam.ToInt32();
int param2 = lParam.ToInt32();
Console.WriteLine($"自訂訊息: Param1={param1}, Param2={param2}");
}
private void HandleDllNotification(IntPtr wParam, IntPtr lParam)
{
Console.WriteLine("收到 DLL 通知");
}
}
public class DllCommunicator
{
private IntPtr _dllWindowHandle;
public void SetDllWindowHandle(IntPtr hwnd) => _dllWindowHandle = hwnd;
public void SendStringToDll(string message)
{
if (_dllWindowHandle == IntPtr.Zero) return;
byte[] bytes = System.Text.Encoding.UTF8.GetBytes(message + "\0");
GCHandle handle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
try
{
var cds = new Win32Api.COPYDATASTRUCT
{
dwData = (IntPtr)1, // 自訂類型標識
cbData = bytes.Length,
lpData = handle.AddrOfPinnedObject()
};
Win32Api.SendMessage(_dllWindowHandle, Win32Api.WM_COPYDATA, this.Handle, ref cds);
}
finally
{
handle.Free();
}
}
}
// DllMain.cpp
#include
#include
HWND g_hTargetWnd = NULL; // C# 視窗控制代碼
HWND g_hMyWnd = NULL; // DLL 自己的視窗
// 視窗程序
LRESULT CALLBACK DllWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch (msg)
{
case WM_COPYDATA:
{
COPYDATASTRUCT* cds = (COPYDATASTRUCT*)lParam;
char* data = (char*)cds->lpData;
// 處理來自 C# 的資料
OutputDebugStringA("DLL 收到資料: ");
OutputDebugStringA(data);
OutputDebugStringA("\n");
return TRUE;
}
case WM_DESTROY:
PostQuitMessage(0);
break;
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
// 建立隱藏視窗
extern "C" __declspec(dllexport) void InitializeDll(HWND targetWnd)
{
g_hTargetWnd = targetWnd;
WNDCLASS wc = {0};
wc.lpfnWndProc = DllWndProc;
wc.hInstance = GetModuleHandle(NULL);
wc.lpszClassName = "MyDllWindowClass";
RegisterClass(&wc);
g_hMyWnd = CreateWindow("MyDllWindowClass", "DllHiddenWindow", 0, 0, 0, 0, 0, HWND_MESSAGE, NULL, GetModuleHandle(NULL), NULL);
}
// 發送通知給 C#
extern "C" __declspec(dllexport) void NotifyHost(int eventId, const char* data)
{
if (g_hTargetWnd == NULL) return;
std::string str = data ? data : "";
COPYDATASTRUCT cds = {0};
cds.dwData = eventId;
cds.cbData = (DWORD)str.length() + 1;
cds.lpData = (PVOID)str.c_str();
SendMessage(g_hTargetWnd, WM_COPYDATA, (WPARAM)g_hMyWnd, (LPARAM)&cds);
}
// 取得 DLL 視窗控制代碼供 C# 呼叫
extern "C" __declspec(dllexport) HWND GetDllWindowHandle()
{
return g_hMyWnd;
}
public static class DllImports
{
[DllImport("MyDll.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void InitializeDll(IntPtr targetWnd);
[DllImport("MyDll.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void NotifyHost(int eventId, string data);
[DllImport("MyDll.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr GetDllWindowHandle();
}
// 使用
public class DllHostForm : Form
{
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
DllImports.InitializeDll(this.Handle); // 傳遞 C# 視窗控制代碼給 DLL
IntPtr dllWnd = DllImports.GetDllWindowHandle();
_communicator.SetDllWindowHandle(dllWnd);
}
}
// C# 定義結構 (需與 C++ 記憶體配置一致)
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
public struct SensorData
{
public int SensorId;
public float Temperature;
public float Humidity;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
public string Location;
public long Timestamp;
}
// 發送結構體
public void SendSensorData(SensorData data)
{
int size = Marshal.SizeOf();
IntPtr ptr = Marshal.AllocHGlobal(size);
try
{
Marshal.StructureToPtr(data, ptr, false);
var cds = new Win32Api.COPYDATASTRUCT
{
dwData = (IntPtr)100, // SensorData 類型
cbData = size,
lpData = ptr
};
Win32Api.SendMessage(_dllWindowHandle, Win32Api.WM_COPYDATA, this.Handle, ref cds);
}
finally
{
Marshal.FreeHGlobal(ptr);
}
}
| 方式 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| WM_COPYDATA | 簡單、系統管理記憶體、同步/非同步皆可 | 需視窗控制代碼、資料大小限制 (~64KB)、單向 | C# 與 C++ DLL 簡單通訊、舊系統整合 |
| Named Pipes | 雙向、流式、跨機器、大資料 | 程式碼較複雜、需處理連線生命週期 | 高效能、大量資料、服務間通訊 |
| Shared Memory (MMF) | 極高效能、隨機存取 | 需同步機制 (Mutex)、生命週期管理複雜 | 即時資料共享、大型陣列/影像 |
| COM Interop | 型別安全、標準化、支援事件 | 註冊需求、版本控制、效能較低 | 商業元件、Office 自動化 |
| gRPC / HTTP | 跨平台、跨語言、契約優先 | 需網路堆疊、序列化開銷 | 微服務、現代架構 |
Windows 訊息機制 (WM_COPYDATA) 是 C# 與非受控 DLL 交互的經典橋樑。關鍵實作要點:
WndProc,處理 WM_COPYDATA 並正確解碼 COPYDATASTRUCTHWND_MESSAGE 視窗接收訊息,匯出函數供 C# 取得視窗控制代碼Marshal.StructureToPtr/PtrToStructure 確保記憶體配置一致 (Pack=1, CharSet.Ansi)AllocHGlobal/FreeHGlobal,接收端由系統自動管理