通过函数指针将带有数组引用的 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 数组,它也不知道它应该从第二个参数中获取大小。所以即使你从某个地方得到了一个非空数组,你也会立即开始得到索引越界异常。
您的选择是:
- 重构本机 C++ 进程以接受输入数据而不是用于创建输入的惰性函数
或
- 将 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);
所以我有一个 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 数组,它也不知道它应该从第二个参数中获取大小。所以即使你从某个地方得到了一个非空数组,你也会立即开始得到索引越界异常。
您的选择是:
- 重构本机 C++ 进程以接受输入数据而不是用于创建输入的惰性函数
或
- 将 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);