c# - 为什么编码回调委托(delegate)结构会导致 AccessViolationException

标签 c# delegates marshalling

简介

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

原生.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 没有。此外, native 代码显示在调用 cb2 之前,其地址已更改。为什么会发生这种情况?据我所知,没有一个委托(delegate)应该被 gc'ed。作为附加信息,编码 IntPtr 的结构并使用 Marshal.GetFunctionPtrForDelegate 工作正常(即使对于获取本地 ptr 来调用的 cb3),但是,能够直接编码委托(delegate)更有意义/更具可读性。

最佳答案

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

在传入结构之前,cb1、cb2 和 cb3 中的每一个都应该固定,最好是在它们被新建之后立即固定。否则它们可能会在内存中重新定位。

是否决定使用结构来构建经典函数映射以避免这种重定位?如果是这样,那最终是没有帮助的。

关于c# - 为什么编码回调委托(delegate)结构会导致 AccessViolationException,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/1569153/

相关文章:

ios - Swift 中的自定义 UITableViewCell 委托(delegate)模式

vb.net - 将内存中的字节复制到 VB.NET 中的数组

c# - C# 编译器如何检测 COM 类型?

c# - wpf如何从嵌套 View 切换父选项卡

java - 无法使用 groovy @Delegate 从 java 类生成的方法

c# - 表达式树深度限制

c# - 迭代文件夹和子文件夹的最佳方式

c# - 在 using 语句中操作流

c# - MarshalAsAttribute 字符串数组

java - 使用 JAXB 注释外部库中的对象