Friday, April 06, 2007

Now that the book is written and all urgent tasks I had to defer due to the book are done, I find some time to blog about technical topics.

Recently, a customer asked me how to marshal function pointers across managed / unmanaged interop boundaries. If you know a simple API and two attributes and if you are aware of a pitfall specific to marshaling function pointers, the job can be quite easy.

To discuss this topic, consider the following simple API:

namespace NativeAPI
{
  struct CallbackData
  {
    int i;
    double d;
  };

  typedef void (*PFNCallback)(CallbackData* p);

  class SampleClass
  {
    PFNCallback _pfn;
  public:
    SampleClass(PFNCallback pfn);
    void F();
  };
}

In my book I focus on wrapping class libraries that use virtual functions for callbacks instead of function pointers, because virtual functions are the typical C++-like approach for callbacks. But obviously, a lot of C++ class libraries have their roots in C which can force you to care about arguments of function pointer types.

The managed equivalent to a native function pointer is a delegate. There are different ways to map between delegates and function pointers. The following code shows a delegate that can be mapped to the native function pointer of type PFNCallback:

namespace ManagedWrapper {   using namespace System::Runtime::InteropServices;

  [StructLayout(LayoutKind::Sequential)]
  public value struct CallbackData
  {
    int i;
    double d;
  };

  [UnmanagedFunctionPointer(CallingConvention::Cdecl)]
  public delegate void CallbackDelegate(CallbackData% p);
}

