0

I know, that GC can move objects, so if you want to avoid this for some time, you use

GCHandle.Alloc(..., GCHandleType.Pinned)

I want to pass a C# function to C++ DLL, which will be called several times. I saw this answer about how to pass C# function to C++ DLL and my approach is:

C++:

#include <string>

typedef void(*MyFunction)(const char*);

void foo(MyFunction func) {
    for (int i = 0; i < 1000; i++) {
        std::string s = std::to_string(i);
        func(s.c_str());
    }
}

C#:

class Program {
    delegate void MyFunction([MarshalAs(UnmanagedType.LPStr)] string s);

    [DllImport(..., CallingConvention = CallingConvention.Cdecl)]
    static extern foo(MyFunction function);

    static void Baz(string s) {
         Console.WriteLine(s);
    }

    private static MyFunction delegateInstance;

    static void Main(string[] args) {
        delegateInstance = Baz;
        foo(delegateInstance);
    }
}

This program works fine if built for x64, but silently dies if built for x86. I think GC moves my Baz function at some point and C++ DLL is trying to call it.

If it is, how can I avoid this?

Edit

It looks like the delegate functions must be declared with UnmanagedFunctionPointer to look like

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate void MyFunction([MarshalAs(UnmanagedType.LPStr)] string s);

Now it works fine, but does anyone knows, why is it necessary and did work on x64?

elo
  • 489
  • 3
  • 13
  • 1
    If you hold a reference to `delegateInstance` then GC will not move the unmanaged thunk pointing to it – Charlieface Oct 18 '21 at 10:18
  • 1
    On x64 the calling convention is **fastcall**. Btw this problem isn't related to GC. GC does nothing with unmanaged heap. – aepot Oct 18 '21 at 11:06
  • @Charlieface but if I just call foo(Baz) it may be moved by GC? – elo Oct 18 '21 at 16:44
  • Correct, in that case you need to hold `Baz` in a local variable and call `GC.KeepAlive` after the last time it would be used – Charlieface Oct 18 '21 at 20:16

1 Answers1

1

There is only one calling convention on x64 (and it isn't cdecl). Your CallingConvention.Cdecl applies to x86 only (where there are multiple valid calling conventions). It's therefore unsurprising that things worked fine with the "wrong" calling convention specified on x64, as there's only one possible calling convention and so your CallingConvention was ignored.

Switch to x86 however and suddenly specifying the wrong calling convention matters.

Also note, having the wrong calling convention would cause your app to crash immediately. GC problems tend to kick in after a number of seconds -- things work for a bit, and then stop working. It should be fairly easy to distinguish problems related to the GC (which work for a bit, and then stop) from other sorts of p/invoke problems (which cause an immediate crash)

canton7
  • 37,633
  • 3
  • 64
  • 77
  • A bit unrelated, but doesn't windows and linux use different calling conventions for x64? – Evk Oct 18 '21 at 12:36
  • @Evk Yes they do. But each OS only uses a single calling convention. – canton7 Oct 18 '21 at 12:37
  • If there is only 1 calling convention for x64, which one I need to specify in C# code? – elo Oct 18 '21 at 13:11
  • @elo For x64, it doesn't matter what you put: you can put anything, or nothing at all, and it doesn't matter, because the x64 calling convention will always be used. However, you need to specify the calling convention for x86. So you should specify the calling convention for x86 to use in your code. – canton7 Oct 18 '21 at 13:20