通过函数指针将带有数组引用的 C# 委托传递给非托管代码

Passing C# delegate with array reference to unmanaged code via function pointer

所以我有一个 c++ 函数,它接受一个方法作为标准函数,它接受一个原始指针并填充它和 returns 所有值的累积,我希望将它公开给 C# API。这是我的方法:

// acc_process.h
#include <vector>
#include <numeric>

template<typename T>
static T process_accumulate(std::function<void(T*, size_t)> populate_func)
{
   std::vector<T> data(1000);
   populate_func(data.data(), data.capacity()); 
   return std::accumulate(data.begin(), data.end(), 0);
}

后跟一个 cli 包装器以将其公开给 C#:

// acc_process_cli.h

#include acc_process.h

#pragma managed(push, off)
typedef void(__stdcall *ReadCallbackDouble)(double* data, size_t data_size);
#pragma managed(pop)

delegate void PopFunctionDoubleDelegate(cli::array<double> ^ % data, size_t data_size );


public ref class ProcessDoubleCLI {

    public:

    ProcessDoubleCLI() {};

    double accumulate_cli(PopFunctionDoubleDelegate^ delegate_func);

};

实施:

// acc_process_cli.cpp
#include "acc_process_cli.h"

double ProcessDoubleCLI::accumulate_cli(PopFunctionDoubleDelegate^ delegate_func)
{    
    IntPtr FunctionPointer =
    Marshal::GetFunctionPointerForDelegate(delegate_func);
    ReadCallbackDouble func_ptr = static_cast<ReadCallbackDouble>(FunctionPointer.ToPointer());

    auto func_bind =  std::bind(
    (void (*)(double* , size_t))func_ptr, std::placeholders::_1, std::placeholders::_2);

    return process_accumulate<double>(func_bind);         
}

最后一个 C# 示例应用程序提供用于填充数据的委托函数,(例如 populate_func 用于 process_accumulate 的参数):

// test.cs

class DoubleReader {

    public static void PopulateFunctionDoubleIncr(ref double[] data, ulong size_data) {
        ulong idx = 0;
        ulong size = size_data;
        while (idx < size) {
             data[idx] = (double)idx;
             ++idx;
        }         
    }



    static void Main(string[] args) 
    {
        DoubleReader dr = new DoubleReader();
        PopFunctionDoubleDelegate func = PopulateFunctionDoubleIncr;                
        ProcessDoubleCLI process = new ProcessDoubleCLI();
        double acc_value = process.accumulate_cli(func);
    }    
}

但是在 C# 上传递给 PopulateFunctionDoubleIncr 时数组始终为 null。我这样做是否正确或者这可能吗?

注意:如果可能的话,是否也可以直接从某种托管向量引用转换为非托管std::vector引用而不是原始指针,那会更好。

GetFunctionPointerForDelegate 在您的委托类型上将有一个双指针参数。 double* 的 C# 等价物不是 ref double[],它只是 double[].

但是您有一个更大的问题,您的 C# 惰性初始化函数期望在本机内存中创建一个双精度数组,并作为指针传递以被视为 C# double[](.NET 数组)。但是 传递的缓冲区不是 .NET 数组,从来不是,也不可能是 。它没有 System.Object 元数据。它没有 Length 字段。即使 GetFunctionPointerForDelegate 足够聪明来尝试创建一个与本机 double* 相匹配的 .NET 数组,它也不知道它应该从第二个参数中获取大小。所以即使你从某个地方得到了一个非空数组,你也会立即开始得到索引越界异常。

您的选择是:

  1. 重构本机 C++ 进程以接受输入数据而不是用于创建输入的惰性函数

  1. 将 C++/CLI shim 传递给包装进程,这将同时满足 C++ 和 .NET 协定。它可以创建一个正确大小的 .NET 数组,调用 C# 委托(此处不涉及到函数指针的转换),然后将结果复制到本机缓冲区中。 std::function 的好处是传递一个拥有 gcroot<PopFunctionDoubleDelegate^> 的有状态仿函数没什么大不了的。如果你真的想变得更花哨,你可以重复使用相同的 .NET 数组来填充处理结果。

根据你的评论,我认为你正在尝试做这样的事情:

static void f(std::function<void(double*, size_t)> cb, double* data, size_t data_size) // existing code, can't change
{
    cb(data, data_size);
    std::for_each(data, data+data_size, [](double &n) { n += 1; });
}

然后您希望最终从 C# 调用 f(),如下所示:

