简体   繁体   English

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

[英]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. 在同一个单线程COM客户端中实例化两个独立的.NET COM可见类时,.NET会将它们都加载到同一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 . 此行为的示例在此GitHub存储库中显示

Essentially, the demonstration is as follows: 本质上,该演示如下:

  1. Instantiate one COM class 实例化一个COM类
  2. Set an attribute on the first COM object which, in the back-end calls SetData on the CurrentDomain . 在第一个COM对象上设置一个属性,该对象在后端调用CurrentDomain上的SetData
  3. Instantiate a second, independent COM class (different interface name, GUIDs, etc) 实例化第二个独立的COM类(不同的接口名称,GUID等)
  4. Read the AppDomain attribute 阅读AppDomain属性
  5. Demonstrate that it appears the same 证明它看起来一样
  6. Also, get the hash code from both AppDomain s, noting that it is also the same 另外,从两个AppDomain都获取哈希码,请注意,哈希码也相同

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. 当两个类都实现了AppDomain.CurrentDomain.AssemblyResolve事件(或者与此相关的任何其他AppDomain事件)时,这些事件可能会相互干扰。 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. 我认为处理此问题的最佳方法是为每个COM对象创建一个新的AppDomain。 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. 因为我找不到(或Google)以托管方式执行此操作的方法,所以我认为可能有必要在非托管代码中执行此操作。

I did a little detective work. 我做了一些侦探工作。 In OleView, the InprocServer32 attribute for a .NET COM-visible class is mscoree.dll . 在OleView中,.NET COM可见类的InprocServer32属性是mscoree.dll So, I created a "shim" DLL which forwarded all of its EXPORTS to mscoree.dll. 因此,我创建了一个“ shim” DLL,它将所有EXPORTS转发到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. 通过消除过程(消除导出直到COM不再加载),我发现mscoree中的DllGetClassObject负责启动.NET运行时,并返回实例化的COM对象。

So, what I can do is implement my own DllGetClassObject , like so: 因此,我可以做的是实现自己的DllGetClassObject ,如下所示:

  1. Host the .NET runtime in an unmanaged assembly using CLRCreateInstance 使用CLRCreateInstance在非托管程序集中承载 .NET运行时
  2. Create the object in a new AppDomain , and return it 在新的AppDomain创建对象,然后将其返回

(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? 是否有一种托管方法可以使.NET COM可见类在其自己的AppDomain中运行?
  2. If not, is this the "right" way of doing it, or am I missing an obvious 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. CLSCTX_LOCAL_SERVER传递给CoCreateInstance ,每个类将在dllhost托管进程中创建。

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 . 您还可以注册自定义主机,并将标准InProcServer32交换为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
}

Well... here's a managed proof-of-concept using RGiesecke.DllExport that works ; 嗯......这里是一个使用管理证明的概念RGiesecke.DllExport作品 ; 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 ; 可以改进的一件事是,我们不需要为每个实例都使用新的AppDomain 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. 我们编译并注册DLL,然后使用OleView(或注册表)将默认的ProcServer32值更改为指向托管DLL本身。 This can be automated by providing a method in the DLL that is decorated with [ComRegisterFunction()] . 通过在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://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
        }
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM