Archives

Anticipation

  • No dates present

AppDomains and unmanaged callbacks, redux

Quite a while back, I posted an article about getting native callbacks to work across AppDomains. Since then, I’ve gotten quite a few comments with varying levels of confusion, and seen a few implementations that appear to have missed something along the way. So I thought it’d be a good idea to post a clarification.

Fundamentally, there are just a few things that you need to remember:

  • You cannot access managed objects (including GCHandles and gcroots) outside of the AppDomain in which they were created.
  • Each thread which has executed managed code has a “history” linking it to a particular AppDomain. If a thread has never executed managed code before, though, then the first time it tries it will be assumed to be in the first AppDomain.
  • Delegates contain information about which AppDomain they should be executed in, so they can “cross the line” when an otherwise pure native thread has no idea which AppDomain it should be using.
  • Managed delegates can be turned into native function pointers via the Marshal::GetFunctionPointerForDelegate method.
  • If you create a function pointer from a delegate, you must keep the delegate alive (by keeping a reference to it) for as long as the native pointer exists.
  • Native functions can only have native types as parameters (but they don’t have to just be primitive types). (Actually, there are some limited exceptions to this, which I cover below.)

So, here’s a few example scenarios, to get your head around the ideas. Note that none of these care whether you’re crossing AppDomain boundaries or not (they’re cross-AppDomain-safe, but can be used within the same AppDomain too), so they can also serve as good examples for regular interop scenarios as well.

Direct-use callbacks

This is where you’ve started in managed code, but dropped into native code for a moment in order to use a native callback, but that callback is used immediately and not stored anywhere. A simple example of this is the WinAPI function EnumWindows. In this case, you don’t have to do anything fancy:

delegate BOOL WndEnumProc(HWND hwnd, LPARAM lParam);
 
public ref class NativeMethods abstract sealed
{
public:
    static void Test()
    {
        WndEnumProc^ callback = gcnew WndEnumProc(&NativeMethods::WindowCallback);
        EnumWindows((WNDENUMPROC)Marshal::GetFunctionPointerForDelegate(callback).ToPointer(), 0);
    }
 
private:
    static BOOL CALLBACK WindowCallback(HWND hwnd, LPARAM lParam)
    {
        _TCHAR title[1024];
        if (GetWindowText(hwnd, title, 1024))
        {
            Console::WriteLine("\"{0}\"", marshal_as<String^>(title));
        }
        return TRUE;
    }
};

