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:
SetData
on the CurrentDomain
.AppDomain
attributeAppDomain
s, noting that it is also the sameWhy 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:
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:
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
}