Error: Cannot pass a GCHandle across AppDomains

| 1 Comment

I'm currently working on adding easy to use Real Time Data server support to my Managed XLL Excel Addin system. This lets you use the =RTD() functionality of Excel to push real time data into your spreadsheets without needing to understand COM and without needing to register any COM objects on your machine. You simply add some attributes to your managed code, compile your assembly and drop it in the same directory as the Managed XLL and it does the rest. This is working well and makes Excel Real Time Data servers VERY easy to write.

Anyway, during this I came across a slight problem. The architecture is such that I create a custom AppDomain for the managed code that the XLL hosts inside of Excel. The XLL then causes unmanaged callbacks into this managed code when the various Excel RTD COM object functions are called. The bridge between the unmanaged code and the managed code is a gcroot<> that holds onto the managed code inside the unmanaged wrapper via a GCHandle. The COM call eventually calls through this gcroot<> and into the managed code. Unfortunately unmanaged code knows nothing of AppDomains and (it seems from reading this) that an AppDomain is chosen arbitrarily when the unmanaged call into the managed code occurs. This causes the code to fail with a "Cannot pass a GCHandle across AppDomains" exception.

Luckily for me, Miral, over at Thoughts from Mirality has already solved this issue and covers the problem and a solution here. The trick is that you need to use a delegate, which knows about the AppDomain that it relates to, and then call through the delegate by converting it to a function pointer. This effectively marshals the unmanaged call into the correct AppDomain before executing the managed code.

Miral's example code was enough to help me along my way but I stumbled a couple of times before I solved my particular version of the problem so I thought it would be worth documenting what I needed to do to get this to work for me.

Lets say we have the following unmanaged callback. Some code that knows nothing about managed code wants to call the following function:

void Connect(
   const long topicId, 
   const JetByteTools::Win32::StringVector &topics, 
   const bool getNewValue, 
   VARIANT &result);

The first thing we need to do is create a typedef for the function signature and a managed class that can provide us with the delegate that we need to be able to call this correctly. Something like this, perhaps:

typedef void (__stdcall ConnectFnc)(
   const long topicId, 
   const JetByteTools::Win32::StringVector &topics, 
   const bool getNewValue, 
   VARIANT &result);
 
ref class ConnectDelegate
{
   public :
 
      ConnectDelegate();
         :  m_delegate(gcnew Delegate(this, &ConnectDelegate::Connect))
      {
      }
 
      ConnectFnc *GetDelegateFunctionPointer();
      {
         return   (ConnectFnc*)(Marshal::GetFunctionPointerForDelegate(m_delegate).ToPointer());
      }
 
   private :
 
      void Connect(
         long topicId, 
         JetByteTools::Win32::StringVector &topics, 
         bool getNewValue, 
         VARIANT &result)
      {
         // YOUR CODE GOES HERE and is executed in the correct AppDomain...
      }
 
      delegate void Delegate(
         long topicId, 
         JetByteTools::Win32::StringVector &topics, 
         bool getNewValue, 
         VARIANT &result);
 
      Delegate ^m_delegate;
};

The managed code that you want to execute inside the correct AppDomain lives in ConnectDelegate::Connect(). The constructor of our class simply creates the delegate to call the function and GetDelegateFunctionPointer() does exactly what it says on the tin. An instance of the ConnectDelegate class needs to be created by code that is running inside the AppDomain that you want your unmanaged callback to call into.

It might look like you can then simply call ConnectDelegate::GetDelegateFunctionPointer() from your unmanaged code to obtain a function pointer and make your callback, but unfortunately you need one further level of indirection before that's possible. As I said at the start, the problem is that calling through a GCHandle via a gcroot<> from unmanaged code gives a "Cannot pass a GCHandle across AppDomains" exception. Well, right now you still need a gcroot<> to hold onto your ConnectDelegate so that you can call it from unmanaged code and calling ConnectDelegate::GetDelegateFunctionPointer() calls through that GCHandle and raises the exact same exception... The trick is that you also need to call ConnectDelegate::GetDelegateFunctionPointer() from within your target AppDomain and then store away the function pointer for later use. Since this leaves you with a gcroot<ConnectDelegate^> and a ConnectFnc* to hold onto in unmanaged code I put together a simple unmanaged class which manages both of these and provides an overloaded function call operator to make it easy to call the ConnectFnc*. The result is something like this:

class ConnectFunction
{
   public :
 
      ConnectFunction()
         :   m_pDelegate(0),
             m_pFunction(0)
      {
      }
 
      ~ConnectFunction()
      {
         delete m_pDelegate;
      }
 
      void operator()(
         const long topicId, 
         const JetByteTools::Win32::StringVector &topics, 
         const bool getNewValue, 
         VARIANT &result) const
      {
         m_pFunction(topicId, topics, getNewValue, result);
      }
 
      void Setup()
      {
         m_pDelegate = new gcroot<connectdelegate^>(gcnew ConnectDelegate());   
 
         m_pFunction = (*m_pDelegate)->GetDelegateFunctionPointer();
      }
 
   private :
 
      gcroot<ConnectDelegate ^> *m_pDelegate;
 
      ConnectFnc *m_pFunction;
 
      // No copies do not implement
      ConnectFunction(const ConnectFunction &rhs);
      ConnectFunction &operator=(const ConnectFunction &rhs);
};

You can then create an instance of this class and then call Setup() from within your target AppDomain. You can then use the function call operator on your instance from within your unmanaged code to make the callback. You can move the body of Setup() into the constructor if you like, my particular situation requires two stage construction.

This is crying out to be generalised with a template or two, but for now that's left as an exercise for the reader...

1 Comment

This will certainly work but in my case, number calls are huge. Any other solution?

Leave a comment