(Of course it’s possible to do this sort of thing directly from C# with P/Invoke, too. But for these examples I’m sticking with C++/CLI to be consistent with some of the more complicated examples below.)

Longer-term parameter-based callbacks

When the callback function isn’t used immediately, but is saved for later use by some external code, then you must ensure that the delegate is kept alive just as long as that external code has a reference to the callback. For this example, I’m assuming that you already have some external library which provides this API:

typedef void (CALLBACK *NLibLogFn)(const std::string& message);
void NLibSetLogger(NLibLogFn logger);

(Note that this assumes that the external API is already __stdcall, which is the case with CALLBACK; if your external API is __cdecl or something else instead, then you’ll need to make some changes to the code. This is discussed in more detail below.)

Now we want to write a logging class which can log messages both from managed code and this native library. We do that like so:

public ref class Logger
{
public:
    Logger()
    {
        m_Delegate = gcnew LogDelegate(this, &Logger::Log);
        NLibSetLogger((NLibLogFn)Marshal::GetFunctionPointerForDelegate(m_Delegate).ToPointer());
    }
    ~Logger()
    {
        this->!Logger();
    }
    !Logger()
    {
        NLibSetLogger(NULL);
    }
 
    void Log(String^ message)
    {
        Console::WriteLine(message);
    }
 
private:
    delegate void LogDelegate(const std::string& message);
    void Log(const std::string& message)
    {
        Log(marshal_as<String^>(message));
    }
 
private:
    LogDelegate^ m_Delegate;
};

(Note that if the native library can call the callback from multiple threads, then you’ll have to use standard thread-safety features such as Monitor as normal.)

To use this, we simply need to create an instance of Logger from managed code, and keep it alive for as long as we want the logging to be available (typically the entire lifetime of the application). Don’t forget to call delete (or Dispose from other languages) on it when you’re done, otherwise there’s a slight risk that the external library will try to log something while the GC is trying to collect the object, which can lead to undefined behaviour. (Using the finalizer will help reduce the danger window, but it won’t eliminate it.)

Note that in this case we’re assuming that the external library only supports having a single active logger via NLibSetLogger; if it supported multiple loggers (eg. the interface had the pair NLibAddLogger and NLibRemoveLogger instead), then we would have had to store a copy of the native function pointer ourselves for use at unregistration time.

Longer-term class-based callbacks

This is the scenario which I blogged about earlier, but I’ll use a slightly different example this time for clarity. In this case, you have an existing external native framework which provides some callback API (again, we’ll assume that this is for logging, but the principle applies to anything); but unlike the example above, this one uses the Functor or Observer patterns to call methods on a provided callback class, instead of calling a function pointer directly. We’ll assume that the external API looks like this:

namespace NativeFramework
{
    class ILogger
    {
    public:
        virtual ~ILogger();
        virtual void Log(const std::string& message) = 0;
    };
 
    void RegisterLogger(ILogger *logger);
    void UnregisterLogger(ILogger *logger);
 
    inline ILogger::~ILogger()
    {
        UnregisterLogger(this);
    }
}

For this case, we can construct the same managed-or-native Logger class as in the above example, but we need to define an additional helper class to do so:

delegate void LogDelegate(const std::string& message);
 
class NativeLogger : public NativeFramework::ILogger
{
public:
    explicit NativeLogger(LogDelegate^ logger)
      : m_Delegate(logger)
    {
        m_Native = (LogNative)Marshal::GetFunctionPointerForDelegate(logger).ToPointer();
    }
 
    virtual void Log(const std::string& message) override
    {
        m_Native(message);
    }
 
private:
    typedef void (__stdcall *LogNative)(const std::string& message);
 
private:
    gcroot<LogDelegate^> m_Delegate;
    LogNative m_Native;
};
 
public ref class Logger
{
public:
    Logger()
    {
        m_Logger = new NativeLogger(gcnew LogDelegate(this, &Logger::Log));
        NativeFramework::RegisterLogger(m_Logger);
    }
    ~Logger()
    {
        this->!Logger();
    }
    !Logger()
    {
        delete m_Logger;
        m_Logger = NULL;
    }
 
    void Log(String^ message)
    {
        Console::WriteLine(message);
    }
 
private:
    void Log(const std::string& message)
    {
        Log(marshal_as<String^>(message));
    }
 
private:
    NativeLogger *m_Logger;
};

Of particular note here is that NativeLogger::Log must not call through the delegate directly, even though it appears to have access to it — if you are in a cross-AppDomain scenario then an attempt to get the delegate out of the gcroot will fail. This is why the call through the native function pointer is required. Also note that in this case the native function pointer is being stored by our code; this is to avoid calling GetFunctionPointerForDelegate more than once, which is potentially wasteful. (In the previous example, the native library would have stored this pointer for us.)

Using managed signatures

But wait, there’s more! Not only is GetFunctionPointerForDelegate not limited to primitive types (as some people seemed to think from my previous post), it’s not even limited to native types. In some situations, you can include managed parameters in your native signatures and simplify the code a bit. Which situations? Essentially, it will work if the managed objects can be constructed within the native handler, and where those types are [Serializable]; it will end up constructing the objects in one AppDomain, serialising them, and then deserialising them in the “real” AppDomain at the other end. (It will not work for non-serializable types, or for existing objects fetched via gcroot; also, using this trick for complex types will mean that these types have to get loaded in the first AppDomain, which may defeat the point of using multiple AppDomains in the first place.)

Note that this does imply that there’s a bit of additional overhead here, so the slight improvement to code tidiness (specifically, the elimination of the native variant of the Log method in the Logger class) may not be worth it for complex types, especially for high-use code paths. In the case of our example, though, the cost of serializing a String (or even a wrapper class that contains a single String) is so minimal that it makes no difference.

Applying this technique to our example, we get this:

delegate void LogDelegate(String^ message);
 
class NativeLogger : public NativeFramework::ILogger
{
public:
    explicit NativeLogger(LogDelegate^ logger)
      : m_Delegate(logger)
    {
        m_Native = (LogNative)Marshal::GetFunctionPointerForDelegate(logger).ToPointer();
    }
 
    virtual void Log(const std::string& message) override
    {
        m_Native(marshal_as<String^>(message));
    }
 
private:
    typedef void (__stdcall *LogNative)(String^ message);
 
private:
    gcroot<LogDelegate^> m_Delegate;
    LogNative m_Native;
};
 
public ref class Logger
{
public:
    Logger()
    {
        m_Logger = new NativeLogger(gcnew LogDelegate(this, &Logger::Log));
        NativeFramework::RegisterLogger(m_Logger);
    }
    ~Logger()
    {
        this->!Logger();
    }
    !Logger()
    {
        delete m_Logger;
        m_Logger = NULL;
    }
 
    void Log(String^ message)
    {
        Console::WriteLine(message);
    }
 
private:
    NativeLogger *m_Logger;
};

Calling Conventions and other tweaks

One snag that you might run into when using this technique to interop with native code is that by default GetFunctionPointerForDelegate will return a function pointer with stdcall calling conventions. This is usually fine when you’re trying to use WinAPI callbacks, but sometimes you’ll need cdecl or one of the other conventions instead. (You’ll note that all of the examples above used __stdcall functions.)

One way to handle this, using C++/CLI at least, is to define a native __cdecl (or whatever) function which you pass to the native API and which internally calls the native function pointer, thereby side-stepping the issue. In some respects, this is similar to how the class-based callback example above works.

But there’s another way — the UnmanagedFunctionPointer attribute can be applied to the delegate type, and this allows you to specify options such as the calling convention and ANSI/Unicode string conversion. For example, if CALLBACK had been missing from the longer-term parameter-based callback example (thereby making the callback function __cdecl), you could use exactly the same code but define the delegate like this:

    [UnmanagedFunctionPointer(CallingConvention = CallingConvention.Cdecl)]
    delegate void LogDelegate(const std::string& message);

(And just like with P/Invoke, you can apply MarshalAs attributes to the parameters of the delegate to influence how the unmanaged function signature will appear and how data will be transferred across the boundary.)

It’s important to remember this — bugs involving calling conventions can manifest quite subtly, especially between stdcall and cdecl and if (as by default) functions use standard “stack frames” (because that often hides the problem) — and in fact if you actually try it, you’ll find that the example above is simple enough that it’ll still appear to work perfectly without the attribute. (I once posted a bit of a rant on a particularly irritating example I encountered.)

Afterword

Hopefully this has cleared up some of the confusion and misconceptions stemming from my earlier post. In particular one of the common mistakes I saw was to assume that only primitive types could be used, simply because I was using a BYTE array and count as my parameters in the previous post. This was actually due to the requirements of the specific native library I was using; there’s no trouble at all in passing any native types through, even complex C++ classes; and in some cases it’s even possible to use managed types, as shown above.

As always, if there’s any more remaining confusion or other things that you’d like me to clarify, then please comment below (or in the Suggestion Box).

9 comments to AppDomains and unmanaged callbacks, redux


  • Catchable fatal error: Object of class WP_Comment could not be converted to string in /home/uecasm/public_html/lambert/wpdata/wp-content/themes/atahualpa/functions/bfa_custom_comments.php on line 16