C#调用C++函数传入回调函数出现空引用问题


前言

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#自动构建的委托,因此程序后面会出现空引用异常。
解决办法是自己主动创建一个委托进行传入,而不要直接传入函数。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值