【问题标题】:How to return array of struct from C++ dll to C#如何将结构数组从 C++ dll 返回到 C#
【发布时间】:2018-11-07 02:40:54
【问题描述】:

我需要在 dll 中调用一个函数并返回一个结构数组。我事先不知道数组的大小。如何才能做到这一点?错误can not marshal 'returns value' invalid managed / unmanaged

C# 中的代码:

[DllImport("CppDll"]
public static extern ResultOfStrategy[] MyCppFunc(int countO, Data[] dataO, int countF, Data[] dataF);

在 C++ 中:

extern "C" _declspec(dllexport) ResultOfStrategy* WINAPI MyCppFunc(int countO, MYDATA * dataO, int countF, MYDATA * dataF)
{
    return Optimization(countO, dataO, countF, dataF);
}

返回结构数组:

struct ResultOfStrategy
{
bool isGood;
double allProfit;
double CAGR;
double DD;
int countDeals;
double allProfitF;
double CAGRF;
double DDF;
int countDealsF;
Param Fast;
Param Slow;
Param Stop;
Param Tp;
newStop stloss;
};

【问题讨论】:

  • 这在 C++ 中很难正确完成,当你 pinvoke 时也不会变得更好。有一个令人讨厌的内存管理问题,该数组的存储需要再次释放,使用与创建它完全相同的分配器。 pinvoke marshaller 告诉您它不想解决这个问题。标准技术是允许客户端程序调用函数两次。首先使用数组参数的空指针,然后函数所做的就是返回所需的大小。所以在第二次调用中它可以传递正确大小的数组。
  • 和往常一样,封送字符串和数组的三个问题是:谁分配内存,需要分配多少内存,谁(以及如何)释放内存。
  • @HansPassant 非常感谢您的回答,现在更清楚该做什么了。以双重通话为代价,我明白了。但是@Hanatos 说你可以让 C++ 能够通过 C# 分配内存。我对这个话题太新了,如果容易的话,请你写一个最简单的例子。
  • @Fresto 在这种情况下没用...最好使用共享分配器,例如相当于 LocalAlloc 的 Marshal.AllocHGlobal 或相当于 CoTaskMemAlloc 的 Marshal.AllocCoTaskMem
  • 一般来说,我看不到的是 C++ 将如何返回数组的长度。

标签: c# c++ marshalling


【解决方案1】:

我会给你两个答复。第一个是一种非常基本的方法。第二个是相当先进的。

给定:

C端:

struct ResultOfStrategy
{
    //bool isGood;
    double allProfit;
    double CAGR;
    double DD;
    int countDeals;
    double allProfitF;
    double CAGRF;
    double DDF;
    int countDealsF;
    ResultOfStrategy *ptr;
};

C#端:

public struct ResultOfStrategy
{
    //[MarshalAs(UnmanagedType.I1)]
    //public bool isGood;
    public double allProfit;
    public double CAGR;
    public double DD;
    public int countDeals;
    public double allProfitF;
    public double CAGRF;
    public double DDF;
    public int countDealsF;
    public IntPtr ptr;
}

请注意,我已经删除了bool,因为它在案例 2 中存在一些问题(但它适用于案例 1)......现在......

案例 1 非常基本,它会导致 .NET 封送处理程序将 C 中内置的数组复制到 C# 数组中。

我写的案例 2 是相当先进的,它试图绕过这个 marshal-by-copy 并让 C 和 .NET 可以共享相同的内存。

为了检查差异我写了一个方法:

static void CheckIfMarshaled(ResultOfStrategy[] ros)
{
    GCHandle h = default(GCHandle);

    try
    {
        try
        {
        }
        finally
        {
            h = GCHandle.Alloc(ros, GCHandleType.Pinned);
        }

        Console.WriteLine("ros was {0}", ros[0].ptr == h.AddrOfPinnedObject() ? "marshaled in place" : "marshaled by copy");
    }
    finally
    {
        if (h.IsAllocated)
        {
            h.Free();
        }
    }
}

并且我在struct 中添加了一个ptr 字段,其中包含struct(C 端)的原始地址,以查看它是否已被复制或是否是原始struct .

案例 1:

C端:

__declspec(dllexport) void MyCppFunc(ResultOfStrategy** ros, int* length)
{
    *ros = (ResultOfStrategy*)::CoTaskMemAlloc(sizeof(ResultOfStrategy) * 2);
    ::memset(*ros, 0, sizeof(ResultOfStrategy) * 2);
    (*ros)[0].ptr = *ros;
    (*ros)[0].allProfit = 100;
    (*ros)[1].ptr = *ros + 1;
    (*ros)[1].allProfit = 200;
    *length = 2;
}

和 C# 方面:

public static extern void MyCppFunc(
    [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.Struct, SizeParamIndex = 1)] out ResultOfStrategy[] ros, 
    out int length
);

