Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Callback from unmanaged C++ to C# works, but only in debugger

Tags:

c#

c++-cli

Callbacks to C# from unmanaged C++ are tricky. I learned most of the required cruft from this MSDN article and this stackoverflow tip, and the result works fine in the debugger. But outside of the debugger it fails with "Object reference not set to an instance of an object".

Here's the (simplified) C# code:

class CSharpCode
{
    delegate void CallbackDelegate();

    void DoCSharp()
    {
        CallbackDelegate callbackDelegate = TheCallback;
        IntPtr callbackDelegatePointer = Marshal.GetFunctionPointerForDelegate(callbackDelegate);
        GCHandle gchCallbackDelegate = GCHandle.Alloc(callbackDelegatePointer);

        GC.Collect(); // create max space for unmanaged allocations
        CppCliCode.DoCppCli(callbackDelegatePointer);
    }

    public static void TheCallback()
    {
        MessageBox.Show("It worked");
    }
}

And here's the C++ code:

#pragma managed

public ref class CppCliCode
{
    static void DoCppCli(IntPtr^ callbackDelegatePointer)
    {
        callback theCallback = static_cast<callback>(callbackDelegatePointer->ToPointer());
        DoCpp(theCallback);
    }
}

#pragma unmanaged

typedef void (__stdcall *callback)();

void DoCpp(callback theCallback)
{
    theCallback();
}

The error occurs somewhere between invoking theCallback() and arriving at TheCallback(). The error suggests that some invisible managed object has become null.

If I remove the GC.Collect() the problem goes away. But that just means it will reappear someday as an intermittent mystery when a GC happens to occur at the wrong moment.

The GCHandle protects the delegate from being collected but allows it to be relocated. The MSDN article says "If a delegate is re-located by a garbage collection, it will not affect the underlaying managed callback, so Alloc is used to add a reference to the delegate, allowing relocation of the delegate, but preventing disposal. Using GCHandle instead of pin_ptr reduces fragmentation potential of the managed heap."

What's wrong?

like image 937
Rick Mohr Avatar asked Dec 13 '11 15:12

Rick Mohr


1 Answers

You must allocate the delegate itself, not its IntPtr. Also you must free the GCHandle when you are done with CSharpCode instance.

class CSharpCode : IDisposible
{
    delegate void CallbackDelegate();
    GCHandle gchCallbackDelegate;

    void DoCSharp()
    {
        CallbackDelegate callbackDelegate = TheCallback;
        IntPtr callbackDelegatePointer = Marshal.GetFunctionPointerForDelegate(callbackDelegate);
        gchCallbackDelegate = GCHandle.Alloc(callbackDelegate); // !!!!

        GC.Collect(); // create max space for unmanaged allocations
        CppCliCode.DoCppCli(callbackDelegatePointer);
    }

    public void Dispose()
    {
        CleanUp();
    }

    ~CSharpCode()
    {
        CleanUp();
    } 

    CleanUp()
    {
        if(gchCallbackDelegate.IsAllocated)
            gchCallbackDelegate.Free();
    }


}

By the way I hope you have more powerful naming system of yours. Names like DoCSharp, TheCallBack, theCallBack etc. gave me a hard time to understand the question.

like image 56
ali_bahoo Avatar answered Oct 21 '22 02:10

ali_bahoo