Local Machine Interprocess Communication with .NET

Note: updated on 29/11/2024

Introduction

TL; DR; A description of several techniques to pass messages between processes running on the same machine.

At times, there is the need for multiple processes running on the same machine to talk to each other. This is, for example, so that they can synchronize or share some kind of data. This is generally called Interprocess Communication, or IPC.

In this post, I am going to cover several ways by which .NET code can communicate with other .NET code on the same machine. The techniques I will talk about are:

You might have noticed that I am only going to talk about Windows/.NET native stuff, no additional libraries or services. Of course, keep in mind that the code I am going to show is just proof of concept, if it was to be used in real-life applications, it would need some improvements.

This is a long post, beware!

Contracts

So, we shall have an interface that describes the client side (IIpcClient) and another that describes the server side (IIpcServer). Their definitions are:

[ServiceContract]
public interface IIpcClient
{
    [OperationContract(IsOneWay = true)]
    void Send(string data);
}
 
public interface IIpcServer : IDisposable
{
    void Start();
    void Stop();
 
    event EventHandler<DataReceivedEventArgs> Received;
}
 
[Serializable]
public sealed class DataReceivedEventArgs : EventArgs
{
    public DataReceivedEventArgs(string data)
    {
        this.Data = data;
    }
 
    public string Data { get; private set; }
}

As you can see, these are very simple, just one-way communication from client to server. In case you are wondering, the [ServiceContract] and [OperationContract] attributes are really only useful for the WCF implementation, but I left them here because they really won’t cause any harm. More on this in a minute.

IIpcClient only allows to send a text message.

IIpcServer is slightly more complex, since one can start and stop the server, as well as receive events from it. It implements IDisposable because some implementations may need to free unmanaged resources.

WCF

So, the first implementation uses WCF and the NetNamedPipeBinding binding (transport). The reasons I chose this binding were:

  • It is binary;
  • It is fast;
  • Doesn’t need to open TCP sockets;
  • Is optimized for same machine (actually, the WCF implementation only works this way, even though the named pipes protocol can be used across machines).

Here are my implementations of the client:

public class WcfClient : ClientBase<IIpcClient>, IIpcClient
{
    public WcfClient() : base(new NetNamedPipeBinding(), new EndpointAddress(string.Format("net.pipe://localhost/{0}", typeof(IIpcClient).Name)))
    {
    }
 
    public void Send(string data)
    {
        this.Channel.Send(data);
    }
}

Because my contract’s Send method is decorated with a OperationContractAttribute with the IsOneWay property set, the message is sent without the need to wait for a response message, making it slightly faster.

As for the server, here it is:

public sealed class WcfServer : IIpcServer
{
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
    private class _Server : IIpcClient
    {
        private readonly WcfServer server;
 
        public _Server(WcfServer server)
        {
            this.server = server;
        }
 
        public void Send(string data)
        {
            this.server.OnReceived(new DataReceivedEventArgs(data));
        }
    }
 
    private readonly ServiceHost host;
 
    private void OnReceived(DataReceivedEventArgs e)
    {
        var handler = this.Received;
 
        if (handler != null)
        {
            handler(this, e);
        }
    }
 
    public WcfServer()
    {
        this.host = new ServiceHost(new _Server(this), new Uri(string.Format("net.pipe://localhost/{0}", typeof(IIpcClient).Name)));
    }
 
    public event EventHandler<DataReceivedEventArgs> Received;
 
    public void Start()
    {
        this.host.Open();
    }
 
    public void Stop()
    {
        this.host.Close();
    }
 
    void IDisposable.Dispose()
    {
        this.Stop();
 
        (this.host as IDisposable).Dispose();
    }
}

Again, nothing special, only worth noticing that, for simplicity’s sake, I only allow a single instance of the server (InstanceContextMode.Single).

Sockets

Windows does not support the AF_UNIX family of sockets, only AF_INET (IPV4) or AF_INET6 (IPV6), so, for demonstrating IP network communication I could have chosen either TCP or UDP, but I went for UDP because of the better performance and because of the relative simplicity that this example required.

Here is the client part:

public class SocketClient : IIpcClient
{
    public void Send(string data)
    {
        using (var client = new UdpClient())
        {
            client.Connect(string.Empty, 9000);
 
            var bytes = Encoding.Default.GetBytes(data);
 
            client.Send(bytes, bytes.Length);
        }
    }
}

It leverages the UdpClient class to send byte-array messages, because this class hides some of the complexity of Socket, making it easier to use. As a side note, it is even possible to use WCF with a UDP transport (binding).

The matching server class:

public sealed class SocketServer : IIpcServer
{
    private readonly UdpClient server = new UdpClient(9000);
 
