【问题标题】:P/Invoke with arrays of double - marshalling data between C# and C++P/Invoke 在 C# 和 C++ 之间使用双组数据编组数据
【发布时间】:2014-02-03 23:17:53
【问题描述】:

我已经阅读了有关 C++ Interop 与 P/Invoke herehere 的各种 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,这就是我寻找替代品的原因。

【问题讨论】:

    标签: c# c++ interop pinvoke marshalling


    【解决方案1】:

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

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

    样板示例如下:

    [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 堆,这不是一件好事。但当然很容易避免。

    【讨论】:

    • 正如我在问题中所写,我事先不知道输出数组的大小。确定这个大小需要计算整个事情。关于错误代码的好点,我从这个例子中删除了所有内容。
    • 输出数组的大小只有在C++中对输入数组进行计算后才能知道。我确实知道一些上限,但该上限是预期输出大小的 2-3 倍。
    • 嗯,这也使得在 C++ 代码中调用 new double[] 变得非常困难。回调是一个明显的选择。
    • 好的,谢谢。我也希望我能接受你的回答。很有帮助!
    【解决方案2】:

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

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

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

    修复调用约定不匹配后,代码在 x86 中的工作方式完全相同。在 C++ 端,调用约定是 cdecl,但在 C# 端是 stdcall。这在 x64 上无关紧要,因为只有一个调用约定。但是在x86下会出问题。

    一些cmets:

    • 您不需要使用[Out]out。后者隐含前者。
    • 您可以通过分配共享堆来避免导出解除分配器。例如在 C++ 端使用 CoTaskMemAlloc,然后在 C# 端使用 Mashal.FreeCoTaskMem 解除分配。

    【讨论】:

    • 是的,我在问题中写道,我事先不知道输出大小:)。感谢您的提示,CoTaskMemAlloc 确实改进并简化了代码。
    • 是的,但是您可以询问非托管代码在一次调用中需要多少空间,分配内存,然后在第二次调用中将固定的内存传回。汉斯演示的方式。
    【解决方案3】:

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

    但如果它只是输出,我看不到任何没有副本的版本。您可以使用 SAFEARRAY,以便 P/Invoke 代替您进行复制,仅此而已。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-01-21
      • 2017-01-18
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多