Introduction
This article demonstrates a quick and easy-to-use implementation for cross-AppDomain communication in .NET by leveraging Windows native messaging. The XDMessaging library is based on code developed to aid rapid development for a recent Vista project that required a lot of cross-AppDomain communication in a locked-down environment. It proved to be extremely useful in a number of scenarios where .NET Remoting would have been impractical, if not impossible, and actually solved more problems than I could have imagined due to its simplicity. The library is intended to send messages between multiple applications in a same-box scenario. For example, a task-tray application might want to communicate with or monitor a separate desktop application. The library does not implement cross-domain communication across a network, for which case .NET Remoting is sufficient.
Background
So, why not use .NET Remoting? Well, in the past I've personally found this extremely tedious to set up and configure. The other issue is the lack of helpful error reporting when things go wrong, invariably with permissions. Don't get me wrong, I'm not opposed to .NET Remoting. It has a lot more functionality than my own implementation, and of course is not limited to communication on a single box. However, for same-box communications, it doesn't need to be that complex. So, why not leverage Windows Messaging? After all, this is the mechanism that unmanaged applications use for exactly this purpose. Well now, there's an idea…
If you've never heard of them, Windows messages are low-level communications used by the Windows operating system to broadcast information regarding user-input, system changes and other events that applications running on the system can react to. For instance, application repaints are triggered by the WM_PAINT message. As well as system messages, unmanaged applications may also define custom Windows messages and use them to communicate with other windows. These usually take the form of WM_USER messages. If you have Spy++ installed (Visual Studio Tools), you can monitor all of the messages that a window receives in real-time.
The XDMessaging Library
The XDMessaging library provides an easy-to-use, zero-configuration solution to same-box cross-AppDomain communications. It provides a simple API for sending and receiving targeted string messages across application boundaries. The library allows the use of user-defined pseudo 'channels' through which messages may be sent and received. Any application can send a message to any channel, but it must register as a listener with the channel in order to receive. In this way, developers can quickly and programmatically devise how best their applications can communicate with each other and work in harmony.
Example: Sending a Message
XDBroadcast.SendToChannel("commands", "shutdown");
Example: Listening on a Channel
XDListener listener = new XDListener();
listener.RegisterChannel("events");
listener.RegisterChannel("status");
listener.RegisterChannel("commands");
listener.UnRegisterChannel("status");
Example: Handling the Messages
listener.MessageReceived+=XDMessageHandler(this.listener_MessageReceived);
private void listener_MessageReceived(object sender, XDMessageEventArgs e)
{
switch(e.DataGram.Message)
{
case "shutdown":
this.Close();
break;
}
}
The Messenger Demo
To see the demo, you will need to launch multiple instances of the Messenger.exe application. The demo application serves no practical purpose other than to demonstrate the use of the XDMessaging library. It shows how messages may be passed to multiple instances of a desktop application across application boundaries. The application uses two arbitrary channels, named Status and UserMessage. Window events such as onClosing and onLoad are broadcast as messages on the Status channel (displayed in green) and user messages are broadcast on the UserMessage channel (displayed in blue). By checking or unchecking the options, you may toggle which channel messages the window will listen for.
How It Works
The library makes use of a Windows system message of the type WM_COPYDATA. This system message allows data to be passed between multiple applications by carrying a pointer to the data we wish to copy, in this case a string. This is sent to other windows using the SendMessage Win32 API with PInvoke.
[StructLayout(LayoutKind.Sequential)]
public struct COPYDATASTRUCT
{
public IntPtr dwData;
public int cbData;
public IntPtr lpData;
}
[DllImport("user32", CharSet = CharSet.Auto)]
public extern static int SendMessage(IntPtr hwnd, int wMsg,
int wParam, ref COPYDATASTRUCT lParam);
The COPYDATASTRUCT struct contains information regarding the message data that we want to transfer to another application. This is referenced by the members lpData and dwData. lpData is a pointer to the string data that is stored in memory. dwData is the size of the transfer data. The cdData member is not used in our case. In order to pass the data, we must first allocate the message string to an address in memory and get a pointer to this data. To do this, we use the Marshal API as follows:
BinaryFormatter b = new BinaryFormatter();
MemoryStream stream = new MemoryStream();
b.Serialize(stream, raw);
stream.Flush();
int dataSize = (int)stream.Length;
byte[] bytes = new byte[dataSize];
stream.Seek(0, SeekOrigin.Begin);
stream.Read(bytes, 0, dataSize);
stream.Close();
IntPtr ptrData = Marshal.AllocCoTaskMem(dataSize);
Marshal.Copy(bytes, 0, ptrData, dataSize);
With the string data now in memory, referenced by our ptrData pointer in the above code, we can create our COPYDATASTRUCT instance and populate the lpData and dwData members accordingly. So, now that we've wrapped our message up as a COPYDATASTRUCT object, we're ready to send this to another Window using the SendMessage API. However, to do this we first need to know which applications should receive the data, i.e. which are listening and on the correct channel. Also, what if the application doesn't have a Window handle to send our message to?
To overcome this, we use some native Window properties and our XDListener class. When an instance of the class is invoked, it creates a hidden window on the desktop which acts as listener for all Windows messages. This is done by extending the NativeWindow class of System.Windows.Forms. By overriding the WndProc method, this allows us to filter Windows messages and look for our WM_COPYDATA message containing the message data.
The XDListener class also makes use of Window properties to create property flags indicating which channels the instance is listening on, and therefore which messages it should receive. When a message is broadcast, it enumerates all of the desktop Windows using the EnumChildWindows Win32 API. It looks for a flag (property name) on the window that represents the channel name. If found, the Windows WM_COPYDATA message is sent to that window. Once there, it will be caught and processed by the XDListener instance that owns the hidden window. To read the message data, we use the lParam of the native Windows message in order to expand the COPYDATASTRUCT instance. From this, we can locate and restore the original string message which was stored in memory earlier.
protected override void WndProc(ref Message msg)
{
base.WndProc(ref msg);
if (msg.Msg == Win32.WM_COPYDATA)
{
Win32.COPYDATASTRUCT dataStruct =
(Win32.COPYDATASTRUCT)Marshal.PtrToStructure(
msg.LParam , typeof(Win32.COPYDATASTRUCT));
byte[] bytes = new byte[this.dataStruct.cbData];
Marshal.Copy(this.dataStruct.lpData, bytes, 0,
this.dataStruct.cbData);
MemoryStream stream = new MemoryStream(bytes);
BinaryFormatter b = new BinaryFormatter();
string rawmessage = (string)b.Deserialize(stream);
}
}
Note that because the message is stored in memory whilst it's broadcast to other applications, we must remember to free that memory after the message has been sent. Each Window receiving the data will make its own copy, so the original data can be destroyed safely as soon as the messages have been sent. This is necessary because the data is stored in unmanaged memory and would otherwise result in a memory leak.
Marshal.FreeCoTaskMem(lpData);
Below are the other PInvoke methods used for setting and removing Window properties, as well as enumerating the desktop Windows.
public delegate int EnumWindowsProc(IntPtr hwnd, int lParam);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool EnumChildWindows(IntPtr hwndParent,
EnumWindowsProc lpEnumFunc, IntPtr lParam);
[DllImport("user32", CharSet = CharSet.Auto)]
public extern static int GetProp(IntPtr hwnd, string lpString);
[DllImport("user32", CharSet = CharSet.Auto)]
public extern static int SetProp(IntPtr hwnd, string lpString, int hData);
[DllImport("user32", CharSet = CharSet.Auto)]
public extern static int RemoveProp(IntPtr hwnd, string lpString);
Further Reading
History
- Feb 2007: Initial release
- 25th Feb, 2007: Added VB port to demo
- 29th May, 2007: Article edited and posted to the main CodeProject.com article base
- 2nd June, 2008: Improved threading support to avoid application hanging
| You must Sign In to use this message board. |
|
|
 |