    void IDisposable.Dispose()
    {
        this.Stop();
 
        (this.server as IDisposable).Dispose();
    }
 
    public void Start()
    {
        Task.Factory.StartNew(() =>
        {
            var ip = new IPEndPoint(IPAddress.Any, 0);
 
            while (true)
            {
                var bytes = this.server.Receive(ref ip);
                var data = Encoding.Default.GetString(bytes);
                this.OnReceived(new DataReceivedEventArgs(data));
            }
        });
    }
 
    private void OnReceived(DataReceivedEventArgs e)
    {
        var handler = this.Received;
 
        if (handler != null)
        {
            handler(this, e);
        }
    }
 
    public void Stop()
    {
        this.server.Close();
    }
 
    public event EventHandler<DataReceivedEventArgs> Received;
}

Because the Receive method blocks until there is some contents to receive, we need to spawn a thread to avoid blocking.

.NET Remoting

.NET Remoting, in the old days, was .NET’s response to Java RMI, and basically was a remote references implementation, similar to CORBA. With Remoting, one can call methods on an object that resides in a different machine. .NET Remoting has long since been superseded by WCF, but it is still a viable alternative, particularly because WCF does not allow remote references.

My client implementation using Remoting goes like this:

public class RemotingClient : IIpcClient
{
    private static readonly IServerChannelSinkProvider serverSinkProvider = new BinaryServerFormatterSinkProvider { TypeFilterLevel = TypeFilterLevel.Full };
 
    public void Send(string data)
    {
        var properties = new Hashtable();
        properties["portName"] = Guid.NewGuid().ToString();
        properties["exclusiveAddressUse"] = false;
 
        var channel = new IpcChannel(properties, null, serverSinkProvider);
 
        try
        {
            ChannelServices.RegisterChannel(channel, true);
        }
        catch
        {
            //the channel might be already registered, so ignore it
        }
 
        var uri = string.Format("ipc://{0}/{1}.rem", typeof(IIpcClient).Name, typeof(RemoteObject).Name);
        var svc = Activator.GetObject(typeof(RemoteObject), uri) as IIpcClient;
 
        svc.Send(data);
 
        try
        {
            ChannelServices.UnregisterChannel(channel);
        }
        catch
        {
        }
}
}

The RemoteObject class needs to be shared by both the client and the server code, and it should look like this:

public class RemoteObject : MarshalByRefObject, IIpcClient
{
    private readonly IIpcClient svc;
 
    public RemoteObject()
    {
    }
 
    public RemoteObject(IIpcClient svc)
    {
        this.svc = svc;
    }
 
    public override object InitializeLifetimeService()
    {
        return null;
    }
 
    void IIpcClient.Send(string data)
    {
        if (this.svc != null)
        {
            this.svc.Send(data);
        }
    }
}

We need to pass it a reference to a IIpcClient implementation, so that we can receive messages, like a chain of responsibility. It should become clear with the code for the server:

public sealed class RemotingServer : IIpcServer
{
    private class _Server : IIpcClient
    {
        private readonly RemotingServer server;
 
        public _Server(RemotingServer server)
        {
            this.server = server;
        }
 
        public void Send(string data)
        {
            this.server.OnReceived(new DataReceivedEventArgs(data));
        }
    }
 
    private readonly ManualResetEvent killer = new ManualResetEvent(false);
 
    private static readonly IServerChannelSinkProvider serverSinkProvider = new BinaryServerFormatterSinkProvider { TypeFilterLevel = TypeFilterLevel.Full };
 
    public void Start()
    {
        Task.Factory.StartNew(() =>
        {
            var properties = new Hashtable();
            properties["portName"] = typeof(IIpcClient).Name;
            var channel = new IpcChannel(properties, null, serverSinkProvider);
 
            try
            {
                ChannelServices.RegisterChannel(channel, true);
            }
            catch
            {
                //might be already registered, ignore it
            }
 
            var remoteObject = new RemoteObject(new _Server(this));
 
            RemotingServices.Marshal(remoteObject, typeof(RemoteObject).Name + ".rem");
 
            this.killer.WaitOne();
 
            RemotingServices.Disconnect(remoteObject);
 
            try
            {
                ChannelServices.UnregisterChannel(channel);
            }
            catch
            {
            }
        });
    }
 
    public void Stop()
    {
        this.killer.Set();
    }
 
    void IDisposable.Dispose()
    {
        this.Stop();
 
        this.killer.Dispose();
    }
 
    private void OnReceived(DataReceivedEventArgs e)
    {
        var handler = this.Received;
 
        if (handler != null)
        {
            handler(this, e);
        }
    }
 
    public event EventHandler<DataReceivedEventArgs> Received;
}