Notice that I first define a managed wrapper type for CallbackData that has the same binary layout as its native counterpart. This is done with the StructLayout attribute. The attribute is only used for documentation purposes, because sequential layout is the default setting for custom value types defined in C++/CLI (and C#). The delegate type has the UnmanagedFunctionPointerAttribute, which is not optional in this case. Using this attribute, you can specify the calling convention of the native function pointer type. In the native API's header file, the PFNCallback is defined without an explicit calling convention specification – this is not recommended, but it occurs quite often. In this case, the function pointer type has the default calling convention, which depends on compiler switches. If no compiler switch is used, the default calling convention for C-style function pointers is __cdecl. Using the compiler switches /Gz or /Gr, the default calling convention can be changed to __stdcall or __fastcall. In the concrete scenario that my customer faced, neither /Gz nor /Gr are used. To express that the CallbackDelegate should be marshaled to a __cdecl function pointer, the UnmanagedFunctionPointerAttribute is necessary. If the native function pointer is a __fastcall function, you can not provide a simple mapping, because calling __fastcall functions from managed code is not supported by the current version (2.0) of the CLR. Once you have properly defined the delegate, you can use the function Marshal::GetFunctionPointerForDelegate to receive a pointer to a native->managed thunk for the delegate. This pointer can then be passed to native code. If native code uses this pointer for function calls, the thunk performs the native->managed transition and invokes the delegate. To wrap NativeAPI::SampleClass, you can implement the following wrapper class:

namespace ManagedWrapper
{
  public ref class SampleClass
  {
    NativeAPI::SampleClass* _pWrappedObject;
    CallbackDelegate^ _callbackDelegate;
  public:
    SampleClass(CallbackDelegate^ cbd);
    void F();
    ~SampleClass();
  };
}

In the constructor of this wrapper class, you can use Marshal::GetFunctionPointerForDelegate to determine the function pointer that is passed to the constructor of NativeLib::SampleClass. The following code shows the implementation of ManagedWrapper::SampleClass.

namespace ManagedWrapper
{
  SampleClass::SampleClass(CallbackDelegate^ callbackDelegate)
  : _callbackDelegate(callbackDelegate),
  _pWrappedObject(nullptr)
  {
    IntPtr p = Marshal::GetFunctionPointerForDelegate(_callbackDelegate);
    _pWrappedObject = new NativeAPI::SampleClass((NativeAPI::PFNCallback)p.ToPointer());
    if (!_pWrappedObject)
      throw gcnew OutOfMemoryException("Could not allocate memory on C++ free store");
  }

  void SampleClass::F()
  {
    _pWrappedObject->F();
  }

  SampleClass::~SampleClass()
  {
    NativeAPI::SampleClass* pWrappedObject = _pWrappedObject;
    _pWrappedObject = nullptr;
    delete _pWrappedObject;
  }
}

Notice that in the member initialization list of the constructor, I first store a handle to the target delegate in a member variable. This is important, to control the lifetime of the thunk. At the first view, one might think that the thunk manages a tracking handle to the target delegate, which would keep the delegate alive as long as the thunk exists. However, the opposite is the case. It is not the thunk that keeps the delegate alive; the delegate keeps the thunk alive: The thunk is guaranteed to exist only as long as the delegate exists. The thunk only contains a weak reference to the target delegate which it can use for invocation if the delegate is not garbage collected. Due to this implementation, the following code would definitely be a bug:

SampleClass::SampleClass(CallbackDelegate^ callbackDelegate)
: _pWrappedObject(nullptr)
{
  IntPtr p = Marshal::GetFunctionPointerForDelegate(callbackDelegate);
  _pWrappedObject = new NativeAPI::SampleClass((NativeAPI::PFNCallback)p.ToPointer());
  if (!_pWrappedObject)
    throw gcnew OutOfMemoryException("Could not allocate memory on C++ free store");
}

Since the delegate that was used to create the thunk (and the function pointer referring to the thunk) is not stored in a member variable, it can be GCed unless other references to the delegate exist. When this delegate is GCed, the thunk can be removed as well. Notice that the thunk is not automatically removed when its target delegate is deleted, because the thunk and the delegate are created on different heaps: the delegate is instantiated on the GC heap, whereas the thunk is created on a heap that I like to call the CLR's code heap. An important difference of these heaps is that the code heap consists of memory pages that have the attribute PAGE_EXECUTE_READWRITE. The following code shows you some internals about Marshal::GetDelegateForFunctionPointer

// GFPFDTests.cpp
// build with "CL /clr GFPFDTests.cpp"

#include "windows.h"

using namespace System;
using namespace System::Diagnostics;
using namespace System::Runtime::InteropServices;

typedef void (__cdecl* PFN)();

[UnmanagedFunctionPointer(CallingConvention::Cdecl)]
public delegate void PFNDelegate();

// managed function has __cdecl calling convention
// therefore, a PFN can point to it.
void __cdecl F()
{
Console::WriteLine("::F called");
}
int main(array ^args)
{
  PFNDelegate^ d1 = gcnew PFNDelegate(F);
  IntPtr p1 = Marshal::GetFunctionPointerForDelegate(d1);
  IntPtr p2 = Marshal::GetFunctionPointerForDelegate(d1);
  // calling M::GFPFD twice for same delegate returns same thunk
  Debug::Assert(p1 == p2);

  PFNDelegate^ d2 = gcnew PFNDelegate(F);
  p2 = Marshal::GetFunctionPointerForDelegate(d2);
  // calling M::GFPFS twice for different delegates returns
  // different thunks even if delegate target is the same
  Debug::Assert(p1 != p2);
  MEMORY_BASIC_INFORMATION mbi;
  VirtualQuery(p1.ToPointer(), &mbi, sizeof(mbi));
  // thunks are allocated on a heap that consits of pages   // with the PAGE_EXECUTE_READWRITE flag
  Debug::Assert((mbi.Protect & PAGE_EXECUTE_READWRITE) ==
                 PAGE_EXECUTE_READWRITE);

  void* pD1 = *(void**)&d1; // hack to get native pointer to delegate
  VirtualQuery(pD1, &mbi, sizeof(mbi));
  // .NET objects are allocated on the GC heap which consists of
  // pages with the PAGE_READWRITE flag
  Debug::Assert((mbi.Protect & PAGE_READWRITE) ==
                PAGE_READWRITE);

  PFN pfn = (PFN)p1.ToPointer();
  pfn();
  WeakReference wr(d1);
  d1 = nullptr;
  GC::Collect(2);
  Debug::Assert(!wr.IsAlive);
  try
  {
    pfn();
    // since the target delegate does not exist any more,
    // an exception is thrown here and the line below is
    // not executed
    // if you execute this in a debugger, you will likely see
    // a "Managed Debugging Assistant" instead.
    // MDAs are CLR internal assertion-like constructs     Debug::Assert(FALSE);
  }
  catch (System::AccessViolationException^ ex)
  {
    Debug::WriteLine("Expected exception");
  }
}

In this post I have discussed how to treat marshal delegates to native function pointers. However, I have not discussed what you should do if the signature of the native function requires parameter marshalling. This will be addressed in my next post.

4/6/2007 10:13:08 AM (GMT Daylight Time, UTC+01:00)  #    Disclaimer  |   |  Trackback