gev29
gev29

Reputation: 59

Marshaling C/C++ function with callback function in parameter to C#

I have the following in my native code:

typedef void (__stdcall * HandlerCallBack)(float);

class ASSIMP_API NewProgressHandler : public ProgressHandler
{
    HandlerCallBack CallBack;
public:
    bool Update(float percentage = -1.f){
        if (CallBack) CallBack (percentage);
        return true;
    }

    void SetCallBack (HandlerCallBack callback) {
        CallBack = callback;
    }
};

void Importer::SetProgressHandlerCallBack (HandlerCallBack CallBack) {
   NewProgressHandler* pNewHandler = new NewProgressHandler ();
   pNewHandler->SetCallBack (CallBack);
   ProgressHandler* pHandler = pNewHandler;
   SetProgressHandler (pHandler);
}

I need to call

SetProgressHandlerCallBack (HandlerCallBack CallBack)

from c# code, so I do the following:

[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate void HandlerCallBack([MarshalAs(UnmanagedType.R4)]float percent);

[DllImport("my.dll")]
public static extern void SetProgressHandlerCallBack ([MarshalAs(UnmanagedType.FunctionPtr)] 
                                                      HandlerCallBack callBack);

public void AssimpCallBack ([MarshalAs(UnmanagedType.R4)]float percent) {
    Debug.Log (percent);
}



void SomeFunction () {
   HandlerCallBack cb = new HandlerCallBack (AssimpCallBack);
   SetProgressHandlerCallBack (cb);
}

but I'm getting an exception,

SetProgressHandlerCallBack

at (wrapper managed-to-native) ModellInstantiator:SetProgressHandlerCallBack (ModellInstantiator/HandlerCallBack)

and I cant figure out what am I missing.

Upvotes: 4

Views: 994

Answers (1)

theB
theB

Reputation: 6738

In your code above the SetProgressHandlerCallback function you show looks like this:

void Importer::SetProgressHandlerCallBack(...) { ... }

There are 2 problems with this, both of which can be explained with a simple Minimum Complete Verifiable Example, I created1 a new Visual Studio solution, containing a C# (WinForms) project and a C++ Dll (unmanaged not C++/CLI).

In the DLL (dllmain.cpp) I placed the following code:

typedef void (__stdcall *HandlerCallBack)(float);

extern "C" {

    // extern "C" ignored for C++ classes
    class __declspec(dllexport) TestClass
    {
    public:
        static void CallMeBack(HandlerCallBack callback)
        {
            callback(1.0f);
        }
    };


    void __declspec(dllexport) CallRightBack(HandlerCallBack callback)
    {
        TestClass::CallMeBack(callback);
    }
}

You can see the callback signature matching the one you're using, and a static class method and a exported function. Build this, start the Visual Studio command prompt and run

dumpbin /exports <dllname>.dll

This gives you the exported functions from the DLL, which looks something like this:

(Trimmed to just show what we care about)
    ordinal hint RVA      name

          1    0 00011082 ??4TestClass@@QEAAAEAV0@$$QEAV0@@Z = @ILT+125(??4TestClass@@QEAAAEAV0@$$QEAV0@@Z)
          2    1 000110E1 ??4TestClass@@QEAAAEAV0@AEBV0@@Z = @ILT+220(??4TestClass@@QEAAAEAV0@AEBV0@@Z)
          3    2 00011069 ?CallMeBack@TestClass@@SAXP6AXM@Z@Z = @ILT+100(?CallMeBack@TestClass@@SAXP6AXM@Z@Z)
          4    3 00011014 CallRightBack = @ILT+15(CallRightBack)

You'll notice that the regular global function is exported with its undecorated name, but that the static method is exported with a decorated (mangled) name.

Now lets add some code to the C# project and see why this becomes a problem. Add 2 buttons (button1 and button22). To the form, I added the following code:

[UnmanagedFunctionPointer(CallingConvention.StdCall)]
delegate void HandlerCallbackDelegate(float value);

[DllImport("CppDllTestProj.dll")]
static extern void CallRightBack([MarshalAs(UnmanagedType.FunctionPtr)]HandlerCallbackDelegate callback);

[DllImport("CppDllTestProj.dll")]
static extern void CallMeBack([MarshalAs(UnmanagedType.FunctionPtr)]HandlerCallbackDelegate callback);

HandlerCallbackDelegate callbackDel;

// in the designer associated with button1
private void button1_Click(object sender, EventArgs e)
{
    if (this.callbackDel == null)
    {
        this.callbackDel = new HandlerCallbackDelegate(this.Callback);
    }
    CallRightBack(callbackDel);
}

// in the designer associated with button2
private void button2_Click(object sender, EventArgs e)
{
    if (this.callbackDel == null)
    {
        this.callbackDel = new HandlerCallbackDelegate(this.Callback);
    }
    CallMeBack(callbackDel);
}

void Callback(float value)
{
    MessageBox.Show(value.ToString());
}

Compile, copy the dll into the C# project's build directory, and run. Click button1, the callback is called, and the message box shows up. Click button 2 and the program crashes with the exception:

Unable to find an entry point named 'CallMeBack' in DLL 'CppDllTestProj.dll'. at ScratchApp.Form1.CallMeBack(HandlerCallbackDelegate callback) at ScratchApp.Form1.button2_Click(Object sender, EventArgs e) in Form1.cs:line 49

Unable to find an entry point named 'CallMeBack' means that it couldn't find a function in that DLL with that name. If I now change the CallMeBack definition in the C# test application to:

[DllImport("CppDllTestProj.dll", EntryPoint="?CallMeBack@TestClass@@SAXP6AXM@Z@Z")]
static extern void CallMeBack([MarshalAs(UnmanagedType.FunctionPtr)]HandlerCallbackDelegate callback);

Recompile, and restart, now both buttons function correctly. (Your mangled/decorated name may look different. Use whatever value you get from dumpbin.)

The root of this problem is that the extern "c" storage class specifier is ignored for C++ classes and class members3.

The other problem which you will have as soon as you fix the above, is this code:

void SomeFunction () {
   HandlerCallBack cb = new HandlerCallBack (AssimpCallBack);
   SetProgressHandlerCallBack (cb);
}

You'll note that cb becomes available for garbage collection the instant that control returns from the call to SetProgressHandlerCallBack. Once that delegate is garbage collected any callback will fail. Spectacularly.


1Technically the same project I use for all testing, but I clear the code each time.
2Creative names, I know.
3Source: Is it possible to export a C++ member method with C linkage in a DLL?

Upvotes: 2

Related Questions