【问题标题】:Marshaling a return array from C++ to C#将返回数组从 C++ 编组到 C#
【发布时间】:2020-07-06 12:13:12
【问题描述】:

我的 C++ 应用程序中有一个数组,我想使用它的指针从它创建一个 C# 数组委托而不复制。 所以我可以让它从 C# 到 C++ 工作,C++ 访问在 C# 中定义的同一个数组,但不能让它反向工作。

C#代码:

   [DllImport("CppDll.dll",CallingConvention=CallingConvention.Cdecl)]
   [return: MarshalAs(UnmanagedType.LPArray)]
   public static extern int[] GetCppArray();

C++ 代码:

    int test_data[5] = { 12,60,55,49,26 };

    extern "C" __declspec(dllexport) int* GetCppArray()
    {
        return test_data;
    }

在 C# 中使用:

int[] cpparray = NativeLib.GetCppArray();

我得到了这个错误:

System.Runtime.InteropServices.MarshalDirectiveException: '不能 marshal 'return value': 无效的托管/非托管类型组合。'

我知道我可以使用内存写入器通过数组指针地址直接写入 C++ 内存。 如果在参数中使用相同的 MarshalAs(UnmanagedType.LPArray) 并将 C# 数组传递给 c++,它可以工作,但为什么它在相反的操作中不起作用?

注意:我的数据量很大,我真的不能在这里使用任何副本。

【问题讨论】:

  • 是.NET framework还是.NET core?
  • 使用 .NET Framework,如果您不想复制,则必须 1) 从 GetCppArray 返回 IntPtr 而不是 int[] 和 2) 使用 unsafe 关键字并强制转换返回 @ 987654329@as int*,有点像:stackoverflow.com/a/28279003/403671 如果你不想要unsafe 代码,你将不得不以某种方式复制数组。
  • @SimonMourier 我不想使用不安全的
  • 所以你必须以某种方式复制(使用 Array.Copy)。不能吃你的蛋糕。它被称为“托管”。
  • 另一种选择是使用C++\CLI。我更新了我的答案,向您展示了一个示例。没有数据复制,没有不安全,并且您使用 C++\CLI 包装器的方式与使用简单的 C# 数组的方式非常相似。

标签: c# c++ pinvoke dllimport


【解决方案1】:

你应该先完整阅读这篇文章:https://docs.microsoft.com/en-us/dotnet/framework/interop/default-marshaling-for-arrays#unmanaged-arrays

我还发现了这个相关的 QA:How to create and initialize SAFEARRAY of doubles in C++ to pass to C#

您的 GetCppArray 函数只返回一个指针 - 它不返回一个自描述的“安全”数组,而 .NET 中的数组包含长度和排名(维度)信息,因此您肯定需要修改 C++ 代码正确地做到这一点。

第一个选项是将数组作为COM风格的安全数组返回,这是通过the SAFEARRAY( typename ) macro完成的 - 它必须作为参数传递,而不是返回值。 p>

在 C++ 中使用 COM 安全数组有两种主要方法:使用 Win32 函数,如 SafeArrayCreate - 正确使用会很痛苦,或者使用 the ATL CComSafeArray

(免责声明:我通过查看 API 参考编写了这段代码,我还没有测试过 - 我什至不知道它是否会编译)。

// C++ code for SafeArrayCreate:

#include <comdef.h>
int test_data[5] = { 12, 60, 55, 49, 26 };

extern "C" __declspec(dllexport) HRESULT GetCppArray( [out] SAFEARRAY( int )** arr )
{
    SAFEARRAYBOUND bounds;
    bounds.lLbound   = 0;
    bounds.cElements = sizeof(test_data);

    *arr = SafeArrayCreate(
        VT_I4,  // element type
        1,      // dimensions
        &bounds 
    );
    if( !arr ) {
        // SafeArrayCreate failed.
        return E_UNEXPECTED;
    }

    int* arrPtr;
    HRESULT hr = SafeArrayAccessData( *arr, &arrPtr );
    if( !SUCCEEDED( hr ) ) {
         hr = SafeArrayDestroy( arr );
         // After SafeArrayDestory, if `hr != S_OK` then something is really wrong.
         return E_UNEXPECTED;
    }

    for( size_t i = 0; i < sizeof(test_data); i++ ) {
        *arrPtr[i] = test_data[i];
    }

    hr = SafeArrayUnaccessData( *arrPtr );
    if( !SUCCEEDED( hr ) ) {
         hr = SafeArrayDestroy( arr );
         return E_UNEXPECTED;
    }

    return S_OK;
}

然后需要更新 C# 代码以声明它返回 SafeArray:

// HRESULT is best represented as a UInt32 instead of Int32.

[DllImport( "CppDll.dll", CallingConvention = CallingConvention.Cdecl )]
public static extern UInt32 GetCppArray(
    [MarshalAs( UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_I4 )] out Int32[] arr
);

【讨论】:

  • 你刚刚发布了一个你没有测试的代码?我相信它比这更简单我不想参与 COM 接口,我可能会使用导致 COM 问题的自定义结构
【解决方案2】:

也许您可以使用Marshal.ReadInt32() 来处理您的数据?

