前言
C#调用C++函数时,如果C++函数需要传入回调函数,当C#调用时,如果直接传入函数作为参数的话,那么Release版本运行时就会出现**System.NullReferenceException: 未将对象引用设置到对象的实例。**这个异常问题。(Debug测试了一个晚上没出现不清楚为啥)这是由于直接传入函数作为参数的话,C#会自动构建一个委托传给C++,但是由于是传给非托管了,因此C#自动构建的委托没有引用,在后面垃圾回收时会将这个委托给回收了,当C++调用C#传入的委托时,会由于找不到,所以会出现异常。
一、先看测试代码
C++代码:
#include"test.h"
#include<Windows.h>
#include<unordered_set>
std::unordered_set<LogCallbackFun> LogCallbackFuns;
extern "C" void __declspec(dllexport) AddLogCallbacFun(LogCallbackFun fun)
{
if (fun != NULL)
{
LogCallbackFuns.insert(fun);
}
}
extern "C" void __declspec(dllexport) Run()
{
while (true)
{
auto iter = LogCallbackFuns.begin();
while (iter != LogCallbackFuns.end())
{
char dd[128];
sprintf_s(dd, "回调函数地址:%ul", (long)(*iter));
(*iter)(dd);
iter++;
}
Sleep(2000);
}
}
C#代码:
using System;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp1
{
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate void LogCallbackFun([In] StringBuilder msg);
class Program
{
[DllImport("Project1.dll", EntryPoint = "AddLogCallbacFun")]
public static extern void AddLogCallbacFun([MarshalAs(UnmanagedType.FunctionPtr)] LogCallbackFun fun);
[DllImport("Project1.dll", EntryPoint = "Run")]
public static extern void Run();
//代理委托,传给C++回调函数,不然会出现空引用异常
private static LogCallbackFun logCallbackFun = null;
private static void _LogCallbackFun([In] StringBuilder msg)
{
Console.WriteLine(DateTime.Now.ToString() + "" + msg.ToString());
}
static void Main(string[] args)
{
logCallbackFun = _LogCallbackFun;
//AddLogCallbacFun(logCallbackFun);
AddLogCallbacFun(_LogCallbackFun);
Task.Run(() =>
{
try
{
Run();
}
catch (Exception ex)
{
_LogCallbackFun(new StringBuilder(ex.ToString()));
}
});
//AddLogCallbacFun(logCallbackFun);
AddLogCallbacFun(_LogCallbackFun);
Console.ReadKey();
}
}
}
二、查看传入的函数地址
1.直接传入函数
当C#直接传入函数时,在C++里面看时,会发现两次传入的地址不同,在C++代码里面发现存在两个函数地址。
//AddLogCallbacFun(logCallbackFun);
AddLogCallbacFun(_LogCallbackFun);
Task.Run(() =>
{
try
{
Run();
}
catch (Exception ex)
{
_LogCallbackFun(new StringBuilder(ex.ToString()));
}
});
//AddLogCallbacFun(logCallbackFun);
AddLogCallbacFun(_LogCallbackFun);
2021/6/20 10:01:11回调函数地址:2067138764l
2021/6/20 10:01:11回调函数地址:2067141868l
2021/6/20 10:01:13回调函数地址:2067138764l
2021/6/20 10:01:13回调函数地址:2067141868l
2021/6/20 10:01:42回调函数地址:2067138764l
2021/6/20 10:01:42回调函数地址:2067141868l
2021/6/20 10:01:44回调函数地址:2067138764l
2021/6/20 10:01:44回调函数地址:2067141868l
2021/6/20 10:01:46回调函数地址:2067138764l
2021/6/20 10:01:46回调函数地址:2067141868l
2.传入委托
修改C#代码传入委托,会发现每次传入的委托一样,因此LogCallbackFuns里面只会存在一个地址。
AddLogCallbacFun(logCallbackFun);
//AddLogCallbacFun(_LogCallbackFun);
Task.Run(() =>
{
try
{
Run();
}
catch (Exception ex)
{
_LogCallbackFun(new StringBuilder(ex.ToString()));
}
});
AddLogCallbacFun(logCallbackFun);
//AddLogCallbacFun(_LogCallbackFun);
2021/6/20 10:05:12回调函数地址:1852901580l
2021/6/20 10:05:14回调函数地址:1852901580l
2021/6/20 10:05:16回调函数地址:1852901580l
2021/6/20 10:05:18回调函数地址:1852901580l
2021/6/20 10:05:20回调函数地址:1852901580l
2021/6/20 10:05:36回调函数地址:1852901580l
2021/6/20 10:05:38回调函数地址:1852901580l
2021/6/20 10:05:40回调函数地址:1852901580l
2021/6/20 10:05:42回调函数地址:1852901580l
2021/6/20 10:05:44回调函数地址:1852901580l
3.小结
如果直接传入函数,C#会自动构建一个委托去传入C++.因此会出现每次传入的地址不同,解决办法就是自己构建一个委托对象,然后传入给C++
二、测试空引用BUG
这是在Relase下测试的Debug测试不出来
1.直接传入函数
为了加速出现,我们添加5秒后手动收集垃圾的代码。
//AddLogCallbacFun(logCallbackFun);
AddLogCallbacFun(_LogCallbackFun);
Task.Run(() =>
{
try
{
Run();
}
catch (Exception ex)
{
_LogCallbackFun(new StringBuilder(ex.ToString()));
}
});
//AddLogCallbacFun(logCallbackFun);
AddLogCallbacFun(_LogCallbackFun);
Thread.Sleep(5000);
GC.Collect();//为了加速出现异常,5秒后手动收集垃圾
Console.ReadKey();
会发现5秒后,VS出现**托管调试助手 “CallbackOnCollectedDelegate”:“对“ConsoleApp1!ConsoleApp1.LogCallbackFun::Invoke”类型的已垃圾回收委托进行了回调。这可能会导致应用程序崩溃、损坏和数据丢失。向非托管代码传递委托时,托管应用程序必须让这些委托保持活动状态,直到确信不会再次调用它们。”**这个问题,继续调试后控制台的显示为:
2021/6/20 10:28:48回调函数地址:1395001548l
2021/6/20 10:28:48回调函数地址:1395004652l
2021/6/20 10:28:50回调函数地址:1395001548l
2021/6/20 10:28:50回调函数地址:1395004652l
2021/6/20 10:28:52回调函数地址:1395001548l
2021/6/20 10:28:52回调函数地址:1395004652l
2021/6/20 10:30:39System.NullReferenceException: 未将对象引用设置到对象的实例。
在 System.StubHelpers.StubHelpers.CheckCollectedDelegateMDA(IntPtr pEntryThunk)
在 ConsoleApp1.Program.Run()
在 ConsoleApp1.Program.<>c.<Main>b__4_0() 位置 D:\AnjisProject\Solution1\ConsoleApp1\Program.cs:行号 32
这就完美复现了空引用的bug。
2.直接传入委托
logCallbackFun = _LogCallbackFun;
AddLogCallbacFun(logCallbackFun);
//AddLogCallbacFun(_LogCallbackFun);
Task.Run(() =>
{
try
{
Run();
}
catch (Exception ex)
{
_LogCallbackFun(new StringBuilder(ex.ToString()));
}
});
AddLogCallbacFun(logCallbackFun);
//AddLogCallbacFun(_LogCallbackFun);
Thread.Sleep(5000);
GC.Collect();//为了加速出现异常,5秒后手动收集垃圾
Console.ReadKey();
2021/6/20 10:32:44回调函数地址:1347029196l
2021/6/20 10:32:46回调函数地址:1347029196l
2021/6/20 10:32:48回调函数地址:1347029196l
2021/6/20 10:32:50回调函数地址:1347029196l
2021/6/20 10:32:52回调函数地址:1347029196l
2021/6/20 10:32:54回调函数地址:1347029196l
2021/6/20 10:32:56回调函数地址:1347029196l
2021/6/20 10:32:58回调函数地址:1347029196l
2021/6/20 10:33:00回调函数地址:1347029196l
2021/6/20 10:33:02回调函数地址:1347029196l
2021/6/20 10:33:04回调函数地址:1347029196l
2021/6/20 10:33:06回调函数地址:1347029196l
2021/6/20 10:33:08回调函数地址:1347029196l
2021/6/20 10:33:10回调函数地址:1347029196l
2021/6/20 10:33:12回调函数地址:1347029196l
2021/6/20 10:33:14回调函数地址:1347029196l
2021/6/20 10:33:16回调函数地址:1347029196l
2021/6/20 10:33:18回调函数地址:1347029196l
2021/6/20 10:33:20回调函数地址:1347029196l
2021/6/20 10:33:22回调函数地址:1347029196l
2021/6/20 10:33:24回调函数地址:1347029196l
2021/6/20 10:33:26回调函数地址:1347029196l
2021/6/20 10:33:28回调函数地址:1347029196l
2021/6/20 10:33:30回调函数地址:1347029196l
2021/6/20 10:33:32回调函数地址:1347029196l
2021/6/20 10:33:34回调函数地址:1347029196l
2021/6/20 10:33:36回调函数地址:1347029196l
2021/6/20 10:33:38回调函数地址:1347029196l
2021/6/20 10:33:40回调函数地址:1347029196l
测试了10几秒之后发现未出现这个bug。
总结
C#直接传入函数给C++时,由于C#会自动构建一个委托在传入C++,因此每次C++收到的回调函数地址不同,并且垃圾回收机制会直接回收C#自动构建的委托,因此程序后面会出现空引用异常。
解决办法是自己主动创建一个委托进行传入,而不要直接传入函数。