class DoubleReader
{
    public void PopulateFunctionDoubleIncr(double[] data)
    {
        int idx = 0;
        while (idx < data.Length)
        {
            data[idx] = (double)idx;
            ++idx;
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        var data = new double[1000];

        DoubleReader dr = new DoubleReader();
        My.PopFunctionDoubleDelegate func = dr.PopulateFunctionDoubleIncr;

        My.add_cli(data, func);
    }
}

一种方法是在 C++/CLI 中使用 lambda 和 gcroot

public ref struct My
{
    delegate void PopFunctionDoubleDelegate(cli::array<double>^  data);
    static void add_cli(cli::array<double>^  data, PopFunctionDoubleDelegate^ callback);
};

class InvokeDelegate
{
    msclr::gcroot<cli::array<double>^> _data;
    msclr::gcroot<My::PopFunctionDoubleDelegate^> _delegate;

    void callback()
    {
        _delegate->Invoke(_data);
    }

public:
    InvokeDelegate(cli::array<double>^  data, My::PopFunctionDoubleDelegate^ delegate)
        : _data(data), _delegate(delegate) { }

    void invoke()
    {
        cli::pin_ptr<double> pPinnedData = &_data[0];
        double* pData = pPinnedData;

        f([&](double*, size_t) { callback(); }, pData, _data->Length);
    }
};

void My::add_cli(cli::array<double>^ data, PopFunctionDoubleDelegate^ callback)
{
    InvokeDelegate invokeDelegate(data, callback);
    invokeDelegate.invoke();
}

这里的 "magic" 是因为我们将(固定的)成员数据传递给成员函数,我们可以忽略 "unmanaged" 参数并将托管数据发送回.NET。

[我正在写一个新问题,因为这似乎已经足够不同了。 ]

正如我所说,我已经为您提供了将这些组合在一起的所有部分(假设您现在清楚您要做什么)。根据我在另一个答案中的评论,您只需要一个围绕 C 样式数组的 .NET 包装器。也许有一个很好的方法来做到这一点(SafeBuffer?),但是编写自己的方法很容易:

namespace My
{
    public ref class DoubleArray 
    {
        double* m_data;
        size_t m_size;

    public:
        DoubleArray(double* data, size_t size) : m_data(data), m_size(size) {}

        property int Length { int get() { return m_size; }}
        void SetValue(int index, double value) { m_data[index] = value; }
    };
}

现在,您将两部分连接在一起,就像之前使用 InvokeDelegate 助手 class:

class InvokeDelegate
{
    msclr::gcroot<My::PopFunctionDoubleDelegate^> _delegate;

    void callback(double* data, size_t size)
    {
        _delegate->Invoke(gcnew My::DoubleArray(data, size));
    }

public:
    InvokeDelegate(My::PopFunctionDoubleDelegate^ delegate) : _delegate(delegate) { }
    double invoke()
    {
        return process_accumulate<double>([&](double* data, size_t size) { callback(data, size); });
    }
};

My 命名空间的其余部分是:

namespace My
{
    public delegate void PopFunctionDoubleDelegate(DoubleArray^);
    public ref struct ProcessDoubleCLI abstract sealed
    {
        static double add_cli(PopFunctionDoubleDelegate^ delegate_func);
    };
}

add_cli 几乎和以前一样:

double My::ProcessDoubleCLI::add_cli(My::PopFunctionDoubleDelegate^ delegate_func)
{
    InvokeDelegate invokeDelegate(delegate_func);
    return invokeDelegate.invoke();
}

现在从 C# 使用它:

class DoubleReader
{
    public DoubleReader() { }

    public void PopulateFunctionDoubleIncr(My.DoubleArray data)
    {
        int idx = 0;
        while (idx < data.Length)
        {
            data.SetValue(idx, (double)idx);
            ++idx;
        }
    }

    static void Main(string[] args)
    {
        DoubleReader dr = new DoubleReader();
        My.PopFunctionDoubleDelegate func = dr.PopulateFunctionDoubleIncr;
        var result = My.ProcessDoubleCLI.add_cli(func);
        Console.WriteLine(result);
    }
}

编辑 因为您似乎真的想将委托作为函数指针传递,下面是如何做到这一点:

public delegate void PopFunctionPtrDelegate(System::IntPtr, int);

double My::ProcessDoubleCLI::add_cli(My::PopFunctionPtrDelegate^ delegate_func)
{
    auto FunctionPointer = System::Runtime::InteropServices::Marshal::GetFunctionPointerForDelegate(delegate_func);
    auto func_ptr = static_cast<void(*)(double*, size_t)>(FunctionPointer.ToPointer());
    return process_accumulate<double>(func_ptr);
}

但是,正如我所说,这很难在 C# 中使用,因为您的代码是

public void PopulateFunctionPtrIncr(IntPtr pData, int count)
{
}
My.PopFunctionPtrDelegate ptrFunc = dr.PopulateFunctionPtrIncr;
result = My.ProcessDoubleCLI.add_cli(ptrFunc);