Interprocess Communication Between .NET and MFC Using C# and COPYDATA

This lab note introduces several .NET features that facilitate communication between processes. Numerous methods of interprocess communication exist between applications including sockets, named or anonymous pipes, shared memory, etc. This note uses the Windows API SendMessage function, which is provided by the user32.dll library. In the example below, a C# program using .NET will send a message to a C++ program written using MFC. In addition to demonstrating the interprocess communication method, this example illustrates memory allocation and string conversion using the Marshal class, how public DLL functions can be called from a .NET program, and how a Windows Forms application is structured. The result will appear as two side-by-side programs with controls allowing the C# .NET program to send a user-defined message and the C++ MFC program returning a user-defined response.

Step 1. The C# .NET Windows Forms Application

We begin by creating a new project of the type Windows Forms Application. Here, we've named our solution "k5" and our project "sender". We've set the Assembly name on the project properties dialog to be "sender" so that our executable will be "sender.exe".

Note that we've renamed the default Form1 class to "senderform" and have added an additional "sender.cs" class file. Also, we will need the System, System.Drawing, and System.Windows.Forms references. The three .cs files in the Properties folder are system generated. We need not, and should not, edit them.

Step 2. Coding the C# Classes

The program entry is the static Main method of the "sender" class. This is a very simple class that makes three method calls. The first call is to the EnableVisualStyles method of the Application class. This method enables the controls to be drawn with visual styles. This is usually the first call in the Main method of a Windows Forms application. The second call is to the SetCompatibleTextRenderingsDefault method. Passing "false" to this method instructs .NET to use the TextRenderer class to render text using GDI. Passing "true" to this methods instructs .NET to use the Graphics class to render text using GDI+. Finally, the Main method calls the Run method. We pass to the Run method a new instance of our second class, the "senderfrom" class. Note in this example that we are using the "namespace" keyword to encapsulate our class names within a single namespace.

Our second class, "senderform" is a bit more complicated. First we will need to access several .NET classes, including Process, Marshal, Encoding, Form and Message. So, we have added the necessary "using" statements at the start of the C# source file. Next we declare our senderform class, inherited from the Form base class. Note that this is a "partial" declaration because some methods for this class will be automatically created for us in the senderform.Designer.cs as we construct the form using the form designer.

At the start of the class declaration, we define a symbolic constant WM_COPYDATA that must evaluate to the Windows API message number for this message. We also declare a structure, COPYDATASTRUCT, that must correspond to the structure expected by the Windows API. Lastly, before our methods, we declare an external function SendMessage and inform .NET that this method is to be found in the user32.dll library.

There are four methods defined for senderclass in senderclass.cs. The first method is the constructor. This method simply calls InitializeComponent to perform common form initialization functions and display the form.

The second method implements a handler for the Send_Click event. This event occurs when the "Send" control, defined as a button on our form, is clicked. This method interrogates the value of the Request edit control text. If no text has been entered, the user is prompted to enter a request to send. If text was entered, the GetProcessesByName method is called to find our MFC program instances. If the length of the returned array is zero, then our MFC program is not running. In this event, we notify the user and set the input focus back to the request field.

If the user did enter a request to send and the MFC program is running, we initialize our COPYDATASTRUCT local variable to hold our .NET program window handle, an ANSI representation of our request string, and the length of our request. Note that we use AllocCoTaskMem and StringToCoTaskMemAnsi methods to allocate storage and populate that storage with the ANSI string expected by the MFC application.

Next, we iterate through each instance of our MFC application, allocating a block of memory to hold the unmarshalled data to be sent with the WM_COPYDATA message. The StructureToPtr method takes the marshalled structure instances cds and populates allocated memory pointed to by iPtr with the unmarshalled data. Then the user32.dll function SendMessage can be called, referencing the IntPtr iPtr which address the unmarshalled data. For each loop through our foreach clause, we release the unmarshalled memory. Then after we have sent our request to each instance of the MFC application, we release the memory allocated for our marshalled COPYDATASTRUCT structure.

The third senderform method implemented in senderform.cs is a second event handler. This handler is for the "Clear_Click" event which occurs when the user clicks the "Clear" button. This method simply clears the Request and Response control text, sets the Status control text to a default message, and sets the input focus to the Request control.

Finally, we implement a WndProc method that overrides the default WndProc method for the Form class. Note the "override" keyword in the method declaration. This method handles a WM_COPYDATA message received by the .NET application. This event will occur when our MFC application returns a response to the .NET application by sending a WM_COPYDATA message to the window handle included in the original request.

When the application receives the WM_COPYDATA message, a new COPYDATASTRUCT instance is created and the PtrToStructure method is called to take the unmarshalled data from the message and populate our marshalled COPYDATASTRUCT with it. If data was returned as a response, we copy the response data into a marshalled byte array. Then, we setup an Encoding object to convert the ASCII data into a Unicode string. We set the Response control contents to this string, update our status message and set our message result code.

If no response data was found, we notify the user of this in the Status control. For any other message received by the form, we allow the base Form class to process it by calling base.WndProc.

To access the form designer to graphically edit our Windows form, simply right click on the senderform.cs file and select "View Designer".

Step 3. Coding the MFC Application

For this example, we created a dialog-based application in Visual C++ using the application wizard. This creates all the required files. What remains is to add controls to the dialog, event handlers and some initialization logic.

All the code we need to add is in the mfc1Dlg.cpp file. There are four methods for our dialog box application class that we need to edit. The first two are event handlers for the Send and Clear buttons, similar to those we saw above for the .NET application.

In the OnSend1 method, we setup a COPYDATASTRUCT structure to send a test message, "Test" to ourself, the MFC application. This can be used to independently test the handling of the WM_COPYDATA message logic that we will see below. In the OnClear1 method, we reset the IDC_REQUEST and IDC_RESPONSE control text, Set the status message to a default message and set the focus to the response control.

When the MFC application receives the WM_COPYDATA message, it will call the OnCopyData method of the dialog. Here, we verify that we have received a pointer to a COPYDATASTRUCT structure. Next, if we have both a data and a length in the COPYDATASTRUCT we set the text of the IDC_REQUEST control to the data passed from the C# program. If a window handle was also passed in the dwData member of the COPYDATASTRUCT, we construct a response in a local COPYDATASTRUCT and send the response message back to the C# program. Finally, we update the status control text.

Lastly, we add some lines of code in the OnInitDialog method to initialize the contents of our controls.