如何在单线程客户端中将 ComVisible Class 实例化为自己的 AppDomain?
How to Instantiate a ComVisible Class into Its Own AppDomain in a Single-Threaded Client?
问题
在同一个单线程 COM 客户端中实例化两个独立的 .NET COM 可见 classes 时,.NET 将它们都加载到同一个 AppDomain 中。
我猜这是因为它们被加载到同一个 thread/process。
此行为的示例显示在 this GitHub repository。
本质上,演示如下:
- 实例化一个COMclass
- 在第一个 COM 对象上设置一个属性,该对象在后端调用
CurrentDomain
上的 SetData
。
- 实例化第二个独立的 COM class(不同的接口名称、GUID 等)
- 读取
AppDomain
属性
- 证明它看起来一样
- 此外,从两个
AppDomain
中获取哈希码,注意它也是相同的
为什么会出现这个问题?
当两个 classes 都实现了 AppDomain.CurrentDomain.AssemblyResolve
事件(或任何其他 AppDomain 事件)时,这些事件可能会相互干扰。这至少是一个并发症;我猜可能还有其他人。
一个想法
我认为处理此问题的最佳方法是为每个 COM 对象创建一个新的 AppDomain。因为我找不到(或 Google)以托管方式执行此操作的方法,所以我认为可能有必要在非托管代码中执行此操作。
我做了一点侦探工作。在 OleView 中,.NET COM 可见的 InprocServer32 属性 class 是 mscoree.dll
。因此,我创建了一个 "shim" DLL,它将其所有 EXPORTS
转发到 mscoree.dll。通过消除过程(消除导出直到 COM 不再加载),我发现 mscoree
中的 DllGetClassObject
负责启动 .NET 运行time,而 returning 实例化的 COM 对象。
所以,我能做的就是实现我自己的 DllGetClassObject
,像这样:
- 使用 CLRCreateInstance
在非托管程序集中托管 .NET 运行时间
- 在新
AppDomain
中创建对象,并 return 它
(不过我猜这并不像听起来那么简单)
问题
在开始这个可能困难且漫长的过程之前,我想知道:
- 是否有一种管理方法可以让 .NET COM 在其自己的 AppDomain 中可见 class 到 运行?
- 如果不是,这是 "right" 的方法,还是我错过了一个明显的解决方案?
嗯...这是一个使用 RGiesecke.DllExport 的托管 proof-of-concept, 有效 ;它是否是一个好的解决方案还有待观察......所以:使用风险自负。我仍在寻找更好的答案。
可以改进的一件事是我们不需要为每个 实例化 新的 AppDomain
;仅针对每个对象。我确定我还遗漏了其他一些细微之处。
我们编译并注册 DLL,然后使用 OleView(或注册表)更改默认的 ProcServer32 值以指向托管 DLL 本身。这可以通过在 DLL 中提供一个用 [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;
//
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
}
}
}
如果代码不必 运行 在同一进程中,out-of-process 服务器将是最简单的修复方法。将 CLSCTX_LOCAL_SERVER
传递给 CoCreateInstance
,每个 class 将在 dllhost
托管进程中创建。
例如在客户端:
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);
您还可以注册自定义主机,并将标准 InProcServer32
换成 LocalServer32
。对于示例服务器
// 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
}
问题
在同一个单线程 COM 客户端中实例化两个独立的 .NET COM 可见 classes 时,.NET 将它们都加载到同一个 AppDomain 中。
我猜这是因为它们被加载到同一个 thread/process。
此行为的示例显示在 this GitHub repository。
本质上,演示如下:
- 实例化一个COMclass
- 在第一个 COM 对象上设置一个属性,该对象在后端调用
CurrentDomain
上的SetData
。 - 实例化第二个独立的 COM class(不同的接口名称、GUID 等)
- 读取
AppDomain
属性 - 证明它看起来一样
- 此外,从两个
AppDomain
中获取哈希码,注意它也是相同的
为什么会出现这个问题?
当两个 classes 都实现了 AppDomain.CurrentDomain.AssemblyResolve
事件(或任何其他 AppDomain 事件)时,这些事件可能会相互干扰。这至少是一个并发症;我猜可能还有其他人。
一个想法
我认为处理此问题的最佳方法是为每个 COM 对象创建一个新的 AppDomain。因为我找不到(或 Google)以托管方式执行此操作的方法,所以我认为可能有必要在非托管代码中执行此操作。
我做了一点侦探工作。在 OleView 中,.NET COM 可见的 InprocServer32 属性 class 是 mscoree.dll
。因此,我创建了一个 "shim" DLL,它将其所有 EXPORTS
转发到 mscoree.dll。通过消除过程(消除导出直到 COM 不再加载),我发现 mscoree
中的 DllGetClassObject
负责启动 .NET 运行time,而 returning 实例化的 COM 对象。
所以,我能做的就是实现我自己的 DllGetClassObject
,像这样:
- 使用 CLRCreateInstance 在非托管程序集中托管 .NET 运行时间
- 在新
AppDomain
中创建对象,并 return 它
(不过我猜这并不像听起来那么简单)
问题
在开始这个可能困难且漫长的过程之前,我想知道:
- 是否有一种管理方法可以让 .NET COM 在其自己的 AppDomain 中可见 class 到 运行?
- 如果不是,这是 "right" 的方法,还是我错过了一个明显的解决方案?
嗯...这是一个使用 RGiesecke.DllExport 的托管 proof-of-concept, 有效 ;它是否是一个好的解决方案还有待观察......所以:使用风险自负。我仍在寻找更好的答案。
可以改进的一件事是我们不需要为每个 实例化 新的 AppDomain
;仅针对每个对象。我确定我还遗漏了其他一些细微之处。
我们编译并注册 DLL,然后使用 OleView(或注册表)更改默认的 ProcServer32 值以指向托管 DLL 本身。这可以通过在 DLL 中提供一个用 [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;
//
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
}
}
}
如果代码不必 运行 在同一进程中,out-of-process 服务器将是最简单的修复方法。将 CLSCTX_LOCAL_SERVER
传递给 CoCreateInstance
,每个 class 将在 dllhost
托管进程中创建。
例如在客户端:
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);
您还可以注册自定义主机,并将标准 InProcServer32
换成 LocalServer32
。对于示例服务器
// 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
}