|
 |
I really like the article, 5 stars for me. Another approach could be to use sockets of course (maybe using Datagram Socket) and create a library so that the client use the sockets transparently, without knowing it. This is a really good insight into PInvoke and using unmanaged code in C# though, and I really appreciate your approach. Keep up the good work
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
This is a really useful utility - thanks!
I thought it would be really handy for monitoring/profiling web services but I hit a snag. Although it works fine when running the web service on the VS dev webserver, it doesn't when it's run on the local IIS. There must be another boundary issue specific to the IIS process that's stopping it. Unfortunately I don't have time to investigate at the moment, so I'll have to revert to the old write-to-a-file method!
If there was a way to get this working in IIS it would be perfect!
(As a simple test, create a new web service in VS, and put something like:
TheCodeKing.Net.Messaging.XDBroadcast.SendToChannel("Status", "Hello World " & Now.Ticks)
in the HelloWorld sample method.)
|
| Sign In·View Thread·PermaLink | 5.00/5 |
|
|
|
 |
|
 |
This code works in C#:
protected override void WndProc(ref Message msg) { base.WndProc(ref msg); if (msg.Msg == Win32.WM_COPYDATA) { if (MessageReceived == null) return; DataGram dataGram = DataGram.FromPointer(msg.LParam); MessageReceived(this, new XDMessageEventArgs(dataGram)); } }
This code does not work in VB.NET:
Protected Overloads Overrides Sub WndProc(ByRef msg As Message) MyBase.WndProc(msg) If msg.Msg = im.Win32.WM_COPYDATA Then If MessageReceived Is Nothing Then Return End If Dim dataGram__1 As Sune2.Messaging.DataGram = Sune2.Messaging.DataGram.FromPointer(msg.LParam) RaiseEvent MessageReceived(Me, New MessageEventArgs(dataGram__1)) End If End Sub ERROR: 'Public Event MessageReceived(sender As Object, e As MessageEventArgs)' is an event, and cannot be called directly.
I would be very grateful if you could explain how to do this in VB.
/k
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
The last link, "Boundaries: Processes and Application Domains" is broken, it should point to: http://msdn.microsoft.com/en-us/library/kt21t9h7(VS.80).aspx
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
 |
