C# P/Invoke调用中的AccessViolationException

C# P/Invoke调用中的AccessViolationException,c#,.net,c,C#,.net,C,我正在通过p/Invoke调用编写一个小的zlib包装器。它在64位目标(64位C#build,64位DLL)上运行良好,但在32位目标(32位C#build,32位DLL)上抛出AccessViolationException 下面是引发异常的C#签名和代码: [DllImport(Program.UnmanagedDll, CallingConvention = CallingConvention.Cdecl)] private static extern ZLibResult ZLibDe

我正在通过p/Invoke调用编写一个小的zlib包装器。它在64位目标(64位C#build,64位DLL)上运行良好,但在32位目标(32位C#build,32位DLL)上抛出AccessViolationException

下面是引发异常的C#签名和代码:

[DllImport(Program.UnmanagedDll, CallingConvention = CallingConvention.Cdecl)]
private static extern ZLibResult ZLibDecompress(byte[] inStream, uint inLength, byte[] outStream, ref uint outLength);

internal enum ZLibResult : byte {
        Success = 0,
        Failure = 1,
        InvalidLevel = 2,
        InputTooShort = 3
}

internal static ZLibResult Decompress(byte[] compressed, out byte[] data, uint dataLength) {
    var len = (uint) compressed.Length;
    fixed (byte* c = compressed) {
        var buffer = new byte[dataLength];
        ZLibResult result;
        fixed (byte* b = buffer) {
            result = ZLibDecompress(c, len, b, &dataLength);
        }
        if(result == ZLibResult.Success) {
            data = buffer;
            return result;
        }
        data = null;
        return result;
    }
}
下面是C代码(用MinGW-w64编译):

C代码:

[DllImport(Program.UnmanagedDll, CallingConvention = CallingConvention.Cdecl)]
private static extern ZLibResult ZLibDecompress(
    [MarshalAs(UnmanagedType.LPArray)]byte[] inStream, uint inLength,
    [MarshalAs(UnmanagedType.LPArray)]byte[] outStream, ref uint outLength);

internal static ZLibResult Decompress(byte[] compressed, out byte[] data, uint dataLength) {
    var buffer = new byte[dataLength];
    var result = ZLibDecompress(compressed, (uint)compressed.Length, buffer, ref dataLength);
    if(result == ZLibResult.Success) {
        data = buffer;
        return result;
    }
    data = null;
    return result;
}
__declspec(dllexport) uint8_t __cdecl ZLibDecompress(uint8_t* inStream, uint32_t inLength,
                                 uint8_t* outStream, uint32_t* outLength) {
    uLongf oL = (uLongf)*outLength;
    int result = uncompress(outStream, &oL, inStream, inLength);
    *outLength = (uint32_t)oL;
    if(result == Z_OK)
        return ZLibCompressSuccess;
    return ZLibCompressFailure;
}

在64位中,Windows只有一个ABI(没有cdecl/stdcall),因此32位的问题似乎在于调用约定。您的参数指针进入了错误的寄存器,并且本机函数访问了错误的内存区域

要解决这个问题:

  • 尝试注释本机函数中的行(查看它是否崩溃-是的,这不是调用约定)

  • 尝试使用调用约定“cdecl/stdcall”

  • 要检查所有内容,请尝试转储指针值,并查看它们在本机/托管函数中是否一致

  • 编辑:

    然后是指针的问题。您正在C#中分配数组(因此它们位于托管堆中)。您必须使用“[MarshalAs(UnmanagedType.LPArray)]”属性封送它们

    [DllImport(Program.UnmanagedDll, CallingConvention = CallingConvention.Cdecl)]
    private static extern ZLibResult ZLibDecompress(
         [MarshalAs(UnmanagedType.LPArray)] byte[] inStream,
         uint inLength,
         [MarshalAs(UnmanagedType.LPArray)] byte[] outStream,
         ref UInt32 outLength);
    
    [In,Out]修饰符可能也有帮助

    是的,正如汉斯所说,别上指针,不要让它们被垃圾收集

    byte[] theStream = new byte[whateveyouneed];
    // Pin down the byte array
    GCHandle handle = GCHandle.Alloc(theStream, GCHandleType.Pinned); 
    IntPtr address = handle.AddrOfPinnedObject();
    
    然后将其作为IntPtr传递

        fixed (byte* b = buffer) {
            result = ZLibDecompress(c, len, b, &dataLength);
        }
    
    不,那不行。fixed关键字提供了一种高度优化的方法,以确保垃圾收集器移动对象不会造成问题。它不是通过固定对象(如文档所述)来实现的,而是通过向垃圾收集器公开
    b
    变量来实现的。然后看到它引用缓冲区,并在移动
    buffer
    时更新
    b
    的值

    然而,在这种情况下,它无法工作,
    b
    值的副本被传递给ZlibDecompress()。垃圾收集器无法更新该副本。当ZLibDecompress()运行时发生GC时,结果将很糟糕,本机代码将破坏垃圾收集堆的完整性,并最终导致AV

    不能使用fixed,必须使用GCHandle.Alloc()固定缓冲区


    但是也不要那样做,你帮了太多的忙了。pinvoke marshaller已经非常擅长在必要时固定对象。将
    流内
    流外
    参数声明为byte[]而不是byte*。直接传递数组而不做任何特殊的操作。另外,应该声明
    outlength
    参数
    ref int

    实际问题是由MinGW-w64生成错误的DLL引起的。在构建zlib时,我一直在将-ftree矢量化传递给gcc,zlib正在生成32位CLR不喜欢的代码。使用不太激进的优化选项后,代码运行良好。

    您是使用32位MinGW还是尝试交叉编译?您是否尝试过从C代码中使用库?我正在使用MinGW-w64构建一个32位DLL,是的,在C程序中调用时,这些函数工作正常,没有堆损坏。现在看不到任何问题。您是否尝试过使用32位MinGW?虽然我不希望有什么不同。您缺少
    不安全的
    关键字,但除此之外。。。实际上,您可能可以跳过整个指针内容,只传递我认为(使
    不安全
    过时)的数组,这可能会排除另一个可能的错误源。我使用的是32位MinGW,MinGW-w64项目提供32位和64位编译器。另外,包含Decompress()的类是不安全的,所以这不是问题所在。我不希望使用.NET数组,因为它会增加不必要的编组开销。正如我所说,它可以使用64位构建,但不能使用32位构建,这很奇怪,因为我没有使用任何特定于平台的数据类型。谢谢,我从来没有想过使用stdcall。是否仍然可以禁用导出的stdcall函数的名称损坏?我知道Windows API使用STDCULL并且没有被函数化的名称。名称MINGLING只与C++代码相关。函数名前的extern“C”将禁用损坏。但我想你知道的,别管名字弄乱了,把它传给MinGW链接器,把它搞定了。我仍然会遇到stdcall或cdecl的访问冲突,托管代码和本机代码中的inStream和outtream指针完全相同,注释掉C代码中调用uncompress()的行可以阻止崩溃。还有其他想法吗?那么我想我们应该检查编组属性。看起来字节*指向托管堆。请参阅我的编辑。尝试了这两个示例,但它们仍然在32位上提供访问冲突。我应该放弃吗?谢谢你的洞察力,我会记住fixed不会锁定对象。我尝试过直接传递数组,并使用GCHandle.Alloc(),但仍然遇到了32位特定的访问冲突。同样,当从C程序调用本机函数时,它可以正常工作。我很困惑为什么现在不能工作。请注意文章的最后一段。您删除了CallingConvention,但没有。将其重新添加,结果相同。StdCall不应该默认吗?您添加了错误的调用约定,它是Cdecl。你有没有考虑过用别人的图书馆来完成这件事?SharpZipLib和DotNetZip也可以做到这一点。
    
        fixed (byte* b = buffer) {
            result = ZLibDecompress(c, len, b, &dataLength);
        }