How to Instantiate a ComVisible Class into Its Own AppDomain in a Single-Threaded Client?

804 Views Asked by At

The Problem

When instantiating two, independent .NET COM-visible classes within the same, single-threaded COM client, .NET loads them both into the same AppDomain.

I am guessing that this is because they are being loaded into the same thread/process.

An example of this behavior is shown in this GitHub repository.

Essentially, the demonstration is as follows:

  1. Instantiate one COM class
  2. Set an attribute on the first COM object which, in the back-end calls SetData on the CurrentDomain.
  3. Instantiate a second, independent COM class (different interface name, GUIDs, etc)
  4. Read the AppDomain attribute
  5. Demonstrate that it appears the same
  6. Also, get the hash code from both AppDomains, noting that it is also the same

Why is this a problem?

When both classes have the AppDomain.CurrentDomain.AssemblyResolve event implemented (or any other AppDomain event, for that matter), the events can interfere with one another. This is at least one complication; I am guessing that there may be others as well.

An Idea

I thought the best way of handling this would be to create a new AppDomain for each COM object. Because I could not find (or Google) a way of doing this in a managed way, I thought it might be necessary to do it in unmanaged code.

I did a little detective work. In OleView, the InprocServer32 attribute for a .NET COM-visible class is mscoree.dll. So, I created a "shim" DLL which forwarded all of its EXPORTS to mscoree.dll. By process of elimination (eliminating exports until the COM would no longer load), I discovered that DllGetClassObject in mscoree was responsible for starting up the .NET runtime, and returning the instantiated COM object.

So, what I can do is implement my own DllGetClassObject, like so:

  1. Host the .NET runtime in an unmanaged assembly using CLRCreateInstance
  2. Create the object in a new AppDomain, and return it

(I'm guessing it's not as simple as it sounds, though)

The Question

Before I embark on this potentially difficult and lengthy process, I'd like to know:

  1. Is there a managed way of getting a .NET COM-visible class to run in its own AppDomain?
  2. If not, is this the "right" way of doing it, or am I missing an obvious solution?
2

There are 2 best solutions below

3
On BEST ANSWER

If the code does not have to run in the same process, an out-of-process server would be the easiest fix. Pass CLSCTX_LOCAL_SERVER to CoCreateInstance and each class will be created in a dllhost hosting process.

For example on the client:

public static object CreateLocalServer(Guid clsid)
{
    return CoCreateInstance(clsid, null, CLSCTX.LOCAL_SERVER, IID_IUnknown);
}

public static object CreateLocalServer(string progid)
{
    Contract.Requires(!string.IsNullOrEmpty(progid));

    Guid clsid;
    CLSIDFromProgID(progid, out clsid);
    return CreateLocalServer(clsid);
}

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
}

[DllImport(Ole32, ExactSpelling = true, PreserveSig = false)]
[return: MarshalAs(UnmanagedType.Interface)]
public static extern object CoCreateInstance(
   [In, MarshalAs(UnmanagedType.LPStruct)] Guid rclsid,
   [MarshalAs(UnmanagedType.IUnknown)] object pUnkOuter,
   CLSCTX dwClsContext,
   [In, MarshalAs(UnmanagedType.LPStruct)] Guid riid);

[DllImport(Ole32, CharSet = CharSet.Unicode, PreserveSig = false)]
public static extern void CLSIDFromProgID(string progId, out Guid rclsid);

You can also register a custom host, and swap the standard InProcServer32 for LocalServer32. For an example server

// StandardOleMarshalObject keeps us single-threaded on the UI thread
// https://msdn.microsoft.com/en-us/library/74169f59(v=vs.110).aspx
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[ProgId(IpcConstants.CoordinatorProgID)]
public sealed class Coordinator : StandardOleMarshalObject, ICoordinator
{
    public Coordinator()
    {
        // required for regasm
    }

    #region Registration

    [ComRegisterFunction]
    internal static void RegasmRegisterLocalServer(string path)
    {
        // path is HKEY_CLASSES_ROOT\\CLSID\\{clsid}", we only want CLSID...
        path = path.Substring("HKEY_CLASSES_ROOT\\".Length);
        using (RegistryKey keyCLSID = Registry.ClassesRoot.OpenSubKey(path, writable: true))
        {
            // Remove the auto-generated InprocServer32 key after registration
            // (REGASM puts it there but we are going out-of-proc).
            keyCLSID.DeleteSubKeyTree("InprocServer32");

            // Create "LocalServer32" under the CLSID key
            using (RegistryKey subkey = keyCLSID.CreateSubKey("LocalServer32"))
            {
                subkey.SetValue("", Assembly.GetExecutingAssembly().Location, RegistryValueKind.String);
            }
        }
    }

    [ComUnregisterFunction]
    internal static void RegasmUnregisterLocalServer(string path)
    {
        // path is HKEY_CLASSES_ROOT\\CLSID\\{clsid}", we only want CLSID...
        path = path.Substring("HKEY_CLASSES_ROOT\\".Length);
        Registry.ClassesRoot.DeleteSubKeyTree(path, throwOnMissingSubKey: false);
    }

    #endregion
}
0
On

Well... here's a managed proof-of-concept using RGiesecke.DllExport that works; whether or not it's a good solution remains to be seen... so: use at your own risk. I'm still looking for better answers.

One thing that could be improved is that we don't need a new AppDomain for each instantiation; only for each object. I'm sure that there are other subtleties that I'm missing.

We compile and register the DLL, then using OleView (or registry), we change the default ProcServer32 value to point to the managed DLL itself. This can be automated by providing a method in the DLL that is decorated with [ComRegisterFunction()].

using System;
using System.ComponentModel;
using System.Reflection;
using System.Runtime.InteropServices;
using RGiesecke.DllExport;
using System.IO;

namespace Com_1
{

    [Guid("F35D5D5D-4A3C-4042-AC35-CE0C57AF8383")]
    [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
    [ComVisible(true)]
    public interface IComClass1
    {
        void SetAppDomainData(string data);
        string GetAppDomainData();
        int GetAppDomainHash();
    }

    //https://gist.github.com/jjeffery/1568627
    [Guid("00000001-0000-0000-c000-000000000046")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    [ComImport]
    internal interface IClassFactory
    {
        void CreateInstance([MarshalAs(UnmanagedType.IUnknown)] object pUnkOuter, ref Guid riid, [MarshalAs(UnmanagedType.IUnknown)] out object ppvObject);
        void LockServer(bool fLock);
    }

    [ComVisible(true)]
    [ClassInterface(ClassInterfaceType.None)]
    [Guid("3CA12D49-CFE5-45A3-B114-22DF2D7A0CAB")]
    [Description("Sample COM Class 1")]
    [ProgId("Com1.ComClass1")]
    public class ComClass1 : MarshalByRefObject, IComClass1, IClassFactory
    {
        public void SetAppDomainData(string data)
        {
            AppDomain.CurrentDomain.SetData("CurrentDomainCustomData", data);
        }

        public string GetAppDomainData()
        {
            return (string)AppDomain.CurrentDomain.GetData("CurrentDomainCustomData");
        }

        public int GetAppDomainHash()
        {
            return AppDomain.CurrentDomain.GetHashCode();
        }

        [DllExport]
        public static uint DllGetClassObject(Guid rclsid, Guid riid, out IntPtr ppv)
        {
            ppv = IntPtr.Zero;

            try
            {
                if (riid.CompareTo(Guid.Parse("00000001-0000-0000-c000-000000000046")) == 0)
                {
                    //Call to DllClassObject is requesting IClassFactory.
                    var instance = new ComClass1();
                    IntPtr iUnk = Marshal.GetIUnknownForObject(instance);
                    //return instance;
                    Marshal.QueryInterface(iUnk, ref riid, out ppv);
                    return 0;
                }
                else
                    return 0x80040111; //CLASS_E_CLASSNOTAVAILABLE
            }
            catch
            {
                return 0x80040111; //CLASS_E_CLASSNOTAVAILABLE
            }        
        }

        public void CreateInstance([MarshalAs(UnmanagedType.IUnknown)] object pUnkOuter, ref Guid riid, [MarshalAs(UnmanagedType.IUnknown)] out object ppvObject)
        {
            IntPtr ppv = IntPtr.Zero;

            //http://stackoverflow.com/a/13355702/864414
            AppDomainSetup domaininfo = new AppDomainSetup();
            domaininfo.ApplicationBase = Directory.GetParent(Assembly.GetExecutingAssembly().Location).FullName;
            var curDomEvidence = AppDomain.CurrentDomain.Evidence;
            AppDomain newDomain = AppDomain.CreateDomain("MyDomain", curDomEvidence, domaininfo);

            Type type = typeof(ComClass1);
            var instance = newDomain.CreateInstanceAndUnwrap(
                   type.Assembly.FullName,
                   type.FullName);

            ppvObject = instance;
        }

        public void LockServer(bool fLock)
        {
            //Do nothing
        }
    }
}