然后:

ResultOfStrategy[] ros;
int length;
MyCppFunc(out ros, out length);

Console.Write("Case 1: ");
CheckIfMarshaled(ros);

ResultOfStrategy[] ros2;

.NET marshaler 知道(因为我们给了它信息)第二个参数是out ResultOfStrategy[] ros 的长度(参见SizeParamIndex?),因此它可以创建一个 .NET 数组并从 C 复制- 分配数组数据。请注意,在 C 代码中,我使用了 ::CoTaskMemAlloc 来分配内存。 .NET 想要使用该分配器分配内存,因为它会释放它。如果你使用malloc/new/???分配ResultOfStrategy[]内存,会发生不好的事情。

案例 2:

C端:

__declspec(dllexport) void MyCppFunc2(ResultOfStrategy* (*allocator)(size_t length))
{
    ResultOfStrategy *ros = allocator(2);
    ros[0].ptr = ros;
    ros[1].ptr = ros + 1;
    ros[0].allProfit = 100;
    ros[1].allProfit = 200;
}

C#端:

// Allocator of T[] that pins the memory (and handles unpinning)
public sealed class PinnedArray<T> : IDisposable where T : struct
{
    private GCHandle handle;

    public T[] Array { get; private set; }

    public IntPtr CreateArray(int length)
    {
        FreeHandle();

        Array = new T[length];

        // try... finally trick to be sure that the code isn't interrupted by asynchronous exceptions
        try
        {
        }
        finally
        {
            handle = GCHandle.Alloc(Array, GCHandleType.Pinned);
        }

        return handle.AddrOfPinnedObject();
    }

    // Some overloads to handle various possible length types
    // Note that normally size_t is IntPtr
    public IntPtr CreateArray(IntPtr length)
    {
        return CreateArray((int)length);
    }

    public IntPtr CreateArray(long length)
    {
        return CreateArray((int)length);
    }

    public void Dispose()
    {
        FreeHandle();
    }

    ~PinnedArray()
    {
        FreeHandle();
    }

    private void FreeHandle()
    {
        if (handle.IsAllocated)
        {
            handle.Free();
        }
    }
}

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate IntPtr AllocateResultOfStrategyArray(IntPtr length);