|
 |
One simpler approach is to use AppDomain's CreateInstanceFromAndUnwrap method to create proxy to another app domain, given that you have control over the app domains.
|
| Sign In·View Thread·PermaLink | 1.00/5 |
|
|
|
 |
|
 |
Hi,
I am testing few solutions for communication between two instances of an executable and I wasn't recieveing messages send across. I debugged and found that execution is hung at SendMessage method call. I am just trying to import the XDMessaging library and use it directly for testing purposes.
Any suggestions or points as what am I doing wrong?
Thanks, Suneel
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Hi,
The SendMessage Win32 call the library uses is synchronous, so if the call doesn't return it hangs. For some reason this happens when the listener is created outside of the main thread.
There are a number of solutions depending on your specific situation, but the simplist solution is to create the listener on the main thread as in the example. You could also concider using an asynchronous Win32 call as an alternative, however I specifically use synchronous in order that I can free the memory after all the listeners have copied the data. If it's a asynchronous call then I have no way of knowing when it's safe to do so after the message is sent.
[Edited] I've updated the version of the library on my site to use SendMessageTimeout API instead, which prevents application hanging (if the SendMessage does not return after 1 second or the thread is blocked, then the process continues). I'd be interested to know if this resolves your issue. It should certainly prevent hanging, but may still mean the threaded window doesn't receive the message.
I've requested that the article is updated on codeproject, but this may take a while to get actioned. In the mean time it's available here
Mike
modified on Tuesday, June 3, 2008 5:39 PM
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Hi,
This article is great, and it might be exactly what I'm looking for. I need toi write a win service that does some quite intensive tasks, and a monitor app, that, when launched, receive messages from the service and update some status However, the issue is the service app might have to send thousands of messages in a very short period of time (will be a multi-threaded app with a thread pool). Does this library was tested for scalability? What are its performances in this situation? Can it handle reliable thousands of messages send by multiple threads?
Thank you
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Hi,
I haven't performed any scalability testing on the library myself, but Windows is built on Windows Messages so handling large volumes shouldn't be an issue. That said, if performance is a consideration then there are a few areas where you should consider re-factoring when building your solution.
1) The library iterates all of the windows on the desktop looking for a Windows property which indicates the window is acting as a listener for a 'channel'. This isn't the most efficient way of sending messages, and it would be much more efficient to send a message directly to another Window if you can more easily identify it.
2) The library copies data between processes, which requires synchronous messaging. If you don't need to send data between domains, it would be much better to use the asynchronous messaging APIs for the communication to prevent blocking threads.
3) The synchronous Win32 messaging calls used by this library can cause hanging in cross-threading scenarios within a single appdomain. This is because the SendMessage API waits for the message to be processed before it returns, and in some situations with threading this can fail. You may want to consider using a different Win32 API, however it depends of whether you intend to copy data as to whether this is workable. The issue only occurs when the listener is created on seperate thread.
Certainly I would recommend using Windows Messaging for performant cross-domain communication, but consider which Win32 API you use with care.
Good luck,
Mike
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
 |
