为什么编组回调委托的结构会导致AccessViolationException

时间:2009-10-14 21:37:38

标签: c# delegates marshalling

简介

我正在尝试使用P / Invoke来注册一个带有本机dll的回调结构。当调用使本机dll调用回调的函数时,会发生AccessViolationException。我构建了一个“小”测试用例,演示了由2个文件组成的行为,native.cpp编译成native.dll,clr.cs编译成可执行文件。

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

}

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


结果

调用此结果会导致exec_callbacks()抛出AccessViolationException。 cb1已成功调用,但cb2未成功调用。此外,本机代码显示在调用cb2之前,其地址已更改。为什么会这样?据我所知,没有一个代表应该是gc'ed。作为额外的信息,编组IntPtr的结构并使用Marshal.GetFunctionPtrForDelegate正常工作(即使对于获取本机ptr来调用的cb3),但是,能够直接封送代理更有意义/更具可读性。

3 个答案:

答案 0 :(得分:1)

问题是cb1,cb2和cb3是堆分配的,即使它们的存储(结构)不是。因此它们每个都受GC(压缩/重定位,因此使最初传入的指针无效)。

在传入结构之前,应该固定cb1,cb2和cb3中的每一个,理想情况是在它们被新建之后立即固定。否则他们可能会重新安置在记忆中。

是否决定使用结构来构建经典功能映射以避免此重定位?如果是这样,它最终没有帮助。

答案 1 :(得分:0)

首先,发布明确的repro代码的荣誉。现在你的代码有几个问题:

  • 最重要的是,你在set_callback中接收的cb指针(来自C#ref参数)并存储在cbStruct中是不安全的。在set_callback返回后,无法保证它指向的结构将保持不变。如果您更改代码以便通过值传递结构的副本,我认为您的错误将会消失。

  • 所有三个Marshal.GetFunctionPointerForDelegate调用都传递给第一个委托。

  • 如果您想确定委托仍然有效,请在调用exec_callbacks之后插入对GC.KeepAlive(cb.c_cb1)等的调用。

答案 2 :(得分:0)

几天前我遇到了类似的问题。虽然指向结构的指针仍然有效,但是从第一次回调返回后,结构中的函数指针发生了变化。我的方法完全如示例代码中所示。而不是通过val传递结构的副本,我决定传递一个指针并复制它在set_callback中指向的结构,它可以工作。

__declspec(dllexport) void __stdcall set_callback(Callback *cb) {
    cbStruct = new Callback(*cb);
    // TODO: Clean up memory e.g. in release_callback()
}