[DllImport("CplusPlusSide.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void MyCppFunc2(
    AllocateResultOfStrategyArray allocator
);

然后

ResultOfStrategy[] ros;

using (var pa = new PinnedArray<ResultOfStrategy>())
{
    MyCppFunc2(pa.CreateArray);
    ros = pa.Array;

    // Don't do anything inside of here! We have a
    // pinned object here, the .NET GC doesn't like
    // to have pinned objects around!

    Console.Write("Case 2: ");
    CheckIfMarshaled(ros);
}

// Do the work with ros here!

现在这个很有趣... C 函数从 C# 端接收一个分配器(一个函数指针)。此分配器将分配length 元素,然后必须记住已分配内存的地址。这里的技巧是,我们在 C# 端分配了一个 C 所需大小的 ResultOfStrategy[],然后直接在 C 端使用。如果ResultOfStrategy 不是 blittable,这将严重破坏(这个术语意味着您只能在ResultOfStrategy 中使用某些类型,主要是数字类型,没有string,没有char,没有bool,请参阅@987654321 @)。该代码非常先进,因为除此之外,它必须使用GCHandle 来固定.NET 数组,这样它就不会被移动。处理这个GCHandle 非常复杂,所以我必须创建一个ResultOfStrategyContainer,即IDisposable。在这个类中,我什至保存了对创建数组的引用(ResultOfStrategy[] ResultOfStrategy)。注意using 的使用。这是使用类的正确方法。

bool 和案例 2

正如我所说,虽然 bool 处理案例 1,但他们不处理案例 2...但我们可以作弊:

C端:

struct ResultOfStrategy
{
    bool isGood;

C#端:

public struct ResultOfStrategy
{
    private byte isGoodInternal;
    public bool isGood
    {
        get => isGoodInternal != 0;
        set => isGoodInternal = value ? (byte)1 : (byte)0;
    }

这行得通:

C端:

extern "C"
{
    struct ResultOfStrategy
    {
        bool isGood;
        double allProfit;
        double CAGR;
        double DD;
        int countDeals;
        double allProfitF;
        double CAGRF;
        double DDF;
        int countDealsF;
        ResultOfStrategy *ptr;
    };

    int num = 0;
    int size = 10;

    __declspec(dllexport) void MyCppFunc2(ResultOfStrategy* (*allocator)(size_t length))
    {
        ResultOfStrategy *ros = allocator(size);

        for (int i = 0; i < size; i++)
        {
            ros[i].isGood = i & 1;
            ros[i].allProfit = num++;
            ros[i].CAGR = num++;
            ros[i].DD = num++;
            ros[i].countDeals = num++;
            ros[i].allProfitF = num++;
            ros[i].CAGRF = num++;
            ros[i].DDF = num++;
            ros[i].countDealsF = num++;
            ros[i].ptr = ros + i;
        }

        size--;
    }
}

C#端:

[StructLayout(LayoutKind.Sequential)]
public struct ResultOfStrategy
{
    private byte isGoodInternal;
    public bool isGood
    {
        get => isGoodInternal != 0;
        set => isGoodInternal = value ? (byte)1 : (byte)0;
    }
    public double allProfit;
    public double CAGR;
    public double DD;
    public int countDeals;
    public double allProfitF;
    public double CAGRF;
    public double DDF;
    public int countDealsF;
    public IntPtr ptr;
}

然后

ResultOfStrategy[] ros;

for (int i = 0; i < 10; i++)
{
    using (var pa = new PinnedArray<ResultOfStrategy>())
    {
        MyCppFunc2(pa.CreateArray);
        ros = pa.Array;

        // Don't do anything inside of here! We have a
        // pinned object here, the .NET GC doesn't like
        // to have pinned objects around!
    }

    for (int j = 0; j < ros.Length; j++)
    {
        Console.WriteLine($"row {j}: isGood: {ros[j].isGood}, allProfit: {ros[j].allProfit}, CAGR: {ros[j].CAGR}, DD: {ros[j].DD}, countDeals: {ros[j].countDeals}, allProfitF: {ros[j].allProfitF}, CAGRF: {ros[j].CAGRF}, DDF: {ros[j].DDF}, countDealsF: {ros[j].countDealsF}");
    }

    Console.WriteLine();
}

【讨论】:

  • 哦,伙计。非常感谢!我很惊讶在英语 stackoverflow 中给出了非常详细的答案,即使是在如此困难的问题上。我将详细研究第二个选项,第一个选项出现了,一切正常。再次非常感谢!
  • @Fresto 因为在 cut-n-paste 期间我忘记了一行 :-) 我已经添加了它。搜索public static extern void MyCppFunc(。有一个特殊的注释 SizeParamIndex` 可以发挥作用。
  • 非常感谢)
  • 抱歉这个问题。但是,为什么,如果我调用我的函数一次,那么从 C++ 函数返回时结果是正确的。但是如果我用不同的输入值多次调用这个代码using(var container ...) {...},那么第二次值不正确?是内存相交的原因吗?
  • @Fresto 在这里测试...在这里正常工作。你确定你在 C# 和 C 端都有相同的结构吗?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-06-28
  • 2011-06-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多