|
 |
I was delighted to find this article in CodeProject. However, I would want this methodology to work in all environment, e.g. Console apps, Windows Services. So, I added some additional code to the XDListener to make it happen.
It works well. But for it to work with a Windows service you will need to allow the service to interact with the desktop. This can easily be done using the service's Property box.
Congratulations, a splendid piece of work.
public sealed class XDListener : NativeWindow { List channelList = new List(); uint threadId;
void MessageLoop(object state) { threadId = (uint)AppDomain.GetCurrentThreadId(); CreateParams p = new CreateParams(); p.Width = 0; p.Height = 0; p.X = 0; p.Y = 0; p.Style = (int)Win32.WS_CHILD; p.Caption = Guid.NewGuid().ToString(); p.Parent = Win32.GetDesktopWindow(); base.CreateHandle(p);
foreach (string name in this.channelList) { RegisterChannel(name); } Win32.MSG msg = new Win32.MSG(); while (Win32.GetMessage(out msg, IntPtr.Zero, 0, 0)) { Win32.DispatchMessage(ref msg); }
foreach (string name in this.channelList) { UnRegisterChannel(name); } this.channelList.Clear(); base.DestroyHandle(); } public void Start() { base.DestroyHandle(); ThreadPool.QueueUserWorkItem(this.MessageLoop); } public void Stop() { if (this.threadId > 0) { Win32.PostThreadMessage(this.threadId, Win32.WM_QUIT, UIntPtr.Zero, IntPtr.Zero); } } public void RegisterChannel(string channelName) { lock (this) { if (this.Handle != IntPtr.Zero) { Win32.SetProp(this.Handle, GetChannelKey(channelName), (int)this.Handle); } if (!this.channelList.Contains(channelName)) { this.channelList.Add(channelName); } } } public void UnRegisterChannel(string channelName) { lock (this) { if (this.Handle != IntPtr.Zero) { Win32.RemoveProp(this.Handle, GetChannelKey(channelName)); } } } }
-- modified at 0:27 Saturday 16th June, 2007
|
| Sign In·View Thread·PermaLink | 4.20/5 |
|
|
|
 |
|
 |
Thanks for posting the additional code.
> This can easily be done using the service's Property box.
That's only true if the service is run under the LocalSystem account, something Microsoft is trying to get applications developers to stop doing.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
This project is the best. I've been using it for a debugging project and it works great. Thank you very much.
|
| Sign In·View Thread·PermaLink | 1.00/5 |
|
|
|
 |
|
 |