Again, because we need to wait for someone to kill the server, we launch it in another thread. There’s more to it, because .NET Remoting is slightly more complex, but let’s leave it like it is.

Message Queues

Windows has included a message queues implementation for a long time, something that is often forgotten by developers. If you don’t have it installed – you can check if the Message Queuing service exists – you can install it through Programs and FeaturesTurn Windows features on and off on the Control Panel.

The client implementation should look similar to this:

public class MessageQueueClient : IIpcClient
{
    public void Send(string data)
    {
        var name = string.Format(".\\Private$\\{0}", typeof(IIpcClient).Name);
 
        var queue = null as MessageQueue;
 
        if (MessageQueue.Exists(name) == true)
        {
            queue = new MessageQueue(name);
        }
        else
        {
            queue = MessageQueue.Create(name);
        }
 
        using (queue)
        {
            queue.Send(data);
        }
    }
}

And the server:

public sealed class MessageQueueServer : IIpcServer
{
    private MessageQueue queue;
 
    void IDisposable.Dispose()
    {
        this.Stop();
 
        this.queue.Dispose();
    }
 
    public void Start()
    {
        Task.Factory.StartNew(() =>
        {
            var name = string.Format(".\\Private$\\{0}", typeof (IIpcClient).Name);
 
            if (MessageQueue.Exists(name) == true)
            {
                queue = new MessageQueue(name);
            }
            else
            {
                queue = MessageQueue.Create(name);
            }
 
            queue.Formatter = new XmlMessageFormatter(new Type[] { typeof(string) });
 
            while (true)
            {
                var msg = queue.Receive();
                var data = msg.Body.ToString();
                this.OnReceived(new DataReceivedEventArgs(data));
            }
        });
    }
 
    private void OnReceived(DataReceivedEventArgs e)
    {
        var handler = this.Received;
 
        if (handler != null)
        {
            handler(this, e);
        }
    }
 
    public void Stop()
    {
        this.queue.Close();
    }
 
    public event EventHandler<DataReceivedEventArgs> Received;
}

Once again, because the Receive method is blocking, we have to launch another thread and use a ManualResetEvent to terminate it. I am using a private queue without transactional support to make things simpler.

Named Pipes

Named pipes in Windows is a duplex means of sending data between Windows hosts. We used it in the WCF implementation, shown earlier, but .NET has its own built-in support for named pipes communication.

The client-side code is shown below:

public class NamedPipeClient : IIpcClient
{
    public void Send(string data)
    {
        using (var client = new NamedPipeClientStream(".", typeof(IIpcClient).Name, PipeDirection.Out))
        {
            client.Connect();
 
            using (var writer = new StreamWriter(client))
            {
                writer.WriteLine(data);
            }
        }
    }
}

And the server one:

public sealed class NamedPipeServer : IIpcAsyncServer
{
    private readonly NamedPipeServerStream server = new NamedPipeServerStream(typeof (IIpcClient).Name, PipeDirection.In);
 
    private void OnReceived(DataReceivedEventArgs e)
    {
        var handler = this.Received;
 
        if (handler != null)
        {
            handler(this, e);
        }
    }
 
    public event EventHandler<DataReceivedEventArgs> Received;
 
    public void Start()
    {
        Task.Factory.StartNew(() =>
        {
            while (true)
            {
                this.server.WaitForConnection();
 
                using (var reader = new StreamReader(this.server))
                {
                    this.OnReceived(new DataReceivedEventArgs(reader.ReadToEnd()));
                }
            }
        });
    }
 
    public void Stop()
    {
        this.server.Disconnect();
    }
 
    void IDisposable.Dispose()
    {
        this.Stop();
 
        this.server.Dispose();
    }
}

Again, we see the same pattern of spawning a thread pool thread to handle the requests, but this time we terminate it by calling Disconnect.

Memory-Mapped Files

Memory-mapped files in Windows allow us to either map a “window” of a large file on the filesystem, or to create a named memory area that can be shared among processes. In this sample, I am going to create a shared area on the fly and use named AutoResetEvents to control access to it.

As usual, the client code first:

public class SharedMemoryClient : IIpcClient
{
    public void Send(string data)
    {
        var evt = null as EventWaitHandle;
 
        if (EventWaitHandle.TryOpenExisting(typeof (IIpcClient).Name, out evt) == false)
        {
            evt = new EventWaitHandle(false, EventResetMode.AutoReset, typeof(IIpcClient).Name);
        }
 
        using (evt)
        using (var file = MemoryMappedFile.CreateOrOpen(typeof(IIpcClient).Name + "File", 1024))
        using (var view = file.CreateViewAccessor())
        {
            var bytes = Encoding.Default.GetBytes(data);
 
            view.WriteArray(0, bytes, 0, bytes.Length);
 
            evt.Set();
        }
    }
}

