在 c# 中将 uchar[] 从本机 dll 编组为 byte[] 的正确方法

Posted

技术标签:

【中文标题】在 c# 中将 uchar[] 从本机 dll 编组为 byte[] 的正确方法【英文标题】:Correct way to marshall uchar[] from native dll to byte[] in c# 【发布时间】:2012-10-29 14:03:51 【问题描述】:

我正在尝试将我的本机 dll 通过 CoTaskMemAlloc 分配的一些数据编组到我的 c# 应用程序中,并且想知道我这样做的方式是否完全错误,或者我错过了方法 c# 方面的一些微妙装饰。

目前我有 c++ 方面。

extern "C" __declspec(dllexport) bool __stdcall CompressData(  unsigned char* pInputData, unsigned int inSize, unsigned char*& pOutputBuffer, unsigned int& uOutputSize)
 ...
    pOutputBuffer = static_cast<unsigned char*>(CoTaskMemAlloc(60000));
    uOutputSize = 60000;

在 C# 方面。

    private const string dllName = "TestDll.dll";

    [System.Security.SuppressUnmanagedCodeSecurity]
    [DllImport(dllName)]
    public static extern bool CompressData(byte[] inputData, uint inputSize, out byte[] outputData, out uint outputSize );
    ...
    byte[] outputData;
    uint outputSize;
    bool ret = CompressData(packEntry.uncompressedData, (uint)packEntry.uncompressedData.Length, out outputData, out outputSize);

这里 outputSize 是 60000 正如预期的那样,但 outputData 的大小为 1,当我对缓冲区 c++ 端进行 memset 时,它似乎只复制了 1 个字节,所以这是错误的,我需要编组外部的数据使用 IntPtr + outputSize 调用,或者我是否缺少一些微妙的东西来开始我已经拥有的工作?

谢谢。

【问题讨论】:

当前行为是有道理的:因为没有任何迹象表明outputSizeoutputData 有任何关系,所以outputData 被视为指向单个@987654326 的指针@。不幸的是,我无法帮助你正确地写这个,只能解释当前的行为。 是的,这确实是有道理的, outputSize 是为了我在测试中的好处,显然如果 byte[] 知道它应该是什么,那么它就会过时。 :) 无论如何谢谢。 【参考方案1】:

有两件事。

首先,P/Invoke 层不处理 C++ 中的引用参数,它只能处理指针。最后两个参数(pOutputBufferuOutputSize)尤其不能保证正确编组。

我建议您将您的 C++ 方法声明更改为(或创建表单的包装器):

extern "C" __declspec(dllexport) bool __stdcall CompressData(  
    unsigned char* pInputData, unsigned int inSize, 
    unsigned char** pOutputBuffer, unsigned int* uOutputSize)

也就是说,第二个问题来自这样一个事实,即 P/Invoke 层也不知道如何封送回“原始”数组(而不是说,COM 中的 SAFEARRAY 知道它的大小)在非托管代码中分配。

这意味着在 .NET 端,您必须编组返回创建的 指针,然后手动编组数组中的元素(以及处置它,如果那是您的责任,看起来就是这样)。

您的 .NET 声明如下所示:

[System.Security.SuppressUnmanagedCodeSecurity]
[DllImport(dllName)]
public static extern bool CompressData(byte[] inputData, uint inputSize, 
    ref IntPtr outputData, ref uint outputSize);

一旦您将outputData 作为IntPtr(这将指向非托管内存),您可以通过调用Copy method 上的Copy method 转换为字节数组,如下所示:

var bytes = new byte[(int) outputSize];

// Copy.
Marshal.Copy(outputData, bytes, 0, (int) outputSize);

请注意,如果您有责任释放内存,您可以调用FreeCoTaskMem method,如下所示:

Marshal.FreeCoTaskMem(outputData);

当然,你可以把它包装成更好的东西,像这样:

static byte[] CompressData(byte[] input, int size)

    // The output buffer.
    IntPtr output = IntPtr.Zero;

    // Wrap in a try/finally, to make sure unmanaged array
    // is cleaned up.
    try
    
        // Length.
        uint length = 0;

        // Make the call.
        CompressData(input, size, ref output, ref length);

        // Allocate the bytes.
        var bytes = new byte[(int) length)];

        // Copy.
        Marshal.Copy(output, bytes, 0, bytes.Length);

        // Return the byte array.
        return bytes;
    
    finally
    
        // If the pointer is not zero, free.
        if (output != IntPtr.Zero) Marshal.FreeCoTaskMem(output);
    

【讨论】:

谢谢你,我不知道 SAFEARRAY 如果它承担调用者清理内存的负担(而不是调用者 / GC 处理它,我假设它使用 SAFEARRAY?)如果没有,我将采用 IntPtr/Copy 路线,我只是在寻找避免它的方法。投票待定。 :) @Niksan SAFEARRAY 将在 C++ 端产生问题,如果你像我最后那样包装上面的代码,它还不错(你只需要编写一次代码) .最后,对你来说更容易。 嗯,对我来说最简单的是副本,但是了解 SAFEARRAY 不会在我的武器库中出错,关于 SAFEARRAY 会在 c++ 方面引起什么问题?我目前正在处理我的 dll 中的 new[]/delete[] 组合,并由于潜在的重新分配而将最终的 CoTaskMemAlloc 作为对 .NET 的最终推动,我会有效地为 SAFEARRAY 做同样的事情,但如果它更麻烦比它值得我跳过。再次感谢。 @casperOne:嗨,你如何在 C++ 代码中 malloc 这个unsigned char** pOutputBuffer?我想用 c++ 代码将一些数据复制到这个缓冲区中 如果您使用的是非托管代码,那么您可以使用任何您想要的机制。在 C++ 中,您最有可能使用 newmalloc。但是,如果您必须在 托管 代码中释放内存,那么您将需要使用一种可以从托管和非托管代码中分配和释放的机制,例如 CoTaskMemAllocLocalAlloc .【参考方案2】:

pinvoke marshaller 无法猜测返回的 byte[] 可能有多大。 C++ 中指向内存的原始指针没有指向内存块的可发现大小。这就是您添加 uOutputSize 参数的原因。对客户端程序很好,但对 pinvoke 编组器来说还不够好。您必须帮助并将 [MarshalAs] 属性应用于 pOutputBuffer,指定 SizeParamIndex 属性。

请注意,编组器正在复制数组。这不是那么理想,您可以通过允许客户端代码传递数组来避免它。编组器将固定它并将指针传递给托管数组。唯一的麻烦是客户端代码没有像样的方法来猜测数组的大小。典型的解决方案是让客户端调用它两次,第一次用 uOutputSize = 0,函数返回所需的数组大小。这将使 C++ 函数看起来像这样:

extern "C" __declspec(dllexport) 
int __stdcall CompressData(
     const unsigned char* pInputData, unsigned int inSize, 
     [Out]unsigned char* pOutputBuffer, unsigned int uOutputSize)

【讨论】:

以上是关于在 c# 中将 uchar[] 从本机 dll 编组为 byte[] 的正确方法的主要内容,如果未能解决你的问题,请参考以下文章

用于 C++ 的 C# 包装器,但仅编译为静态库

如何从本机 C(++) DLL 调用 .NET (C#) 代码?

如何将整数从 C# 方法复制到本机 C DLL 函数

在本机 C++ 中将变量从 C# 编组为 void*,并在本机程序内更改 Managed/C# 中的变量值

从 C# 调用 Advapi32.dll 本机 EventWrite 函数?

将对象列表从 c# 传递到糟糕的 c++ win32 本机 dll 的最有效方法是啥?