对于一些底层算法库,我们一般封装成c++ dll,如果需要在c#中实现调用,实际上很简单的导出c++ dll的函数即可。但如果c++ dll中有一些实时数据需要传递出来,那么一种常规做法就是在c++中设置回调函数,相当于提供一个接口,c#前端就可以利用这个接口来实时获取c++ dll中的数据
如下是一个实例:
- 首先定义c++ dll,用于模型训练,并回传训练过程中的字符串日志和结果数据。所以分别定义了一个传递日志的回调函数和一个传递数据的回调函数
// -------------------c++ dll.h中的内容-------------------
// 数据类
__declspec(dllexport) class ResultData{
public:
ResultData(float _loss, float _acc) {
loss = _loss;
acc = _acc;
}
float loss;
float acc;
};
// 声明两个函数指针类型
typedef void(*LogCallback)(const char* msg);// 日志回调函数,传递字符串
typedef void(*ProcessCallback)(ResultData res); // 进程回调函数,传递结果数值
// 定义2个全局函数指针对象:
LogCallback logCallback = nullptr;
ProcessCallback processCallback = nullptr;
// 定义注册回调函数工具
extern "C"
__declspec(dllexport) void registerCallback(LogCallback callback1, ProcessCallback callback2);
// 定义主执行函数:内部调用回调函数
extern "C"
__declspec(dllexport) void modelTrain();
- 对应c++ dll的cpp实现
// -------------------c++ dll.cpp中的内容-------------------
// 对回调注册函数的定义
extern "C"
__declspec(dllexport) void registerCallback(LogCallback callback1, ProcessCallback callback2)
{
logCallback = callback1;
processCallback = callback2;
}
void modelTrain()
{
std::string msg = "start train...";
// 调用回调函数:给主程序传递信息
if(logCallback != nullptr) logCallback(msg.c_str());
// 每秒发送1次数据,一共发送100次
for (int i = 1; i <= 10; i++)
{
ResultData result(((float)i)/10, ((float)i)/100);
if(processCallback!=nullptr) processCallback(result);
Sleep(1000);
}
msg = "finished train!";
if (logCallback != nullptr) logCallback(msg.c_str());
}
- 然后就是c#中调用的过程:假定是在c# wpf前端UI中直接显示c++ dll中产生的数据,因此需要在c#中先实现2个回调函数,并通过c#提供的回调函数注册函数,把c#的回调函数地址提供给c++ dll
// -------------------c#中使用c++ dll的回调函数-------------------
// 预定义c++ dll导入的对应数据结构
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential, CharSet = System.Runtime.InteropServices.CharSet.Ansi, Pack = 8)]
public struct ResultData
{
[MarshalAs(UnmanagedType.R4)]
public float loss;
[MarshalAs(UnmanagedType.R4)]
public float acc;
}
public partial class MainWindow : Window
{
// 从dll中导入函数
// 注意:从c++ dll导入的函数必须声明为extern static,也就是静态函数
[DllImport("mylib.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)]
public extern static void modelTrain();
[DllImport("mylib.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)]
public extern static void registerCallback(LogCallback callback1, ProcessCallback callback2);
// 声明两个函数指针类型,对应c++的回调函数类型
public delegate void LogCallback(string log);
public delegate void ProcessCallback(ResultData res);
// 定义两个函数指针对象
private LogCallback logCallback;
private ProcessCallback processCallback;
public MainWindow()
{
InitializeComponent();
// 实例化回调函数
logCallback = new LogCallback(GetLog);
processCallback = new ProcessCallback(GetResult);
// 注册回调函数
registerCallback(logCallback, processCallback);
}
private void btnStart_Click(object sender, RoutedEventArgs e)
{
this.btnStart.IsEnabled = false;
// 开始异步执行dll中的主函数
Task.Run(modelTrain);
}
//回调函数的实现
private void GetLog(string msg)
{
// 调用时该回调函数属于modelTrain这个程序所在的子线程
// 为了能够修改主线程的UI,需要跨线程操作UI,所以需要Dispatcher
this.Dispatcher.Invoke(new Action(()=>{
this.txtLog.Text += msg + "\n";
}));
}
// 回调函数的实现
private void GetResult(ResultData res)
{
// 调用时该回调函数属于modelTrain这个程序所在的子线程
// 为了能够修改主线程的UI,需要跨线程操作UI,所以需要Dispatcher
this.Dispatcher.Invoke(new Action(()=>
{
double loss = res.loss;
double acc = res.acc;
this.txtLog.Text += "loss=" + Convert.ToString(loss) + ", acc=" + Convert.ToString(acc) + "\n";
}));
}
}
enjoy it!