Tidbits

Sharing COM objects between processes

A good 70% of this is taken more or less directly from the .NET Framework source code for the class IWbemClassObjectFreeThreaded and backed by an article from Raymond Chen.

With this code you can create a byte array based on a COM object with which you can create a reference to that COM object from a different process.


public static class ComMarshalHelper
{
    #region Externals
 
    [ResourceExposure(ResourceScope.None), DllImport("ole32.dll", PreserveSig = false)]
    private static extern void CoMarshalInterface([In] IStream pStm, [In] ref Guid riid, [In] IntPtr Unk, [In] uint dwDestContext, [In] IntPtr pvDestContext, [In] uint mshlflags);
 
    [ResourceExposure(ResourceScope.None), DllImport("ole32.dll", PreserveSig = false)]
    private static extern IntPtr CoUnmarshalInterface([In] IStream pStm, [In] ref Guid riid);
 
    [ResourceExposure(ResourceScope.None), DllImport("ole32.dll", PreserveSig = false)]
    private static extern IStream CreateStreamOnHGlobal(IntPtr hGlobalint fDeleteOnRelease);
 
    [ResourceExposure(ResourceScope.None), DllImport("ole32.dll", PreserveSig = false)]
    private static extern IntPtr GetHGlobalFromStream([In] IStream pstm);
 
    [ResourceExposure(ResourceScope.None), DllImport("kernel32.dll", PreserveSig = true)]
    private static extern IntPtr GlobalLock([In] IntPtr hGlobal);
 
    [ResourceExposure(ResourceScope.None), DllImport("kernel32.dll", PreserveSig = true)]
    private static extern int GlobalUnlock([In] IntPtr pData);
 
    private enum MSHCTX
    {
        MSHCTX_LOCAL = 0,
        MSHCTX_NOSHAREDMEM = 1,
        MSHCTX_DIFFERENTMACHINE = 2,
        MSHCTX_INPROC = 3
    }
 
    private enum MSHLFLAGS
    {
        MSHLFLAGS_NORMAL = 0,
        MSHLFLAGS_TABLESTRONG = 1,
        MSHLFLAGS_TABLEWEAK = 2,
        MSHLFLAGS_NOPING = 3
    }
 
    #endregion //Externals
 
    private static readonly Type CoClassAttributeType = typeof(CoClassAttribute);
 
    public static byte[] MarshalComObject<T>(T comObject)
    {
        Type type = typeof(T);
        Guid objectId = type.GUID;
        if (!type.IsInterface)
        {
            foreach (Type comInterface in type.GetInterfaces())
            {
                if (!comInterface.IsDefined(CoClassAttributeType))
                    continue;
 
                objectId = comInterface.GUID;
                break;
            }
        }
 
        IntPtr iUnknown = IntPtr.Zero;
        IStream? stream = null;
        IntPtr lockedStreamPointer = IntPtr.Zero;
        try
        {
            iUnknown = Marshal.GetIUnknownForObject(comObject);
            stream = CreateStreamOnHGlobal(IntPtr.Zero, 1);
            CoMarshalInterface(stream, ref objectId, iUnknown, (uint)MSHCTX.MSHCTX_LOCAL, IntPtr.Zero, (uint)MSHLFLAGS.MSHLFLAGS_NORMAL);
            stream.Stat(out STATSTG streamInfo, 0);
            byte[] array = new byte[streamInfo.cbSize];
            lockedStreamPointer = GlobalLock(GetHGlobalFromStream(stream));
            Marshal.Copy(lockedStreamPointer, array, 0, array.Length);
 
            return array;
        }
        finally
        {
            if (iUnknown != IntPtr.Zero)
                Marshal.Release(iUnknown);
 
            if (lockedStreamPointer != IntPtr.Zero)
                GlobalUnlock(lockedStreamPointer);
 
            if (stream is not null)
                Marshal.ReleaseComObject(stream);
        }
    }
 
    public static T UnmarshalComObject<T>(byte[] data)
    {
        Type type = typeof(T);
        Guid objectId = type.GUID;
        if (!type.IsInterface)
        {
            foreach (Type comInterface in type.GetInterfaces())
            {
                if (!comInterface.IsDefined(CoClassAttributeType))
                    continue;
 
                objectId = comInterface.GUID;
                break;
            }
        }
 
        IntPtr streamPointer = IntPtr.Zero;
        IStream? stream = null;
        try
        {
            streamPointer = Marshal.AllocHGlobal(data.Length);
            Marshal.Copy(data, 0, streamPointer, data.Length);
            stream = CreateStreamOnHGlobal(streamPointer, 0);
            IntPtr iUnknown = CoUnmarshalInterface(stream, ref objectId);
 
            return (T)Marshal.GetObjectForIUnknown(iUnknown);
        }
        finally
        {
            if (stream is not null)
                Marshal.ReleaseComObject(stream);
 
            if (streamPointer != IntPtr.Zero)
                Marshal.FreeHGlobal(streamPointer);
        }
    }
}

What’s the deal with the GetInterfaces?
We need the GUID of the interface, not the CoClass. This code simply tries to backtrack from the CoClass to the interface.

Why make MarshalComObject generic?
GetType won’t always give you what you expect. Often it will just be System.__ComObject, even if the variable you passed in was the actual type. The generic is simply to ensure we know which type the object belongs to for determining the correct GUID.


Here’s an example using the Microsoft XML COM library (v6.0):

Process A creates a document and fills it with data before it is marshaled.

DOMDocument60 document = new();
document.loadXML("<Root>Some text to fill out this string</Root>");
byte[] data = ComMarshalHelper.MarshalComObject(document);

Sharing data can happen any way you like. Since it isn’t very big, one method might be to just convert it into Base64 and pass it on as CLI argument.

Process B can now unmarshal it and output the XML content.

DOMDocument60 document = ComMarshalHelper.UnmarshalComObject<DOMDocument60>(data);
Console.WriteLine(document.xml);//Outputs '<Root>Some text to fill out this string</Root>'