P/Invoke使用C#和C++之间的双编组数据数组

P/Invoke with arrays of double - marshalling data between C# and C++

本文关键字:数组 数据 之间 C++ Invoke 使用      更新时间:2023-10-16

我已经阅读了C++Interop上使用p/Invoke的各种MSDN页面,但我仍然感到困惑。

我有一些大型的double数组,需要将其放入本机代码中,还有一些生成的数组需要返回。我事先不知道输出数组的大小。为了简单起见,我将在示例中只使用一个数组。平台为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来解决此问题
  • 我相信我读到过,可以避免对内置类型的一维数组进行编组。有办法绕过元帅吗。收到了吗

请注意,我们有一个可工作的C++/Cli版本,但在第三方库代码中存在一些与本地静态相关的问题,这些问题会导致崩溃。微软将这个问题标记为WONTFIX,这就是我寻找替代品的原因。

这是可以的。完全缺乏返回错误代码的方法是非常糟糕的,当数组很大并且程序内存不足时,这会造成伤害。你遇到的严重车祸很难诊断。

复制数组并明确发布它们的需要当然不会赢得任何奖励。通过让调用者将指针传递给它自己的数组,然后只编写元素,就可以解决这个问题。然而,您需要一个协议来让调用者计算出数组需要有多大,这将需要调用该方法两次。第一个调用返回所需的大小,第二个调用完成任务。

一个样板示例是:

[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#代码中调用它,然后分配输出缓冲区
  • 再次调用非托管代码,这一次要求它填充输出缓冲区

但我认为您拒绝了这个选项,因为它不切实际。在这种情况下,您的代码是解决问题的一种非常合理的方式。事实上,我想说你做得很好。

一旦修复了调用约定的不匹配,代码在x86中的工作方式将完全相同。在C++端,调用约定是cdecl,但在C#端,它是stdcall。这在x64上并不重要,因为只有一个调用约定。但在x86下,这将是一个问题。

一些评论:

  • 您不需要使用[Out]out。后者意味着前者
  • 您可以通过分配共享堆来避免导出解除定位器。例如,C++侧的CoTaskMemAlloc,然后与C#侧的Mashal.FreeCoTaskMem解除分配

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

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