背景
在 Windows 桌面开发中,C# 应用(WPF/WinForms)有时需要与同机运行的 C++ 原生程序通信。SendMessage 是最直接的进程内/跨进程 IPC 方式之一,尤其适合:
- C# 插件/前端 + C++ 核心引擎的混合架构
- 托管代码调用无法用 COM 或命名管道改造的老旧 C++ 模块
- 简单的命令/通知信号(不需要传大量数据)
P/Invoke 声明
using System;
using System.Runtime.InteropServices;
public class NativeMessaging
{
// 导入 user32.dll 中的 SendMessage
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)]
private static extern IntPtr SendMessage(
IntPtr hWnd, // 目标窗口句柄
uint Msg, // 消息 ID
IntPtr wParam, // 附加参数 1
IntPtr lParam // 附加参数 2
);
// WM_USER = 0x0400,自定义消息从此偏移
private const uint WM_USER = 0x0400;
private const uint WM_MY_COMMAND = WM_USER + 1;
private const uint WM_MY_STRING_MSG = WM_USER + 2;
}
发送简单数值消息
// 发送一个枚举命令(整数值放在 wParam 或 lParam)
public static void SendCommand(IntPtr hWnd, int commandCode)
{
// wParam 传命令码,lParam 不用
IntPtr result = SendMessage(hWnd, WM_MY_COMMAND,
new IntPtr(commandCode),
IntPtr.Zero);
Console.WriteLine($"SendMessage returned: {result}");
}
发送字符串
字符串需要先 Marshal 到非托管内存,再将指针传给 C++:
public static void SendStringToNative(IntPtr hWnd, string text)
{
// 将托管字符串编码为 ANSI(char*)
IntPtr pStr = Marshal.StringToHGlobalAnsi(text);
try
{
// 将字符串指针作为 wParam 传递
// lParam 可传附加标记或保留为 0
SendMessage(hWnd, WM_MY_STRING_MSG, pStr, IntPtr.Zero);
}
finally
{
// 必须释放!Marshal.StringToHGlobalAnsi 在非托管堆分配内存
// SendMessage 是同步的,C++ 侧在此行返回前已处理完毕
Marshal.FreeHGlobal(pStr);
}
}
重要:
SendMessage是同步的——它会等 C++ 的WndProc处理完消息后才返回。因此在finally中释放内存是安全的,C++ 已经完成了对字符串内容的读取。
C++ 接收方
// 对应的消息 ID
constexpr UINT WM_MY_COMMAND = WM_USER + 1;
constexpr UINT WM_MY_STRING_MSG = WM_USER + 2;
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_MY_COMMAND:
{
// wParam 就是命令码(整数)
int code = static_cast<int>(wParam);
switch (code)
{
case 1: /* 处理命令 1 */ break;
case 2: /* 处理命令 2 */ break;
}
return 0;
}
case WM_MY_STRING_MSG:
{
// wParam 是指向 ANSI 字符串的指针
// 注意:该指针由 C# 分配,SendMessage 返回后会被释放
// 所以必须在此函数内完成读取,不能保存指针!
LPCSTR pStr = reinterpret_cast<LPCSTR>(wParam);
// 立即拷贝到本地变量
CString cstr(pStr); // MFC CString(自动转宽字节)
std::string stdStr(pStr); // 或 std::string
// 使用字符串
AfxMessageBox(cstr); // 仅示例,生产代码勿用阻塞 UI
return 0;
}
}
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
跨进程注意事项
上述方案适用于同进程(例如 C++ DLL 被 C# 加载)或同机跨进程。
跨进程时,wParam/lParam 中的指针无法直接跨进程访问(虚拟地址空间隔离)。解决方案:
| 方案 | 适用场景 |
|---|---|
WM_COPYDATA | 跨进程传递数据的官方消息 |
| 共享内存 | 大量数据,高频次 |
| 命名管道 / Socket | 通用 IPC,推荐现代方案 |
WM_COPYDATA 示例(跨进程字符串)
// C# 发送方
[StructLayout(LayoutKind.Sequential)]
private struct COPYDATASTRUCT
{
public IntPtr dwData;
public int cbData;
public IntPtr lpData;
}
public static void SendStringCrossProcess(IntPtr hWnd, string text)
{
byte[] bytes = System.Text.Encoding.UTF8.GetBytes(text + "\0");
IntPtr pData = Marshal.AllocHGlobal(bytes.Length);
Marshal.Copy(bytes, 0, pData, bytes.Length);
var cds = new COPYDATASTRUCT
{
dwData = new IntPtr(1), // 自定义类型标记
cbData = bytes.Length,
lpData = pData
};
IntPtr pCds = Marshal.AllocHGlobal(Marshal.SizeOf(cds));
Marshal.StructureToPtr(cds, pCds, false);
SendMessage(hWnd, 0x004A /* WM_COPYDATA */, IntPtr.Zero, pCds);
Marshal.FreeHGlobal(pCds);
Marshal.FreeHGlobal(pData);
}
// C++ 接收方
case WM_COPYDATA:
{
auto* cds = reinterpret_cast<COPYDATASTRUCT*>(lParam);
if (cds->dwData == 1)
{
// lpData 是系统复制的副本,直接使用,无生命周期问题
std::string text(reinterpret_cast<char*>(cds->lpData),
cds->cbData - 1); // 去掉 null terminator
}
return TRUE;
}
总结
- 同进程:
SendMessage+Marshal.StringToHGlobalAnsi→ C++ 侧直接reinterpret_cast<LPCSTR>读取 - 必须在
finally释放 Marshal 分配的内存(SendMessage同步返回后已安全) - C++ 侧接收后立即拷贝字符串,不要保存原始指针
- 跨进程场景改用
WM_COPYDATA,系统会自动复制内存区域