Why marsing a callback delegate structure throws an AccessViolationException

Introduction

I am trying to use P / Invoke to register a callback structure with a native dll. An AccessViolationException is thrown when a function is called that causes the built-in dll to invoke callbacks. I have built a "small" test case that demonstrates the behavior with 2 files, native.cpp, which compiles to native.dll and clr.cs, which compiles to executable.

native.cpp


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 &lt&lt "Got callbacks: " &lt&lt std::endl &lt&lt
        "cb1: " &lt&lt std::hex &lt&lt cb->cb1 &lt&lt std::endl &lt&lt
        "cb2: " &lt&lt std::hex &lt&lt cb->cb2 &lt&lt std::endl &lt&lt
        "cb3: " &lt&lt std::hex &lt&lt cb->cb3 &lt&lt std::endl;
}


void return_callback(int i) {
    std::cout &lt&lt "[Native] Callback from callback 3 with input: " &lt&lt i &lt&lt std::endl;
}

__declspec(dllexport) void exec_callbacks() {
    std::cout &lt&lt "[Native] Executing callback 1 at " &lt&lt std::hex &lt&lt cbStruct->cb1 &lt&lt std::endl;
    std::cout &lt&lt "[Native] Result: " &lt&lt cbStruct->cb1() &lt&lt std::endl;
    std::cout &lt&lt "[Native] Executing callback 2 at " &lt&lt std::hex &lt&lt cbStruct->cb2 &lt&lt std::endl;
    std::cout &lt&lt "[Native] Result: " &lt&lt cbStruct->cb2("2") &lt&lt std::endl;
    std::cout &lt&lt "[Native] Executing callback 3 with input 3 at " &lt&lt std::hex &lt&lt cbStruct->cb3 &lt&lt std::endl;
    cbStruct->cb3(return_callback, 3);
    std::cout &lt&lt "[Native] Executing callback 3 with input 4 at " &lt&lt std::hex &lt&lt cbStruct->cb3 &lt&lt std::endl;
    cbStruct->cb3(return_callback, 4);
}

}

      

clr.cs


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();
        }
    }
}



      

Result

Calling this result results in exec_callbacks () throwing an AccessViolationException. cb1 is called successfully, but cb2 is not. In addition, the native code shows that its address changed prior to calling cb2. Why is this happening? As far as I know, none of the delegates should have been listed. As additional information, sorting the IntPtr structure and using Marshal.GetFunctionPtrForDelegate works correctly (even for cb3, which gets its own ptr to call), however the ability to marshal delegates directly makes more sense / more readable.

+2


source to share


3 answers


The problem is that cb1, cb2 and cb3 are allocated heaps, although their storage (structure) is not. So each one is subject to GC (compacting / moving, thereby invalidating the pointers originally passed to).

Before passing through the structure, each of cb1, cb2 and cb3 must be anchored, ideally immediately after they are new'd. Otherwise, they will probably move around in memory.



Was the decision to use a framework to build a classic feature map to avoid this movement? If so, it is ultimately not useful.

+1


source


First of all, the prestige for publishing clear code. Now a couple of problems with your code:



  • Most importantly, the cb pointer you get in set_callback (from a C # ref parameter) and storing in cbStruct is safe to store. There is no guarantee that the structure it points to will remain after set_callback returns. If you change your code so that a copy of the struct is passed by value instead, I think your errors will go away.

  • All three calls to Marshal.GetFunctionPointerForDelegate are passed as the first delegate.

  • If you are really sure the delegates are still valid, insert calls to GC.KeepAlive (cb.c_cb1), etc. after calling exec_callbacks.

0


source


I was struggling with a similar issue a couple of days ago. Although the pointer to the structure remained in effect, the pointers to the structured function were changed after the return from the first callback. My approach was exactly as shown in your example code. Instead of passing a copy of the struct val, I decided to pass in a pointer and copy the structure 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()
}

      

0


source







All Articles