Very good article.
I'm not sure if this is an issue, but ¿is it ok to free unmanaged allocated memory just after sending the messages in the broadcast? ¿how can you assure the listening processes have received the messages and they are not reading deallocated memory?
My Win32 API (p/invoke) knowledge is very limited so this question perhaps is a nosense
Apologize for my poor english
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Hi,
The SendMessage API is synchronous, and a copy of the memory is actually passed to the listener prior to the MessageReceived event being fired. Therefore it's safe to deallocate the original memory immediately after sending the message as it is no longer referrenced.
Hope that helps,
Mike
-- modified at 19:47 Friday 1st June, 2007 (sorry wrote asynchronous meant synchronous)
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
 |
|
 |
Great article and approach. But a warning for all, this approach only works for apps in the same process (across AppDomains). Pointers in Win32 are logical addresses in a specific process' memory space, and can not be copied across process boundaries. This even applies to a pointer to shared COM memory. In a true inter-process COM call, Windows handles marshalling of the data between the two processes memory space.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Actually this does work across different processes, that's kinda the point. Two different execuables running in different process are able to send and recieve messages.
Maybe I'm missing something, can you clarify what makes you think it doesn't work? The demo application above is inter-process.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Unless I'm missing something, the demo app above is a single executable (single process) sending and receiving messages to/from itself.
I've tried the SendMessage/PostMessage approach with 2 separate processes and it does not work. The Windows message is sent and received just fine, but the pointer in Message lParam (or the pointer in the COPYDATASTRUCT) point to bogus memory in the receiving process. At best you get trash data, at worst you get an AccessViolation.
In .NET, you can spawn multiple applications in separate AppDomains but in a the same Windows process. This message approach WILL work as advertised in that scenario. However, if you launch a sending application from the command line, then launch a separate receiving application from the command line (separate processes), it will fail. The messages will flow, but the data will not be marshalled across process boundaries. Try it and see.
I assumed from your code and approach that you had it working in a multi-AppDomain, single process scenario. I'd love to be proven wrong in this case because this message driven approach is an extremely useful technique and has a lot of advantages over more cumbersome IPC mechanisms....if it actually worked.
Good luck and prove me wrong!
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Ah yes you are missing something...
The demo application demonstrates sending messages between multiple instances of itself, hence inter-process communication (not just appDomain). Launch the executable multiple times and see for yourself, this is absolutely NOT restricted to single process scenarios.
I'm not sure why this wasn't working for you if you tried something similar in the past, however the library in this article DOES do exactly that.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Ah yes! I stand corrected. It does indeed work.
So I went back to my attempt to see what was different. I found 2 things. First, I was using a wMsg param returned by the RegisterWindowMessage API call that results in a msgid > 0x8000 (in the WM_APP range). I modified your code to use that wMsg and it stopped working. Second, I was trying to use PostMessage for asyncronous message passing. In your code, PostMessage using WM_COPYDATA will fail (the message is never received) for some reason, I think due to using the lower wMsg id range...not sure.
So the take away is that the wMsg range affects the way Windows marshalls the data. There is some documentation to that effect in MSDN under or linked from PostMessage/SendMessage. The docs state that wMsg ids below WM_USER (0x400) are reserved for system use, so I did not think to try that range. They also state that for application level IPC needs to use the RegisterWindowsMessage to obtain the wMsg id. However, if you do, custom marshalling is needed and the technique used here for marshalling data based on pointers does not work.
Again, great job. - rg
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
That's a relief!
Interesting that RegisterWindowMessage doesn't work, I've been meaning to try that but haven't had the time.
PostMessage won't work with the library without further modifications. This is because PostMessage is async and the memory is freed immediately after sending the message.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
PostMessage doesn't work regardless of the memory getting freed. The PostMessage call fails (returns false) and no message will be posted to the receiver's message loop. The only time that happens in my code is when i use WM_COPYDATA for the wMsg param.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Hi! MSDN documentation about WM_COPYDATA holds answers to several of your problems:
An application must use the SendMessage function to send this message, not the PostMessage function.
The data being passed must not contain pointers or other references to objects not accessible to the application receiving the data.
While this message is being sent, the referenced data must not be changed by another thread of the sending process.
Regards, mav
-- Black holes are the places where God divided by 0...
|
| Sign In·View Thread·PermaLink | 2.00/5 |
|
|
|
 |
|
|