[DllImport("CppDll.dll",CallingConvention=CallingConvention.Cdecl)]
public static extern IntPtr GetCppArray();

var cpparray = NativeLib.GetCppArray();
var test_data_2 = Marshal.ReadInt32(cpparray, 2 * sizeof(int));
Console.WriteLine(test_data_2); // 55

看起来Span&lt;T&gt; 可能与您想要的非常接近;有一个非常合适的示例,尽管它仍然需要unsafe

 Span<int> cpparray;
 unsafe
 {
     cpparray = new Span<int>((int*)NativeLib.GetCppArray(), 5);
 } 
 Console.WriteLine(cpparray[2]); // 55

【讨论】:

  • 我提到了禁止复制。我想直接使用数组作为统一网格顶点数组...所以不能使用这个选项。
  • 我知道我可以使用它甚至使用内存读取器来读取一个值 -> 复制到另一个新值 我在我的问题中问的是如何在不复制的情况下将它作为 C# 数组获取。我需要该 C# 数组,例如转换 C++ 数组指针并从中获取 C# 数组,可以反过来将 C# 数组指针传递给 C++ 并将其作为 C++ 数组
  • 我不想使用不安全的代码,没有跨度从 C++ 中获取数组非常容易......我想编组返回我想学习它,看看我的代码有什么问题但是谢谢
  • 还有一件事,你把它作为 span 我想要 C# Array ,在 Unity 中用作 C# 数组属性
  • @DraculaBytePair 无法使用 unsafe 从 C# 直接访问非托管内存。有多种方法可以解决这个问题,但如果没有关于您的实际问题的更多详细信息,很难知道有什么建议。
【解决方案3】:

我建议您使用C++/CLI,从而修改了我的原始答案。你说你使用.NET Framework,所以C++/CLI是一个不错的选择。通过使用它,您无需复制 C++ 指针。只需在 C++ 指针上创建一个 C++/CLI 包装类。 下面是一个例子。

C++ 代码

functions.h

#pragma once
#ifdef LIBRARY_EXPORTS
#define API __declspec(dllexport)
#else
#define API __declspec(dllimport)
#endif

#ifdef __cplusplus
extern "C"
{
#endif

    API int* GetCppArray();
    API int GetCppArraySize();
    API void DeleteCppArray(int* vec);

#ifdef __cplusplus
}
#endif

functions.cpp

#include "pch.h"
#include "functions.h"

const int vecSize = 10000;

int* GetCppArray()
{
    int* vec = new int[vecSize];
    for (int i = 0; i < vecSize; ++i)
    {
        vec[i] = i * 2;
    }

    return vec;
}

int GetCppArraySize()
{
    return vecSize;
}

void DeleteCppArray(int* vec)
{
    delete[] vec;
}

C++/CLI 代码

Wrapper.h

#pragma once

using namespace System;

namespace Wrapper 
{
    public ref class ArrayWrapper : public IDisposable
    {
    private:
        int* values;
        int size;
        bool isDisposed;

    public:
        ArrayWrapper();
        ~ArrayWrapper();
        !ArrayWrapper();

        property int Size
        {
            int get();
        }

        property int default[int]
        {
            int get(int index);
            void set(int index, int value);
        }
    };
}

Wrapper.cpp

#include "pch.h"
#include "Wrapper.h"

#include "../Library/functions.h"

namespace Wrapper
{
    ArrayWrapper::ArrayWrapper()
    {
        this->values = GetCppArray();
        this->size = GetCppArraySize();
        this->isDisposed = false;
    }

    ArrayWrapper::~ArrayWrapper()
    {
        if (this->isDisposed == true)
        {
            return;
        }

        this->!ArrayWrapper();
        this->isDisposed = true;
    }

    ArrayWrapper::!ArrayWrapper()
    {
        DeleteCppArray(this->values);
        this->values = nullptr;
        this->size = 0;
    }

    int ArrayWrapper::Size::get()
    {
        return this->size;
    }

    int ArrayWrapper::default::get(int index)
    {
        if (index < 0 || index >= this->size)
        {
            throw  gcnew Exception("Invalid index");
        }
        return this->values[index];
    }

    void ArrayWrapper::default::set(int index, int value)
    {
        if (index < 0 || index >= this->size)
        {
            throw  gcnew Exception("Invalid index");
        }

        this->values[index] = value;
    }
}

C# 代码

using System;
using Wrapper;

namespace TestApp
{
    class Program
    {
        static void Main(string[] args)
        {
            ArrayWrapper vector = new ArrayWrapper();

            for (int i = 0; i < vector.Size; i++)
            {
                Console.Write($"{vector[i].ToString()} ");
            }
            Console.WriteLine();
            Console.WriteLine("Done");

            int index = vector.Size / 2;
            Console.WriteLine($"vector[{index.ToString()}]={vector[index].ToString()}");
            vector[index] *= 2;
            Console.WriteLine($"vector[{index.ToString()}]={vector[index].ToString()}");
        }
    }
}

【讨论】:

    猜你喜欢
    • 2013-07-12
    • 1970-01-01
    • 1970-01-01
    • 2021-06-08
    • 1970-01-01
    • 2022-01-14
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多