P / Invoke包含C#和C ++之间的双编组数据数组

时间:2014-01-15 13:07:32

标签: c# c++ interop pinvoke marshalling

我已经阅读了有关P / Invoke herehere的C ++ Interop上的各种MSDN页面,但我仍感到困惑。

我有一些大型的双打数组,我需要进入本机代码,以及一些需要返回的结果数组。我事先并不知道输出数组的大小。为简单起见,我将在示例中仅使用单个数组。该平台是x64;我读到在32位和64位环境之间编组内部是完全不同的,所以这可能很重要。

C#

    [DllImport("NativeLib.dll")]
    public static extern void ComputeSomething(double[] inputs, int inlen, 
        [Out] out IntPtr outputs, [Out] out int outlen);

    [DllImport("NativeLib.dll")]
    public static extern void FreeArray(IntPtr outputs);

    public void Compute(double[] inputs, out double[] outputs)
    {           
        IntPtr output_ptr;
        int outlen;
        ComputeSomething(inputs, inputs.Length, out output_ptr, out outlen);

        outputs = new double[outlen];
        Marshal.Copy(output_ptr, outputs, 0, outlen);

        FreeArray(output_ptr);
    }

C ++

extern "C" 
{
    void ComputeSomething(double* inputs, int input_length, 
        double** outputs, int* output_length)
    {
    //...
    *output_length = ...;
    *outputs = new double[output_length];
    //...
    }

    void FreeArray(double* outputs)
    {
        delete[] outputs;
    }
}

它有效,也就是说,我可以读出我在C ++端写入数组的双打。但是,我想知道:

  • 这是否真的是使用P / Invoke的正确方法?
  • 我的签名不是很复杂吗?
  • 可以更有效地使用P / Invoke来解决这个问题吗?
  • 我相信我读过可以避免对内置类型的单维数组进行编组。有没有办法围绕Marshal.Copy?

请注意,我们有一个可用的C ++ / Cli版本,但是在第三方库代码中存在与导致崩溃的本地静态相关的一些问题。 Microsoft marked this issue as WONTFIX,这就是为什么我在寻找其他选择。

3 个答案:

答案 0 :(得分:5)

没关系。完全没有返回错误代码的方法非常糟糕,当数组很大并且程序内存不足时会受到伤害。你得到的严重崩溃是非常不可知的。

复制阵列并明确释放它们的需要当然不会赢得任何奖品。您可以通过让调用者将指针传递给自己的数组来解决这个问题,然后编写元素。但是,您需要一个协议让调用者确定数组需要多大的数量,这需要调用该方法两次。第一个调用返回所需的大小,第二个调用完成工作。

样板示例如下:

[DllImport("foo.dll")]
private static int ReturnData(double[] data, ref int dataLength);

示例用法:

int len = 0;
double[] data = null;
int err = ReturnData(data, ref len);
if (err == ERROR_MORE_DATA) {    // NOTE: expected
    data = new double[len];
    err = ReturnData(data, len);
}

无需复制,无需释放内存,好事。如果本地代码没有注意传递的 len ,那么本机代码可能会损坏GC堆,这不是一件好事。但当然容易避免。

答案 1 :(得分:3)

如果将确定输出长度的代码与填充输出的代码分开是可行的,那么您可以:

  • 导出返回输出长度的函数。
  • 从C#代码中调用它,然后分配输出缓冲区。
  • 再次调用非托管代码,这次要求它填充输出缓冲区。

但我假设您已拒绝此选项,因为它不切实际。在这种情况下,您的代码是解决问题的完美合理方式。事实上,我会说你做得很好。

修复调用约定不匹配后,代码在x86中的工作方式将相同。在C ++方面,调用约定是cdecl,但在C#方面它是stdcall。这与x64无关,因为只有一个调用约定。但是在x86下这将是一个问题。

一些意见:

  • 您不需要使用[Out]以及out。后者意味着前者。
  • 您可以通过分配共享堆来避免导出deallocator。例如,在C ++端的CoTaskMemAlloc,然后在C#端释放Mashal.FreeCoTaskMem

答案 2 :(得分:1)

如果你事先知道了数组大小,你可以编写一个C ++ / CLI DLL,它将托管数组作为参数,将其固定,并在它获得的固定指针上调用本机C ++ DLL。

但如果只是输出,我没有看到没有副本的任何版本。您可以使用SAFEARRAY,因此P / Invoke会复制而不是您,但这就是全部。