Notice the way to create named AutoResetEvents is slightly tricky, because we need to create an instance of EventWaitHandle instead. Upon creation, the MemoryMappedFile instance must be passed a capacity in bytes, I am just passing 1024. Upon sending the message, I signal the shared AutoResetEvent which allows interested parties – the server - to proceed.

And here is the server-side code:

public sealed class SharedMemoryServer : IIpcServer
{
    private readonly ManualResetEvent killer = new ManualResetEvent(false);
    private const int capacity = 1024;
 
    private void OnReceived(DataReceivedEventArgs e)
    {
        var handler = this.Received;
 
        if (handler != null)
        {
            handler(this, e);
        }
    }
 
    public event EventHandler<DataReceivedEventArgs> Received;
 
    public void Start()
    {
        Task.Factory.StartNew(() =>
        {
            var evt = null as EventWaitHandle;
 
            if (EventWaitHandle.TryOpenExisting(typeof(IIpcClient).Name, out evt) == false)
            {
                evt = new EventWaitHandle(false, EventResetMode.AutoReset, typeof(IIpcClient).Name);
            }
 
            using (evt)
            using (var file = MemoryMappedFile.CreateOrOpen(typeof(IIpcClient).Name + "File", capacity))
            using (var view = file.CreateViewAccessor())
            {
                var data = new byte[capacity];
 
                while (WaitHandle.WaitAny(new WaitHandle[] { this.killer, evt }) == 1)
                {
                    view.ReadArray(0, data, 0, data.Length);
 
                    this.OnReceived(new DataReceivedEventArgs(Encoding.Default.GetString(data)));
                }
            }
        });
    }
 
    public void Stop()
    {
        this.killer.Set();
    }
 
    void IDisposable.Dispose()
    {
        this.Stop();
 
        this.killer.Dispose();
    }
}

You can see it follows the same pattern as before, for blocking server implementations.

Event Tracing for Windows

The ETW implementation either requires that you use .NET 4.6 or that you install the Microsoft Event Source Library from NuGet. This is because of API differences in the EventSource and related classes. Of course, ETW is far more useful than just sending text messages between endpoints, but, hey, it can also do that, so here it goes.

First, the server implementation:

public sealed class EtwServer : EventListener, IIpcServer
{
    protected override void OnEventWritten(EventWrittenEventArgs eventData)
    {
        if (eventData.EventName == typeof (IIpcClient).Name)
        {
            var data = eventData.Payload[0];
            this.OnReceived(new DataReceivedEventArgs(data.ToString()));
        }
    }
 
    public void Start()
    {
        this.EnableEvents(EtwClient.Instance, EventLevel.LogAlways);
    }
 
    public void Stop()
    {
        this.DisableEvents(EtwClient.Instance);
    }
 
    private void OnReceived(DataReceivedEventArgs e)
    {
        var handler = this.Received;
 
        if (handler != null)
        {
            handler(this, e);
        }
    }
 
    public event EventHandler<DataReceivedEventArgs> Received;
}

Pretty straightforward, I’d say, it merely listens to messages from a certain event name.

Now, the client part:

public class EtwClient : EventSource, IIpcClient
{
    public static readonly EtwClient Instance = new EtwClient();
 
    private EtwClient(): base(typeof(IIpcClient).Name)
    {
    }
 
    public void Send(string data)
    {
        this.Write(typeof(IIpcClient).Name, new { Data = data });
    }
}

You can see that the client is implemented as a singleton, this makes

things slightly easier.

Why would ETW for this, you ask? Well, for once, it has a very good performance, and you can use it for more complex scenarios.

Files

I hesitated before including this one, but anyway here it goes. Basically, the server and client classes will be trying to acquire the exclusive lock on a file. The server will check first if the file is not empty, otherwise, it will just loop. Unfortunately, there is no easy way to see if a file is locked, this is a common problem.

Here is the client code:

public class FileClient : IIpcClient
{
    private const string filename = "Filename.txt";
    private const int delay = 100;
 
    public void Send(string data)
    {
        while (true)
        {
            try
            {
                var file = System.IO.File.Open(filename, FileMode.Create, FileAccess.Write, FileShare.None);
 
                using (var writer = new StreamWriter(file))
                {
                    writer.Write(data);
                    writer.Flush();
                }
 
                file.Close();
 
                break;
            }
            catch (IOException)
            {
                Thread.Sleep(delay);
            }
        }
    }
}

You can see that if access to the file fails, it will sleep for some time, and will try again an infinite number of times.

Here is the matching server code:

public sealed class FileServer : IIpcServer
{
    private const string filename = "Filename.txt";
    private const int delay = 100;
    private readonly ManualResetEvent killer = new ManualResetEvent(false);
 
