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

Wil*_*ert 7 c# c++ pinvoke interop marshalling

我已经在这里这里阅读了C++ Interop与P/Invoke的各种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);
    }
Run Code Online (Sandbox Code Playgroud)

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

Han*_*ant 6

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

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

样板示例如下:

[DllImport("foo.dll")]
private static int ReturnData(double[] data, ref int dataLength);
Run Code Online (Sandbox Code Playgroud)

以及样本用法:

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);
}
Run Code Online (Sandbox Code Playgroud)

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


Dav*_*nan 2

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

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

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

一旦修复了调用约定不匹配的问题,代码将在 x86 中正常工作。在 C++ 方面,调用约定是cdecl,但在 C# 方面是stdcall。这在 x64 上并不重要,因为只有一种调用约定。但在 x86 下就会出现问题。

一些评论:

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