从C++回调到C#函数时出现的访问冲突异常/崩溃

6

我是一名有用的助手,可以为您进行文本翻译。

我正在处理一个本地第三方C++代码库(.lib和.hpp文件),并使用它构建了一个C++/CLI封装器,最终将其用于C#。

当我从调试模式切换到发布模式时,我遇到了一个特定的问题,即当回调函数的代码返回时,我会收到一个访问冲突异常。

回调函数格式的原始hpp文件的代码如下:

typedef int (*CallbackFunction) (void *inst, const void *data);

C++/CLI包装器中回调函数格式的代码: (稍后我会解释为什么我声明了两个)

public delegate int ManagedCallbackFunction (IntPtr oInst, const IntPtr oData);
public delegate int UnManagedCallbackFunction (void* inst, const void* data);

--快速地,我声明第二个“UnManagedCallbackFunction”的原因是我试图在包装器中创建一个“中间人”回调,所以链从Native C++ > C#变成了Native C++ > C++/CLI包装器 > C#的版本...全面披露,问题仍然存在,只是现在被推到了同一行的C++/CLI包装器(返回)上。
最后,来自C#的崩溃代码:
public static int hReceiveLogEvent(IntPtr pInstance, IntPtr pData)
    {
        Console.WriteLine("in hReceiveLogEvent...");
        Console.WriteLine("pInstance: {0}", pInstance);
        Console.WriteLine("pData: {0}", pData);

        // provide object context for static member function
        helloworld hw = (helloworld)GCHandle.FromIntPtr(pInstance).Target;
        if (hw == null || pData == null)
        {
            Console.WriteLine("hReceiveLogEvent: received null instance pointer or null data\n");
            return 0;
        }

        // typecast data to DataLogger object ptr
        IntPtr ip2 = GCHandle.ToIntPtr(GCHandle.Alloc(new DataLoggerWrap(pData)));
        DataLoggerWrap dlw = (DataLoggerWrap)GCHandle.FromIntPtr(ip2).Target;

        //Do Logging Stuff

        Console.WriteLine("exiting hReceiveLogEvent...");
        Console.WriteLine("pInstance: {0}", pInstance);
        Console.WriteLine("pData: {0}", pData);
        Console.WriteLine("Setting pData to zero...");
        pData = IntPtr.Zero;
        pInstance = IntPtr.Zero;
        Console.WriteLine("pData: {0}", pData);
        Console.WriteLine("pInstance: {0}", pInstance);

        return 1;
    }

所有写入控制台的内容都已完成,然后我们看到了可怕的崩溃:

在helloworld.exe中未处理的异常 0x04d1004c:0xC0000005:访问位置0x04d1004c时违规。

如果我从这里进入调试器,我只能看到调用堆栈上的最后一个条目:>“04d1004c()”,它的十进制值为:80805964

只有当你查看控制台时,这才是有趣的:

entering registerDataLogger
pointer to callback handle: 790848
fp for callback: 2631370
pointer to inst: 790844
in hReceiveLogEvent...
pInstance: 790844
pData: 80805964
exiting hReceiveLogEvent...
pInstance: 790844
pData: 80805964
Setting pData to zero...
pData: 0
pInstance: 0

现在,我知道在微软世界中,在调试和发布之间有一些非常不同的事情。 当然,我担心的是字节填充和变量初始化问题,因此,如果我没有提供某些内容,请告诉我,我会添加到(已经很长的)帖子中。 我还认为托管代码可能没有释放所有权,然后本机C ++(我没有代码)可能尝试删除或杀死pData对象,从而导致应用程序崩溃。

更全面的披露,Debug模式下一切似乎都很正常!

这是一个真正的令人头疼的问题,非常感谢任何帮助!

4个回答

4
我认为堆栈被破坏是因为调用约定不匹配: 尝试添加属性。
 [UnmanagedFunctionPointer(CallingConvention.Cdecl)]

在回调委托声明中。

对于支持,这基本上是正确的。在联系第三方供应商后,我们发现他们使用了cdecl规范进行编译,而不是托管代码合规所需的stdcall规范:http://msdn.microsoft.com/en-us/library/367eeye0%28VS.80%29.aspx。我在StackOverflow上添加了一个问题,询问为什么需要这样做?希望有人能给出比引用的MSDN文章更好的解释。 - TomO
在项目设置中,如果没有使用__declspec()指定,则存在用于调用约定(C/C++)的默认值。这种调用约定在代码中不可见。不匹配约定会发生什么是清楚的:如果栈清理的责任不匹配,它将破坏堆栈(因为要么有双重清理,要么清理不足而未能在调用之前重置其状态)。这取决于在堆栈上传递的参数数量。http://en.wikipedia.org/wiki/Calling_convention - jdehaan

0

我不确定你想要实现什么。

以下是几点建议:

1)在发布模式下,垃圾回收器更加积极,因此如果所有权不当,则您所描述的行为并不罕见。

2)我不理解下面的代码尝试做什么?

IntPtr ip2 = GCHandle.ToIntPtr(GCHandle.Alloc(new DataLoggerWrap(pData)));
DataLoggerWrap dlw = (DataLoggerWrap)GCHandle.FromIntPtr(ip2).Target;

你使用GCHandle.Alloc将DataLoggerWrap的实例锁定在内存中,但是你从未将其传递给非托管代码 - 那么为什么要锁定它?你也从未释放它?

第二行然后重新获取引用 - 为什么要循环路径?为什么需要引用 - 你从未使用它?

3) 你将IntPtrs设置为null - 为什么? - 这对函数作用域外没有影响。

4) 你需要知道回调的契约是什么。pData是回调还是调用函数拥有?


0

这并不是直接回答你的问题, 但它可能会引导你朝着正确的方向进行调试模式和发布模式的区别:

由于调试器向堆栈添加了大量记录信息,通常会填充程序在内存中的大小和布局,因此在调试模式下我“幸运地”覆盖了912字节不太重要的内存。然而,没有调试器,我就会覆盖一些相当重要的东西,最终走出自己的内存空间,导致Interop删除它没有拥有的内存。

DataLoggerWrap的定义是什么?char字段可能太小,无法容纳你接收到的数据。


0

我同意@jdehaan的观点,除非第三方库是用BC++编写的,否则CallingConvetion.StdCall可能是答案。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接