2

I'm trying to pass a C# callback function to a native dll. It works fine, but I couldn't find a way to access the parent object of the callback method. Here's a code which demonstrates what I want to do:

class MyForm: Form {
  public delegate void CallbackDelegate(IntPtr thisPtr);

  [DllImport("mylib.dll", CallingConvention = CallingConvention.StdCall)]
  public static extern void Test(CallbackDelegate callback);

  int Field;

  static void Callback(IntPtr thisPtr)
  {
      // I need to reference class field Field here.
      MyForm thisObject = ?MagicMethod?(thisPtr);

      thisObject.Field = 10;
  }

  void CallExternalMethod()
  {
      Test(Callback);
  }
}

I tried getting the pointer of this but I got the following exception: "Object contains non-primitive or non-blittable data.". I should probably mention that the parent object is a WindowsForms form.

UPDATE

The dll is written in Delphi and the signature of the Test function is the following:

type
  TCallback = procedure of object; stdcall;

procedure Test(Callback: TCallback); stdcall;

I received the above error message when I tried to get the pointer to the class with the following code:

var ptr = GCHandle.Alloc(this, GCHandleType.Pinned).AddrOfPinnedObject();
David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
Max
  • 19,654
  • 13
  • 84
  • 122
  • How's your native Test method in mylib.dll declared? I'm not asking for the definition, just the signature of the method. – Lzh Apr 18 '13 at 19:00
  • Can you give more code. – David Heffernan Apr 18 '13 at 21:51
  • @David Heffernan I'm actually making up the code, rather than copying the original to avoid irrelevant details. I think I wrote all the relevant code. What is missing? – Max Apr 18 '13 at 22:07
  • I have some experience of p/invoke interop to Delphi. And callbacks. But I have simply no idea what you are trying to do. I would love to see a complete program that demonstrates the error. – David Heffernan Apr 18 '13 at 22:14
  • The p/invoke part works perfectly fine. I don't know how to reference fields of MyForm instance from the static callback method. The exception is raised when I try to get the pointer to MyForm instance with `GCHandle.Alloc(this, GCHandleType.Pinned)`. But I'm not sure if that is a correct way for doing what I want. I have updated the code. – Max Apr 18 '13 at 22:20

2 Answers2

4

You are over thinking this. The C# marshalling will create a thunk around your delegate. You must not attempt to do so.

On the Delphi side write it like this:

type
  TCallback = procedure; stdcall;

Simply remove the of object.

On the C# side your delegate is:

public delegate void CallbackDelegate();

The method that you use for your delegate looks like this:

void Callback()
{
    // an instance method, so *this* is available
}

And then you call into the native code like this:

void CallExternalMethod()
{
    Test(Callback);
}

So, let's put it all together. On the Delphi side you can write:

library MyLib;

type
  TCallback = procedure; stdcall;

procedure Test(Callback: TCallback); stdcall;
begin
  Callback;
end;

exports
  Test;

begin
end.

And on the C# side:

class MyForm: Form {
    public delegate void CallbackDelegate();

    [DllImport("mylib.dll")]
    public static extern void Test(CallbackDelegate callback);

    int Field;

    static void Callback()
    {
        Field = 10;
    }

    void CallExternalMethod()
    {
        Field = 0;
        Test(Callback);
        Debug.Assert(Field == 10);
    }
}
David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • Wow! This is really bizarre. So thisPtr is like a dummy parameter here. Thank you very much! That indeed works. – Max Apr 18 '13 at 22:32
  • You need to remove the thisPtr on both sides. Let the marshaller thunk it for you. – David Heffernan Apr 18 '13 at 22:32
  • Here's another question on the topic: http://stackoverflow.com/questions/15666173/how-do-non-static-callbacks-work-from-native-code – David Heffernan Apr 18 '13 at 22:34
  • One thing I forgot to mention is that I cannot remove `of object` from delphi declarion, because it is being used in other parts too. But your before-edit solution worked fine. – Max Apr 18 '13 at 22:36
  • Well, that's a problem. The C# delegate is a single pointer type. The Delphi `of object` pointer is a two pointer type. You really ought to write an adapter layer. You may make it work by good fortune with an extra pointer parameter on the C# side which you ignore, that corresponds to the Delphi `Self` implicit parameter. But that would be pretty wacky. The C# thunk is going to give you the managed `this` no matter what. – David Heffernan Apr 18 '13 at 22:40
  • That's what confused me at first. I knew that `of object` passed `Self` as the first argument, but I didn't know that C# wrapped the non-static delegate into a thunk. – Max Apr 18 '13 at 22:47
0

You're supposed to use the System.Runtime.InteropServices.GCHandle class, its MSDN documentation has examples. It involves using its static methods to convert from C# reference to and from an unmanaged value.

I have no quick way to check, but something like this should work:

static void Callback(IntPtr thisPtr)
{
    MyForm thisObject = (MyForm)GCHandle.FromIntPtr(thisPtr).Target;

    thisObject.Field = 10;
}

The IntPtr should be obtained like this:

IntPtr thisPtr = GCHandle.ToIntPtr(GCHandle.Alloc(thisObject)));

PS: Note that at some point you have to free the handle, either in the callee or in the caller.

Medinoc
  • 6,577
  • 20
  • 42
  • I have updated my question. Could you provide the code to do this? I was trying to use this class, but was unsuccessful. – Max Apr 18 '13 at 21:25
  • @Max I have no quick way to test if this works, but it's the general idea. – Medinoc Apr 18 '13 at 22:06
  • That is what I was doing, except that I used `GCHandleType.Pinned` parameter. I thought that you are not allowed to pass unpinned pointers to native code. – Max Apr 18 '13 at 22:14
  • Your main problem was using `AddrOfPinnedObject` instead of `ToIntPtr`. A `GCHandle` is not a raw pointer, it's more of a cookie to reconstruct the managed pointer (and maintain an "artificial" reference on the object so it's not GC'd). – Medinoc Apr 19 '13 at 07:58