Introduction
I was recently assigned a task where I wanted to use some of the functionality built into the Microsoft .NET Framework with an existing MFC file viewer application. More specifically, I wanted to utilize the FileSystemWatcher
class to notify the MFC Viewer app when a new file was created so it could display it automatically. Since MFC could not directly access this service, I had two options:
- Implement my own
FileSystemWatcher
class in unmanaged Visual C++. - Provide some kind of hosting mechanism or linkage between .NET and MFC.
The first option would have been an interesting feature to create, but it would have taken time that I did not have. The idea of hosting the MFC viewer had the advantage that I could implement the file detection in .NET and call the MFC application from within the hosting mechanism. There is a very good article on MFC hosting, written by Alexey Shalnov, see: Hosting of MFC MDI Applications from Within WinForms. This solution, however, would require a lot of recoding of the MFC application.
My hosting solution only required that the .NET application send the MFC program the file path of the new program when a new file appeared. Thus, I needed a simple interprocess communication mechanism between the running applications. I, therefore, used the WM_COPYDATA
to send the file name to the MFC application. When the MFC application received the COPYDATA
message, it took the file path and called its own display function.
The .NET Application
Obtaining a Handle to the Target Process
The Init
function starts the MFC application as a child process, and saves the main window handle which is needed to transmit the data. For my purposes, this method is much simpler than the traditional way of “finding the process” by using the Win32 functions FindWindow
and EnumWindows
. In addition, it guarantees the handle of the correct instance of the target application.
private void Init(string processName, string cmdParams)
{
Process mfcApp = new Process();
mfcApp.StartInfo.FileName = processName;
mfcApp.StartInfo.Arguments = cmdParams;
mfcApp.Start();
if (mfcApp.WaitForInputIdle(5000))
{
destMainHandle = mfcApp.MainWindowHandle;
}
}
In order to utilize the COPY_DATA
messaging mechanism to send out data to another process, the following Win32 structure and function must be used:
public struct COPYDATASTRUCT
{
public IntPtr dwData;
public int cbData;
public IntPtr lpData;
}
[System.Runtime.InteropServices.DllImport("user32.dll",
CharSet = CharSet.Auto)]
public static extern IntPtr SendMessage(IntPtr hwnd, int msg,
IntPtr wparam, IntPtr lparam);
const int WM_COPYDATA = 0x4A;
Sending Data to the Target Process
The function below sends data from the .NET application to the MFC application. Because the MFC program was not built to be Unicode compatible, the string must be converted to ASCII format before it is to be sent. AStringToCoTaskMemAnsi
converts the Unicode string in the .NET program into the ASCII format needed by MFC.
The next thing that has to be done is to allocate memory from the COPYDATASTRUCT
structure. Since data is being sent out to the unmanaged world, one of two Marshal functions can be used to allocate the unmanaged memory block, AllocCoTaskMem
or AllocHGlobal
. System.Runtime.InteropServices
is then used to perform a SendMessage
to the MFC application.
The function SendMessageWithData
allocates unmanaged memory which contains the data to be sent to the MFC program. The string to be sent must be converted into ASCII format.
The allocation of memory and the string conversion can be done in one of two ways:
- The first method uses the Marshal functions
AllocCoTaskMem
and StringToCoTaskMemAnsi
to perform the memory allocation and string conversion. - The second method uses the functions
AllocHGlobal
and StringToHGlobalAnsi
to accomplish the same tasks.
The difference between these two sets of functions is that AllocCoTaskMem
is essentially a wrapper for the COM function CoTaskMem
, and allocates memory using the COM Task memory allocator. CoTaskMemAlloc
calls the COM API IMalloc::Alloc
. According to Microsoft, this function allocates memory in essentially the same way as the C function Alloc
, whereas AllocHGlobal
exposes the Win32 LocalAlloc
(which is the same as GlobalAlloc
in Win32) API, which is found in kernel32.dll, for the memory allocation.
public static void SendMessageWithData(IntPtr destHandle,
string str, IntPtr srcHandle)
{
COPYDATASTRUCT cds;
cds.dwData = srcHandle;
str = str + '\0';
cds.cbData = str.Length + 1;
cds.lpData = Marshal.AllocCoTaskMem(str.Length);
cds.lpData = Marshal.StringToCoTaskMemAnsi(str);
IntPtr iPtr = Marshal.AllocCoTaskMem(Marshal.SizeOf( cds));
Marshal.StructureToPtr(cds,iPtr, true);
SendMessage(destHandle, WM_COPYDATA, IntPtr.Zero, iPtr);
Marshal.FreeCoTaskMem(cds.lpData);
Marshal.FreeCoTaskMem(iPtr);
}
public static void SendMessageWithDataUsingHGlobal(IntPtr destHandle,
string str, IntPtr srcHandle)
{
COPYDATASTRUCT cds;
cds.dwData = srcHandle;
str = str + '\0';
cds.cbData = str.Length + 1;
cds.lpData = Marshal.AllocHGlobal(str.Length);
cds.lpData = Marshal.StringToHGlobalAnsi(str);
IntPtr iPtr = Marshal.AllocHGlobal(Marshal.SizeOf(cds));
Marshal.StructureToPtr(cds, iPtr, true);
SendMessage(destHandle, WM_COPYDATA, IntPtr.Zero, iPtr);
Marshal.FreeHGlobal(cds.lpData);
Marshal.FreeHGlobal(iPtr);
}
The MFC Application
On the MFC side of things, an OnCopyData
message handler has to be created in the MainFrame
class. (Note that the destination handle to be used in the SendMessage
call in the C# program is the MainWindow
handle of the MFC program’s mainframe window.)
The file path string that was sent from the managed .NET world is then easily translated from the COPYDATASRUCT
block, and can be used by the MFC application. After the MFC program receives a message from the C# application, it sends an acknowledgement back to the caller.
See below:
BOOL CMainFrame::OnCopyData(CWnd* pWnd, COPYDATASTRUCT *pCopyDataStruct)
{
char szbuf[256];
::ZeroMemory(szbuf,256);
strncpy(szbuf,(char *)pCopyDataStruct->lpData,pCopyDataStruct->cbData);
CString filePath = CString(szbuf);
CMFCTestProgramDoc* pDocument =
(CMFCTestProgramDoc *) GetActiveView()->GetDocument();
pDocument->FileImportAutomatic(filePath);
::SendMessage((HWND)pCopyDataStruct->dwData,WM_COPYDATA,0,0);
return TRUE;
}
Receiving the Acknowledgement
To receive the acknowledgement message from the MFC application, the WndProc
handler of the C# application must be overridden. The code below shows how to decode the COPYDATASTRUCT
structure from the message variable, and how to convert the ASCII string into UNICODE format.
protected override void WndProc(ref Message m)
{
switch (m.Msg)
{
case WM_COPYDATA:
if (m.Msg == WM_COPYDATA)
{
COPYDATASTRUCT cds = new COPYDATASTRUCT();
cds = (COPYDATASTRUCT)Marshal.PtrToStructure(m.LParam,
typeof(COPYDATASTRUCT));
if (cds.cbData > 0)
{
byte[] data = new byte[cds.cbData];
Marshal.Copy(cds.lpData, data, 0, cds.cbData);
Encoding unicodeStr = Encoding.ASCII;
char[] myString = unicodeStr.GetChars(data);
string returnText = new string(myString);
MessageBox.Show("ACK Received: " + returnText);
m.Result = (IntPtr)1;
}
}
break;
}
base.WndProc(ref m);
}
Points of Interest
For further investigation into the subject of interprocess communication using WM_COPY
, please refer to the following links:
History
- December 24, completed version 1.0.0.0.