    void IDisposable.Dispose()
    {
        this.Stop();
 
        this.killer.Dispose();
    }
 
    public void Start()
    {
        Task.Factory.StartNew(() =>
        {
            while (this.killer.WaitOne(0) == false)
            {
                try
                {
                    if (new FileInfo(filename).Length > 0)
                    {                        
                        var file = System.IO.File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.None);
 
                        using (var reader = new StreamReader(file))
                        {
                            var data = reader.ReadToEnd();
 
                            this.OnReceive(new DataReceivedEventArgs(data));
 
                            file.Close();
                        }
                    }
                }
                catch (IOException)
                {
                    Thread.Sleep(delay);
                }
            }
        });
    }
 
    public void Stop()
    {
        this.killer.Set();
    }
 
    private void OnReceive(DataReceivedEventArgs e)
    {
        var handler = this.Received;
 
        if (handler != null)
        {
            handler(this, e);
        }
    }
 
    public event EventHandler<DataReceivedEventArgs> Received;
}

Same pattern of spawning a worker thread and using an event to kill it.

COM Interop

COM was introduced in Windows decades ago, and it largely depends on it. It is also the basis for automation and other interesting stuff. It is a standard for interoperability based on language-agnostic interface definitions. COM implementations can be written in a number of languages, from Visual Basic to C and C++, and, of course, C# and .NET.

COM has the concept of a class factory, which is used to build the actual COM interface implementations. We can use our own implementation to always return the same instance – a singleton. For this example, the first instantiation of the COM component will create an instance in memory and the next ones will always access it. Calls to its methods will be serialized and data transferred seamlessly between processes. Now, COM Interop is a complex topic, and I’m only going to scratch the surface of it. This one needs more work than the previous ones.

Without more delay, here is the common code to be shared between the client and the server:

