如何在单线程客户端中将 ComVisible 类实例化为自己的 AppDomain?

Posted

技术标签:

【中文标题】如何在单线程客户端中将 ComVisible 类实例化为自己的 AppDomain?【英文标题】:How to Instantiate a ComVisible Class into Its Own AppDomain in a Single-Threaded Client? 【发布时间】:2017-01-12 15:00:13 【问题描述】:

问题

当在同一个单线程 COM 客户端中实例化两个独立的 .NET COM 可见类时,.NET 会将它们都加载到同一个 AppDomain 中。

我猜这是因为它们被加载到同一个线程/进程中。

this GitHub repository 中显示了此行为的一个示例。

本质上,演示如下:

    实例化一个 COM 类 在第一个 COM 对象上设置属性,该对象在后端调用 CurrentDomain 上的 SetData。 实例化第二个独立的 COM 类(不同的接口名称、GUID 等) 读取AppDomain属性 证明它看起来一样 另外,从AppDomains 中获取哈希码,注意也是一样的

为什么会出现这个问题?

当两个类都实现了AppDomain.CurrentDomain.AssemblyResolve 事件(或任何其他AppDomain 事件,就此而言),这些事件可能会相互干扰。这至少是一种并发症;我猜可能还有其他人。

一个想法

我认为最好的处理方法是为每个 COM 对象创建一个新的 AppDomain。因为我找不到(或 Google)以托管方式执行此操作的方法,所以我认为可能有必要在非托管代码中执行此操作。

我做了一点侦探工作。在 OleView 中,.NET COM 可见类的 InprocServer32 属性是 mscoree.dll。因此,我创建了一个“垫片”DLL,它将其所有 EXPORTS 转发到 mscoree.dll。通过消除过程(消除导出直到 COM 不再加载),我发现 mscoree 中的 DllGetClassObject 负责启动 .NET 运行时并返回实例化的 COM 对象。

所以,我能做的就是实现我自己的DllGetClassObject,就像这样:

    使用CLRCreateInstance 在非托管程序集中托管 .NET 运行时 在新的AppDomain 中创建对象,并返回它

(不过我猜它并不像听起来那么简单)

问题

在开始这个可能困难且漫长的过程之前,我想知道:

    是否有一种托管方式让 .NET COM 可见类在其自己的 AppDomain 中运行? 如果不是,这是“正确”的做法,还是我错过了一个明显的解决方案?

【问题讨论】:

【参考方案1】:

如果代码不必在同一个进程中运行,进程外服务器将是最简单的解决方法。将CLSCTX_LOCAL_SERVER 传递给CoCreateInstance,每个类都将在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 客户端是本机客户端 (VBA),因此我尝试使用 P/Invoke CoCreateInstance。 P/Invoke 确实CLSCTX_INPROC_SERVER 工作,但我在使用CLSCTX_LOCAL_SERVER 时得到E_UNEXPECTED(意外失败)。然后我在我的 GitHub 示例中尝试了你的 LocalServer32 代码,它也产生了失败。尝试通过 CoCreateInstance 和 VBA 的 CreateObject 进行实例化。 FWIW,我还尝试将整个 InprocServer32 密钥交换为 LocalServer32,使用 regedit——只是为了看看它是否会改变任何东西——但是,结果相同。 进程外 COM 服务器似乎运行良好,因此感谢您为我指明了正确的方向。我在 Microsoft 的网站上找到了this example,它与您的网站相似,但有一些额外的代码可以通过消息循环在进程外运行 COM。 Adding Microsoft's code to the minimal example I provided,我可以验证 OOP 对象是在与进程内对象不同的 AppDomain 中创建的。【参考方案2】:

嗯...这是一个使用RGiesecke.DllExport 的托管概念验证工作;这是否是一个好的解决方案还有待观察......所以:使用风险自负。我仍在寻找更好的答案。

可以改进的一点是我们不需要为每个实例化创建一个新的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;

            //http://***.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
        
    

【讨论】:

以上是关于如何在单线程客户端中将 ComVisible 类实例化为自己的 AppDomain?的主要内容,如果未能解决你的问题,请参考以下文章

如何在单线程中运行 ScalaTest 套件?

异步编程如何在单线程编程模型中工作?

我可以在单线程中使用两个套接字吗?

随便选择两个城市作为预选旅游目标。实现两个独立的线程分别显示10次城市名,每次显示后休眠一段随机时间(1000ms以内),哪个先显示完毕,就决定去哪个城市。分别用Runnable接口和Thread类实

随便选择两个城市作为预选旅游目标。实现两个独立的线程分别显示10次城市名,每次显示后休眠一段随机时间(1000ms以内),哪个先显示完毕,就决定去哪个城市。分别用Runnable接口和Thread类实

随便选择两个城市作为预选旅游目标。实现两个独立的线程分别显示10次城市名,每次显示后休眠一段随机时间(1000ms以内),哪个先显示完毕,就决定去哪个城市。分别用Runnable接口和Thread类实