2

我正在通过 P/Invoke 调用编写一个小型 zlib 包装器。它在 64 位目标(64 位 C# 构建,64 位 DLL)上完美运行,但在 32 位目标(32 位 C# 构建,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 编译):

#include <stdint.h>
#include "zlib.h"

#define ZLibCompressSuccess         0
#define ZLibCompressFailure         1

__cdecl __declspec(dllexport) uint8_t 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;
}

我已经查看了所有内容,但无法弄清楚为什么在 32 位版本而不是 64 位版本上会发生访问冲突。ZLibDecompress 在从 C 应用程序调用时可以很好地解压缩相同的流,但在从我的 C# 应用程序调用时会引发访问冲突。

有谁知道为什么会发生这种情况?

编辑: 更新了我的代码,仍然在 32 位版本上遇到访问冲突,但不是 64 位。

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

C代码:

__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;
}
4

3 回答 3

4
    fixed (byte* b = buffer) {
        result = ZLibDecompress(c, len, b, &dataLength);
    }

不,那行不通。fixed关键字提供了一种高度优化的方式来确保垃圾收集器移动对象不会造成麻烦。它不是通过固定对象来做到这一点(如文档所说),而是通过将b变量暴露给垃圾收集器来做到这一点。然后它会看到它引用缓冲区并更新b它何时移动的值buffer

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

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

但也不要那样做,你帮助太多了。pinvoke marshaller 已经非常擅长在必要时固定对象。instream将and参数声明outstream为 byte[] 而不是 byte*。并直接传递数组而不做任何特别的事情。此外,outlength应该声明参数ref int

于 2012-06-30T09:42:10.667 回答
1

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

要解决此问题:

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

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

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

编辑:

然后是指针的问题。您在 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 传递。

于 2012-06-30T08:47:46.460 回答
0

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

于 2012-07-07T11:40:25.223 回答