static internal class COMHelper
{
    public static void RegasmRegisterLocalServer(Type type)
    {
        using (var keyCLSID = Registry.ClassesRoot.OpenSubKey(@"CLSID\" + type.GUID.ToString("B"), true))
        {
            keyCLSID.DeleteSubKeyTree("InprocServer32");
 
            using (var subkey = keyCLSID.CreateSubKey("LocalServer32"))
            {
                subkey.SetValue(string.Empty, typeof(IIpcClientServer).Assembly.Location, RegistryValueKind.String);
            }
        }
    }
 
    public static void RegasmUnregisterLocalServer(Type type)
    {
        Registry.ClassesRoot.DeleteSubKeyTree(@"CLSID\" + type.GUID.ToString("B"));
    }
}
 
static internal class COMNative
{
    [DllImport("ole32.dll")]
    public static extern int CoRegisterClassObject(ref Guid rclsid, [MarshalAs(UnmanagedType.Interface)] IClassFactory pUnk, CLSCTX dwClsContext, REGCLS flags, out uint lpdwRegister);
 
    [DllImport("ole32.dll")]
    public static extern uint CoRevokeClassObject(uint dwRegister);
 
    [DllImport("ole32.dll")]
    public static extern int CoResumeClassObjects();
 
    public const string IID_IClassFactory = "00000001-0000-0000-C000-000000000046";
    public const string IID_IUnknown = "00000000-0000-0000-C000-000000000046";
    public const string IID_IDispatch = "00020400-0000-0000-C000-000000000046";
 
    public const int CLASS_E_NOAGGREGATION = unchecked((int)0x80040110);
    public const int E_NOINTERFACE = unchecked((int)0x80004002);
 
    public const int S_OK = 0;
}
 
[ComImport]
[ComVisible(false)]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid(COMNative.IID_IClassFactory)]
internal interface IClassFactory
{
    [PreserveSig]
    int CreateInstance(IntPtr pUnkOuter, ref Guid riid, out IntPtr ppvObject);
 
    [PreserveSig]
    int LockServer(bool fLock);
}
 
[Flags]
internal enum CLSCTX : uint
{
    INPROC_SERVER = 0x1,
    INPROC_HANDLER = 0x2,
    LOCAL_SERVER = 0x4,
    INPROC_SERVER16 = 0x8,
    REMOTE_SERVER = 0x10,
    INPROC_HANDLER16 = 0x20,
    RESERVED1 = 0x40,
    RESERVED2 = 0x80,
    RESERVED3 = 0x100,
    RESERVED4 = 0x200,
    NO_CODE_DOWNLOAD = 0x400,
    RESERVED5 = 0x800,
    NO_CUSTOM_MARSHAL = 0x1000,
    ENABLE_CODE_DOWNLOAD = 0x2000,
    NO_FAILURE_LOG = 0x4000,
    DISABLE_AAA = 0x8000,
    ENABLE_AAA = 0x10000,
    FROM_DEFAULT_CONTEXT = 0x20000,
    ACTIVATE_32_BIT_SERVER = 0x40000,
    ACTIVATE_64_BIT_SERVER = 0x80000,
    CLSCTX_ENABLE_CLOAKING = 0x100000,
    CLSCTX_APPCONTAINER = 0x400000,
    CLSCTX_ACTIVATE_AAA_AS_IU = 0x800000,
    CLSCTX_PS_DLL = 0x80000000
}
 
[Flags]
internal enum REGCLS : uint
{
    SINGLEUSE = 0,
    MULTIPLEUSE = 1,
    MULTI_SEPARATE = 2,
    SUSPENDED = 4,
    SURROGATE = 8,
}
 
[ComVisible(true)]
internal sealed class IpcClientServerClassFactory : IClassFactory
{
    internal static IpcClientServer instance;
 
    private readonly ComServer server;
 
    public IpcClientServerClassFactory(ComServer server)
    {
        this.server = server;
    }
 
    public int CreateInstance(IntPtr pUnkOuter, ref Guid riid, out IntPtr ppvObject)
    {
        ppvObject = IntPtr.Zero;
 
        if (pUnkOuter != IntPtr.Zero)
        {
            Marshal.ThrowExceptionForHR(COMNative.CLASS_E_NOAGGREGATION);
        }
 
        if ((riid == new Guid(IpcClientServer.ClassId)) ||
            (riid == new Guid(COMNative.IID_IDispatch)) ||
            (riid == new Guid(COMNative.IID_IUnknown)))
        {
            if (instance == null)
            {
                instance = new IpcClientServer();
                instance.Received += (s, e) =>
                {
                    this.server.OnReceived(e);
                };
            }
 
            ppvObject = Marshal.GetComInterfaceForObject(instance, typeof(IIpcClientServer));
        }
        else
        {
            Marshal.ThrowExceptionForHR(COMNative.E_NOINTERFACE);
        }
 
        return COMNative.S_OK;
    }
 
    public int LockServer(bool fLock)
    {
        return COMNative.S_OK;
    }
}
 
[ComImport]
[ComVisible(true)]
[CoClass(typeof(IpcClientServer))]
[Guid(IpcClientServer.InterfaceId)]
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface IIpcClientServer
{
    [DispId(1)]
    void Send(string data);
    [DispId(2)]
    event EventHandler<DataReceivedEventArgs> Received;
}
 
[ComVisible(true)]
[ProgId(IpcClientServer.ProgId)]
[ClassInterface(ClassInterfaceType.None)]
[Guid(IpcClientServer.ClassId)]
public sealed class IpcClientServer : IIpcClientServer
{
    public const string ProgId = "IpcTest.IpcClientServer";
    public const string ClassId = "13FE32AD-4BF8-495f-AB4D-6C61BD463EA4";
    public const string InterfaceId = "D6F88E95-8A27-4ae6-B6DE-0542A0FC7039";
 
    [ComRegisterFunction]
    public static void Register(Type type)
    {
        COMHelper.RegasmRegisterLocalServer(type);
    }
 
    [ComUnregisterFunction]
    public static void Unregister(Type type)
    {
        COMHelper.RegasmUnregisterLocalServer(type);
    }
 
    public void Send(string data)
    {
        var handler = this.Received;
 
        if (handler != null)
        {
            handler(this, new DataReceivedEventArgs(data));
        }
    }
 
    public event EventHandler<DataReceivedEventArgs> Received;
 
    public static void Unregister(uint cookie)
    {
        if (cookie != 0)
        {
            COMNative.CoRevokeClassObject(cookie);
        }
    }
 
    public static uint Register(ComServer server)
    {
        var clsid = new Guid(IpcClientServer.ClassId);
        var result = (uint)COMNative.S_OK;
        var hResult = COMNative.CoRegisterClassObject(ref clsid, new IpcClientServerClassFactory(server), CLSCTX.LOCAL_SERVER, REGCLS.MULTIPLEUSE | REGCLS.SUSPENDED, out result);
 
        if (hResult != COMNative.S_OK)
        {
            throw new ApplicationException("CoRegisterClassObject failed w/err 0x" + hResult.ToString("X"));
        }
 
        hResult = COMNative.CoResumeClassObjects();
 
        if (hResult != COMNative.S_OK)
        {
            if (result != COMNative.S_OK)
            {
                COMNative.CoRevokeClassObject(result);
            }
 
            throw new ApplicationException("CoResumeClassObjects failed w/err 0x" + hResult.ToString("X"));
        }
 
        return result;
    }
}

As I said, this is more complex. What we have here is:

  • COMHelper: a static class containing some helper methods for registering the COM component;
  • COMNative: also a static class with native functions definitions, structures and constants;
  • IClassFactory: a .NET representation of the COM IClassFactory interface;
  • CLSCTX: an enumeration that mimics the identically-named CLSCTX COM enumeration;
  • REGCLS: likewise for REGCLS;
  • IpcClientServerClassFactory: our implementation of IClassFactory;
  • IIpcClientServer: COM interface definition for our component, which contains both the client and the server interfaces;
  • IpcClientServer: the actual implementation.

The client code is quite simple, actually:

public class ComClient : IIpcClient
{
    public void Send(string data)
    {
        var proxy = Activator.CreateInstance(Type.GetTypeFromProgID(IpcClientServer.ProgId)) as IIpcClientServer;
 
        proxy.Send(data);
 
        proxy = null;
    }
}

Only worthy of mention is how we create instances to COM objects through the CreateInstance method of the Activator class, passing it a ProgId, which is basically a string moniker for the CLSID, which is the COM component unique identifier.

And the server part is only slightly more complex:

public sealed class ComServer : IIpcServer
{
    private uint cookie;
 
    void IDisposable.Dispose()
    {
        this.Stop();
    }
 
    public void Start()
    {
        this.cookie = IpcClientServer.Register(this);
    }
 
    public void Stop()
    {
        IpcClientServer.Unregister(this.cookie);
    }
 
    internal void OnReceived(DataReceivedEventArgs e)
    {
        var handler = this.Received;
 
        if (handler != null)
        {
            handler(this, e);
        }
    }
 
    public event EventHandler<DataReceivedEventArgs> Received;
}

This one does not have to wait, because all it’s doing is registering the COM instance, its class factory, etc. The instance will be created when it is first instantiated by some code.

Now, two extra things that you need to do:

  • Compile the COM code as X86, not Any CPU:

image

  • Add the following post-build event command line upon successful build: C:\Windows\Microsoft.NET\Framework\v4.0.30319\regasm.exe "$(TargetPath)":

image

And that’s it. You can even test sending messages to it from a JavaScript file, using Windows Script Host:

var ipc = WScript.CreateObject('IpcTest.IpcClientServer');
ipc.Send('Hello, World!');
ipc = null;

Just save this code in a .JS file and call it using the CSCRIPT.EXE utility.

WM_COPYDATA

WM_COPYDATA probably doesn’t say much to .NET developers, but for old-school Win32 C/C++ developers it certainly does! Basically, it was a way by which one could send arbitrary data, including structured data, between processes (actually, strictly speaking, windows). One would send a WM_COPYDATA message to a window handle, running on any process, and Windows would take care of marshalling the data so that it is available outside the address space of the sending process. It is even possible to send it to all processes, using HWND_BROADCAST, but that probably wouldn’t be wise, because different applications might have different interpretations of it. Also, it needs to be passed with SendMessage, PostMessage won’t work.

Again, first we need to have some shared definitions:

[StructLayout(LayoutKind.Sequential)]
public struct COPYDATASTRUCT
{
    public IntPtr dwData;
    public IntPtr cbData;
    public IntPtr lpData;
}

COPYDATASTRUCT is a Win32 data structure used for sending WM_COPYDATA messages and with this we are bringing it to the .NET world.

Now, the client:

public class CopyDataClient : IIpcClient
{
    [DllImport("user32.dll")]
    private static extern int SendMessage(IntPtr hWnd, int uMsg, IntPtr wparam, IntPtr lparam);
 
    [DllImport("user32.dll")]
    private static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
 
    private const int HWND_BROADCAST = 0xffff;
 
    private const int WM_COPY_DATA = 0x004A;
 
    public void Send(string data)
    {
        var cds = new COPYDATASTRUCT();
        cds.dwData = (IntPtr) Marshal.SizeOf(cds);
        cds.cbData = (IntPtr) data.Length;
        cds.lpData = Marshal.StringToHGlobalAnsi(data);
 
        var ptr = Marshal.AllocCoTaskMem(Marshal.SizeOf(cds));
 
        Marshal.StructureToPtr(cds, ptr, true);
 
        var target = FindWindow(null, typeof(IIpcClient).Name);  //(IntPtr)HWND_BROADCAST;
 
        var result = SendMessage(target, WM_COPY_DATA, IntPtr.Zero, ptr);
 
        Marshal.FreeHGlobal(cds.lpData);
        Marshal.FreeCoTaskMem(ptr);
    }
}

The extra complexity comes from the need to allocate and marshal data using the Win32 API. FindWindow is used to find the handle to the window we’re interested in from its title – see the server code to better understand this – but we could instead use HWND_BROADCAST to send the message to all windows on the system.

And finally, here is the server code:

public class CopyDataServer : IIpcServer
{
    private NativeWindow messageHandler;
 
    [DllImport("user32.dll")]
    private static extern int SendMessage(IntPtr hWnd, int uMsg, IntPtr wparam, IntPtr lparam);
 
    private const int WM_COPY_DATA = 0x004A;
    private const int WM_QUIT = 0x0012;
 
    private sealed class MessageHandler : NativeWindow
    {
        private readonly CopyDataServer server;
 
        public MessageHandler(CopyDataServer server)
        {
            this.server = server;
            this.CreateHandle(new CreateParams() { Caption = typeof(IIpcClient).Name });
        }
 
        protected override void WndProc(ref Message msg)
        {
            if (msg.Msg == WM_COPY_DATA)
            {
                var cds = (COPYDATASTRUCT) Marshal.PtrToStructure(msg.LParam, typeof(COPYDATASTRUCT));
 
                if (cds.cbData.ToInt32() > 0)
                {
                    var bytes = new byte[cds.cbData.ToInt32()];
 
                    Marshal.Copy(cds.lpData, bytes, 0, cds.cbData.ToInt32());
 
                    var chars = Encoding.ASCII.GetChars(bytes);
                    var data = new string(chars);
 
                    this.server.OnReceived(new DataReceivedEventArgs(data));
                }
 
                msg.Result = (IntPtr) 1;
            }
 
            base.WndProc(ref msg);
        }
    }
 
    private void OnReceived(DataReceivedEventArgs e)
    {
        var handler = this.Received;
 
        if (handler != null)
        {
            handler(this, e);
        }
    }
 
    void IDisposable.Dispose()
    {
        this.Stop();
    }
 
    public void Start()
    {
        Task.Factory.StartNew(() =>
        {
            this.messageHandler = new MessageHandler(this);
 
            Application.Run();
        });
    }
 
    public void Stop()
    {
        SendMessage(this.messageHandler.Handle, WM_QUIT, IntPtr.Zero, IntPtr.Zero);
    }
 
    public event EventHandler<DataReceivedEventArgs> Received;
}

OK, so this is complex. We need to instantiate a NativeWindow, so that we can receive windows messages into it. Don’t worry, it isn’t visible. When we has for its handle to be created (CreateHandle), we say that it should have a certain title (the Caption property). The message pump (WndProc method) continuously listens for messages until it receives the termination message (WM_QUIT), which we send when the server is terminated. As usual, the server runs on its own thread, to avoid blocking.

Conclusion

The code demonstrated could be of course be improved so as to make it more robust and allow for more scenarios, like having different named endpoints instead of a single hardcoded one. It does work, however! Winking smile

If you were to ask me, I think that WCF would be the way to go. It allows for structured messaging easily, and it is a breeze to switch bindings and settings. The COM implementation has its interest in the sense that we are sharing an instance between processes, so there is only a very slight delay that has to do with the marshaling of the data to send.

Now, I would really, really like to hear from you on this! Anything I missed? Looking forward for your feedback!

                             

12 Comments

  • Fantastic read. I know you said you were purposely excluding frameworks, but I think a little SignalR sample would be a tasty little tidbit (maybe even a more direct example of using a WebSocket beyond what's buried inside SignalR with its other fallback options). Great work as always.

  • Thanks, Brian!
    Maybe, maybe... ;-)

  • The old WM_COPYDATA method was exactly what I needed to implement a feature on my current project. Totally forgot about that one. Thanks!

  • Mike: glad to be helpful! Keep dropping by!

  • I second Brian's comment on SignalR... that would be an awesome and very complete page that I'll hang onto in favorites for a long time.
    Thanks for the excellent info!

  • I recognize this is a couple years out, but I was intrigued by your ETW implementation for IPC. I crafted a solution around your sample, but was unsuccessful at getting it going. From everything I have read since, EventListener cannot be used cross-process. It does work fine within the same process. Have you had success getting IPC to work with ETW?

  • Hi, SFarrow! Thanks. I did test it when I wrote this post, but it can be that things have changed.
    You can have a look at my repo here: https://github.com/rjperes/IpcTest.
    A word of caution: this hasn't been touched in quite some time!

  • Brillant article!! Thanks!!

  • What an elegant and complete summary, congratulations.
    Just building a portable IPC solution with .NET CORE which is at least "to some extent" safe without the overhead of encrypting the whole communication. Very helpful work, thank you.

  • Thanks!

  • What is needed to make a duplex communication with named pipes?
    Thank you for your help.

  • @Yana: what do you need? Use PipeDirection.InOut.

Add a Comment

As it will appear on the website

Not displayed

Your website