Reputation: 307
I am trying to use P/Invoke to register a struct of callbacks with a native dll. When calling a function that makes the native dll invoke the callbacks an AccessViolationException occurs. I have constructed a "small" test case that demonstrates the behavior comprised of 2 files, native.cpp that compiles into native.dll and clr.cs that compiles into the executable.
extern "C" {
typedef void (*returncb)(int i);
typedef struct _Callback {
int (*cb1)();
int (*cb2)(const char *str);
void (*cb3)(returncb cb, int i);
} Callback;
static Callback *cbStruct;
__declspec(dllexport) void set_callback(Callback *cb) {
cbStruct = cb;
std::cout << "Got callbacks: " << std::endl <<
"cb1: " << std::hex << cb->cb1 << std::endl <<
"cb2: " << std::hex << cb->cb2 << std::endl <<
"cb3: " << std::hex << cb->cb3 << std::endl;
}
void return_callback(int i) {
std::cout << "[Native] Callback from callback 3 with input: " << i << std::endl;
}
__declspec(dllexport) void exec_callbacks() {
std::cout << "[Native] Executing callback 1 at " << std::hex << cbStruct->cb1 << std::endl;
std::cout << "[Native] Result: " << cbStruct->cb1() << std::endl;
std::cout << "[Native] Executing callback 2 at " << std::hex << cbStruct->cb2 << std::endl;
std::cout << "[Native] Result: " << cbStruct->cb2("2") << std::endl;
std::cout << "[Native] Executing callback 3 with input 3 at " << std::hex << cbStruct->cb3 << std::endl;
cbStruct->cb3(return_callback, 3);
std::cout << "[Native] Executing callback 3 with input 4 at " << std::hex << cbStruct->cb3 << std::endl;
cbStruct->cb3(return_callback, 4);
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
namespace clr {
public delegate void returncb(Int32 i);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate int cb1();
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate int cb2(string str);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void cb3(returncb cb, Int32 i);
[StructLayout(LayoutKind.Sequential)]
struct Callback {
[MarshalAs(UnmanagedType.FunctionPtr)]
public cb1 c_cb1;
[MarshalAs(UnmanagedType.FunctionPtr)]
public cb2 c_cb2;
[MarshalAs(UnmanagedType.FunctionPtr)]
public cb3 c_cb3;
}
class Program {
static int cb1Impl() {
Console.WriteLine("[Managed] callback 1");
return 1;
}
static int cb2Impl(string c) {
Console.WriteLine("[Managed] callback 2");
return int.Parse(c);
}
static void cb3Impl(returncb cb, Int32 i) {
Console.WriteLine("[Managed] callback 3");
Console.WriteLine("[Managed] Executing callback to native.");
cb(i);
}
[DllImport("native.dll", CallingConvention = CallingConvention.Cdecl)]
static extern void set_callback(ref Callback cb);
[DllImport("native.dll", CallingConvention = CallingConvention.Cdecl)]
static extern void exec_callbacks();
static void Main(string[] args) {
Callback cb;
cb.c_cb1 = new cb1(cb1Impl);
cb.c_cb2 = new cb2(cb2Impl);
cb.c_cb3 = new cb3(cb3Impl);
Console.WriteLine("Beginning test.");
Console.WriteLine("Sending callbacks: ");
Console.WriteLine("cb1: " + Marshal.GetFunctionPointerForDelegate(cb.c_cb1));
Console.WriteLine("cb2: " + Marshal.GetFunctionPointerForDelegate(cb.c_cb1));
Console.WriteLine("cb3: " + Marshal.GetFunctionPointerForDelegate(cb.c_cb1));
set_callback(ref cb);
exec_callbacks();
Console.ReadLine();
}
}
}
Invoking this results in exec_callbacks() throwing an AccessViolationException. cb1 is successfully invoked, but cb2 is not. Furthermore, the native code shows that before cb2 is called, its address has changed. Why does this occur? To the best of my knowledge, none of the delegates should have been gc'ed. As additionally information, marshalling a struct of IntPtr's and using Marshal.GetFunctionPtrForDelegate works correctly (even for cb3 which gets a native ptr to invoke), however, being able to marshal the delegates directly makes more sense/is more readable.
Upvotes: 2
Views: 4354
Reputation: 8749
The problem is that cb1, cb2 and cb3 are heap allocated, even though their storage (the struct) is not. Thus they are each subject to GC (compaction/relocation, thus invalidating the pointers originally passed in).
Prior to passing in the struct, each of cb1, cb2 and cb3 should be pinned, ideally immediately after they are new'd. Otherwise they will likely relocate in memory.
Was the decision to use a struct to contruct a classic function map done to avoid this relocation? If so, it's ultimately non-helpful.
Upvotes: 1
Reputation: 1
I struggled with a similar problem a couple of days ago. Although the pointer to the struct remained valid, the function pointers withhin the struct changed after returning from the first callback. My approach was exactly as shown in your sample code. Instead of passing a copy of the struct by val, i decided to pass a pointer and copy the struct it pointed to in set_callback, it works.
__declspec(dllexport) void __stdcall set_callback(Callback *cb) {
cbStruct = new Callback(*cb);
// TODO: Clean up memory e.g. in release_callback()
}
Upvotes: 0
Reputation: 4808
First of all, kudos for posting clear repro code. Now a couple of issues with your code:
Most importantly, the cb pointer you're receiving in set_callback (from the C# ref parameter) and storing in cbStruct isn't safe to store. There's no guarantee that the struct it's pointing to will stay around after set_callback returns. If you change your code so a copy of the struct is passed by value instead, I think your errors will go away.
All three Marshal.GetFunctionPointerForDelegate calls are passed the first delegate.
If you want to be really sure that the delegates remain valid, insert calls to GC.KeepAlive(cb.c_cb1) etc after the call to exec_callbacks.
Upvotes: 0