Cannot pass a GCHandle across AppDomains: solution without delegates?
Asked Answered
R

2

9

I have base library in c++ and client application is in C#. There is c++/cli interface to access c++ api's from C#. Every thing works fine until more than one app domain not come into play like NUnit or WCF hosting i.e. with one app domain.

I have stored managed object in gcroot in cli for callback. I have read that this is the root cause of app domain issue ("Cannot pass a GCHandle across AppDomains") because they don't have app domain info (http://lambert.geek.nz/2007/05/29/unmanaged-appdomain-callback/). someone suggested to use delegates but my underlying c++ layer is expecting object not function pointer(http://www.lenholgate.com/blog/2009/07/error-cannot-pass-a-gchandle-across-appdomains.html). I have also tried IntPtr but in this case i am not able to cast it to my managed object during callbacks.

UPDATE

Let me elaborate my problem a bit more.

I have "Receiver" class in C# and this is passed as input parameter to one of the api. This receiver object is used for callback. In C++/CLI I have created a Native /unmanaged class "ObjectBinder" which is same replica (has same methods) of managed Receiver class. It holds reference of managed receiver object in gcroot. When we call that api from C# it comes to CLI layer and app domain is "client exe". we store the parameter "managed receiver object" in ObjectBinder in gcroot and pass reference of native ObjectBinder object to C++. Now the backend code (c++ and c) send an asyn callback (new thread) to c++ layer which use ObjectBinder object to send back call to CLI. Now we are in CLI layer in ObjectBinder object. BUT App domain has been changed (in case of WCF or NUNIT or any other service that creates it's own App domain which is not known at compile time) . Now i want to access managed Receiver object which is stored in gcroot to send back callback to C# but it gave APP DOMAIN error.

I have also tried IntPtr and IUnknown * instead of gcroot with Marshal::GetIUnknownForObject and Marshal::GetObjectForIUnknown but getting same error.

Refractometer answered 9/6, 2014 at 10:12 Comment(6)
Hard to understand your update without seeing the actual code. If I got it right, you have an unmanaged object A which holds a COM interface pointer to a managed object B. A is accessed from another appdomain, where it calls back B, is that right? If so, make B derive from MarshalByRefObject and see if that helps.Idun
@Noseratio You got it right.Refractometer
So, has deriving B from MarshalByRefObject and accessing it via Marshal::GetIUnknownForObject() not helped? Also, are you doing this on the same thread (despite a different appdomain)?Idun
I tried Marshal::GetIUnknownForObject() but it returns IntPtr which should be typecast to my managed object and the only way i know to do this is using GCHandle which gave same error. Also tried GetObjectForIUnknown but not able to typecast to managed object. No as i mentioned callback is asynchronous so basically a new threadRefractometer
Post a self-contained repro case, otherwise I can't help any further.Idun
I will try to post the sample code for this problem so that you can check itRefractometer
I
8

You cannot marshal a managed object between .NET application domains simply with GCHandle.ToIntPtr/GCHandle.FromIntPtr, even if you derive from MarshalByRefObject or ContextBoundObject.

One option to do that is to use COM and Global Interface Table (GIT). The COM Marshaller and .NET runtime will marshal the calls together, but you'd need to stick with a COM interface implemented by the managed object. This will work for calls across different domians and different COM apartment threads.

Another option is to create a COM-callable wrapper (CCW) with Marshal.GetIUnknownForObject, then use Marshal.GetObjectForIUnknown from another domain. You'll get back a managed proxy object if you derived from MarshalByRefObject, or an unmanaged RCW proxy otherwise. This will work if you call your managed object on the same thread (albeit, from another app domain).

Here is an example which illustrates the original problem (as I understood it) and these two possible solutions. I use a late-bound InterfaceIsIDispatch interface here to avoid having to register the type library (no need to do RegAsm, in case you also want to marshal cross-apartments, in addition to cross-domains).

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);
            }
        }
    }
}
Idun answered 9/6, 2014 at 13:14 Comment(2)
[A]ComObject cannot be cast to [B]ComObject. Type A originates from 'CSImageDBConsoleApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' in the context 'Default' at location 'E:\CSImageDBConsoleApp.exe'. Type B originates from 'CSImageDBConsoleApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' in the context 'Default' at location var gcObj2 = (ComObject)(gcHandle2.Target);Refractometer
@dream_machine, I can't tell what's wrong with your code - you haven't even posted any code. OTOH, the code in my answer works just fine. The call to Report gets marshaled to the original domain from "New domain" domain.Idun
R
1

One possible workaround for this problem without delegates is to call a CrossAppDomainSingleton from your ObjectBinder class. The CrossAppDomainSingleton can hold the reference to your Receiver instance. This solution will dispatch your call to a dedicated app domain.

If you have multiple Receiver instances this could still work with a mapping logic in the singleton and passing some kind of id in the callback.

You can find an implementation here.

Radford answered 27/11, 2016 at 10:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.