【问题标题】:Cannot pass a GCHandle across AppDomains: solution without delegates?无法跨 AppDomains 传递 GCHandle:没有委托的解决方案?
【发布时间】:2014-06-09 10:12:53
【问题描述】:

我有 C++ 基础库,客户端应用程序是 C#。有 c++/cli 接口可以从 C# 访问 c++ api。在没有像 NUnit 或 WCF 托管这样的多个应用程序域发挥作用之前,每一件事都可以正常工作,即使用一个应用程序域。

我已将托管对象存储在 cli 的 gcroot 中以供回调。我已经读到这是应用程序域问题的根本原因(“无法跨 AppDomains 传递 GCHandle”),因为它们没有应用程序域信息(http://lambert.geek.nz/2007/05/29/unmanaged-appdomain-callback/)。有人建议使用委托,但我的底层 C++ 层期望对象而不是函数指针(http://www.lenholgate.com/blog/2009/07/error-cannot-pass-a-gchandle-across-appdomains.html)。我也尝试过 IntPtr 但在这种情况下,我无法在回调期间将其转换为我的托管对象。

更新

让我再详细说明一下我的问题。

我在 C# 中有 "Receiver" 类,它作为输入参数传递给其中一个 api。此接收器对象用于回调。在 C++/CLI 中,我创建了一个本机/非托管类 "ObjectBinder",它是托管 Receiver 类的相同副本(具有相同的方法)。它在 gcroot 中保存托管接收器对象的引用。当我们从 C# 调用该 api 时,它涉及 CLI 层,应用程序域是“客户端 exe”。我们将参数“托管接收器对象”存储在 gcroot 中的 ObjectBinder 中,并将本机 ObjectBinder 对象的引用传递给 C++。现在后端代码(c++ 和 c)发送一个 asyn 回调新线程)到 c++ 层,它使用 ObjectBinder 对象将调用发送回 CLI。现在我们在 ObjectBinder 对象的 CLI 层。 但应用程序域已更改(如果是 WCF 或 NUNIT 或任何其他创建它自己的应用程序域的服务,但在编译时是未知的)。现在我想访问存储在 gcroot 中的托管 Receiver 对象以将回调发送回 C#,但它给出了 APP DOMAIN 错误。

我也尝试过 IntPtrIUnknown * 而不是 gcrootMarshal::GetIUnknownForObject >Marshal::GetObjectForIUnknown 但得到同样的错误。

【问题讨论】:

  • 在没有看到实际代码的情况下很难理解您的更新。如果我没记错的话,你有一个 unmanaged 对象 A,它包含一个指向 managed 对象 B 的 COM 接口指针。从另一个 appdomain 访问 A,它回调 B , 是对的吗?如果是这样,让 B 从 MarshalByRefObject 派生,看看是否有帮助。
  • @Noseratio 你说得对。
  • 那么,从MarshalByRefObject 派生B 并通过Marshal::GetIUnknownForObject() 访问它没有帮助吗?另外,您是否在同一个线程上执行此操作(尽管应用程序域不同)?
  • 我尝试了 Marshal::GetIUnknownForObject() 但它返回的 IntPtr 应该被类型转换为我的托管对象,我知道这样做的唯一方法是使用 GCHandle,它给出了同样的错误。还尝试了 GetObjectForIUnknown 但无法强制转换为托管对象。不,因为我提到回调是异步的,所以基本上是一个新线程
  • 贴一个独立的repro案例,否则我无能为力。

标签: c# .net com c++-cli com-interop


【解决方案1】:

您不能仅使用 GCHandle.ToIntPtr/GCHandle.FromIntPtr 在 .NET 应用程序域之间编组托管对象,即使您派生自 MarshalByRefObjectContextBoundObject

一种选择是使用 COM 和Global Interface Table (GIT)。 COM Marshaller 和 .NET 运行时会将调用编组在一起,但您需要坚持使用由托管对象实现的 COM 接口。这适用于跨不同域和不同 COM 单元线程的调用。

另一种选择是使用Marshal.GetIUnknownForObject 创建一个COM 可调用包装器(CCW),然后使用来自另一个域的Marshal.GetObjectForIUnknown。如果您从 MarshalByRefObject 派生,您将返回一个托管代理对象,否则将返回一个非托管 RCW 代理。如果您在同一个线程(尽管来自另一个应用程序域)上调用您的托管对象,这将起作用。

这是一个示例,说明了原始问题(据我所知)和这两种可能的解决方案。我在这里使用了一个后期绑定的InterfaceIsIDispatch 接口,以避免必须注册类型库(无需执行 RegAsm,以防您还想编组跨域,除了跨域)。

using System;
using System.Runtime.InteropServices;
using System.Threading;

namespace ConsoleApplication
{
    public class Program
    {
        [ComVisible(true)]
        [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)] // late binding only
        public interface ITest
        {
            void Report(string step);
        }

        [ComVisible(true)]
        [ClassInterface(ClassInterfaceType.None)]
        [ComDefaultInterface(typeof(ITest))]
        public class ComObject: MarshalByRefObject, ITest
        {
            public void Report(string step)
            {
                Program.Report(step);
            }
        }

        public static void Main(string[] args)
        {
            var obj = new ComObject();
            obj.Report("Object created.");

            System.AppDomain domain = System.AppDomain.CreateDomain("New domain");

            // via GCHandle
            var gcHandle = GCHandle.Alloc(obj);
            domain.SetData("gcCookie", GCHandle.ToIntPtr(gcHandle));

            // via COM GIT
            var git = (ComExt.IGlobalInterfaceTable)(Activator.CreateInstance(Type.GetTypeFromCLSID(ComExt.CLSID_StdGlobalInterfaceTable)));
            var comCookie = git.RegisterInterfaceInGlobal(obj, ComExt.IID_IUnknown);
            domain.SetData("comCookie", comCookie);

            // via COM CCW
            var unkCookie = Marshal.GetIUnknownForObject(obj);
            domain.SetData("unkCookie", unkCookie);

            // invoke in another domain
            domain.DoCallBack(() =>
            {
                Program.Report("Another domain");

                // trying GCHandle - fails
                var gcCookie2 = (IntPtr)(System.AppDomain.CurrentDomain.GetData("gcCookie"));
                var gcHandle2 = GCHandle.FromIntPtr(gcCookie2);
                try
                {
                    var gcObj2 = (ComObject)(gcHandle2.Target);
                    gcObj2.Report("via GCHandle");
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.Message);
                }

                // trying COM GIT - works
                var comCookie2 = (uint)(System.AppDomain.CurrentDomain.GetData("comCookie"));
                var git2 = (ComExt.IGlobalInterfaceTable)(Activator.CreateInstance(Type.GetTypeFromCLSID(ComExt.CLSID_StdGlobalInterfaceTable)));
                var obj2 = (ITest)git2.GetInterfaceFromGlobal(comCookie2, ComExt.IID_IUnknown);
                obj2.Report("via GIT");

                // trying COM CCW
                var unkCookie2 = (IntPtr)(System.AppDomain.CurrentDomain.GetData("unkCookie"));
                // this casting works because we derived from MarshalByRefObject
                var unkObj2 = (ComObject)Marshal.GetObjectForIUnknown(unkCookie2);
                obj2.Report("via CCW");
            });

            Console.ReadLine();
        }

        static void Report(string step)
        {
            Console.WriteLine(new
                {
                    step,
                    ctx = Thread.CurrentContext.GetHashCode(),
                    threadId = Thread.CurrentThread.ManagedThreadId,
                    domain = Thread.GetDomain().FriendlyName,
                });
        }

        public static class ComExt
        {
            static public readonly Guid CLSID_StdGlobalInterfaceTable = new Guid("00000323-0000-0000-c000-000000000046");
            static public readonly Guid IID_IUnknown = new Guid("00000000-0000-0000-C000-000000000046");

            [ComImport(), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("00000146-0000-0000-C000-000000000046")]
            public interface IGlobalInterfaceTable
            {
                uint RegisterInterfaceInGlobal(
                    [MarshalAs(UnmanagedType.IUnknown)] object pUnk,
                    [In, MarshalAs(UnmanagedType.LPStruct)] Guid riid);

                void RevokeInterfaceFromGlobal(uint dwCookie);

                [return: MarshalAs(UnmanagedType.IUnknown)]
                object GetInterfaceFromGlobal(
                    uint dwCookie,
                    [In, MarshalAs(UnmanagedType.LPStruct)] Guid riid);
            }
        }
    }
}

【讨论】:

  • [A]ComObject 无法转换为 [B]ComObject。类型 A 源自位置“E:\CSImageDBConsoleApp.exe”的上下文“默认”中的“CSImageDBConsoleApp,版本=1.0.0.0,文化=中性,PublicKeyToken=null”。类型 B 源自位置 var gcObj2 = (ComObject)(gcHandle2.Target); 的上下文“默认”中的“CSImageDBConsoleApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null”跨度>
  • @dream_machine,我不知道你的代码有什么问题——你甚至没有发布任何代码。 OTOH,我的答案中的代码工作得很好。对Report 的调用从"New domain" 域编组到原始域。
【解决方案2】:

在没有委托的情况下解决此问题的一种可能解决方法是从您的 ObjectBinder 类中调用 CrossAppDomainSingleton。 CrossAppDomainSingleton 可以保存对您的 Receiver 实例的引用。此解决方案会将您的呼叫分派到一个专用的应用程序域。

如果您有多个 Receiver 实例,这仍然可以使用单例中的映射逻辑并在回调中传递某种 id。

你可以找到一个实现here

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多