Search code examples
c#.netcomcomvisible

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


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?

Solution

  • 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
    }