效率提升:C#向C++传递函数问题

一、情况描述

        现在我接触到的生产环境中,使用C#做前端界面,开发效率很高,和负责的界面,使用很短的时间就完成了,程序中的核心算法,使用C++写,运算快,这样能够使整个开发周期缩短,界面上比较好看,运行效率问题也能解决。

        在使用C#和C++结合的过程中就要涉及到C#调用C++dll的问题、向C++函数中传入参数、向C++传入C#的函数、C++返回参数的问题,下面我记录下我使用C#调用C++dll并将C#的函数传入C++中,遇到的主要问题以及几条注意事项;

二、开发环境

        VS版本:vs2015

        操作系统版本:win10

三、主要问题

       下面我通过一个小demo记录主要遇到的问题,这个demo使用C#的winform做了一个时钟,界面如下:

界面中的时间值是通过C++调用C#函数,传入到winform界面中;

        C#向C++传递函数主要是将C#中的委托传递到C++中,在C++中通过函数指针进行接收;

        C++头文件代码:

#ifdef CALLCTEST_EXPORTS
#define CALLCTEST_API __declspec(dllexport)
#else
#define CALLCTEST_API __declspec(dllimport)
#endif

typedef void (__stdcall *CallFunc)(char* info);

CallFunc csharpCallFunc;

extern "C" CALLCTEST_API void RegisterFunc(CallFunc callback);

extern "C" CALLCTEST_API void Run();

 C++的cpp文件:

#include "stdafx.h"
#include "callcTest.h"
#include "windows.h"
#include "stdio.h"


CALLCTEST_API void RegisterFunc(CallFunc callback) {
	csharpCallFunc = callback;
};

CALLCTEST_API void Run()
{
	while (true)
	{
		SYSTEMTIME localtime;
		GetLocalTime(&localtime);
		char *time = new char[20]();
		sprintf_s(time, 20, "%02d-%02d-%02d %02d:%02d:%02d", localtime.wYear, localtime.wMonth, localtime.wDay, localtime.wHour, localtime.wMinute, localtime.wSecond);
		csharpCallFunc(time);
		Sleep(1000);
		delete time;
	}
};

 C#通过DllImport导入C++函数:

    public delegate void CallHandler(string info);
    public static class CallCFunc
    {
        [DllImport("callcTest.dll", CallingConvention = CallingConvention.Cdecl)]
        public static extern void RegisterFunc(CallHandler call);
        [DllImport("callcTest.dll", CallingConvention = CallingConvention.Cdecl)]
        public static extern void Run();
    }

 上面中C#委托CallHandler的参数类型是string,这个在C++中可以使用C++中的char *进行对应;

C#调用C++函数传入C#委托:

CallCFunc.RegisterFunc(WriteLog);

上面就是主要的实现代码,我在下面会贴出百度网盘地址,存入源码,有感兴趣的朋友可以一起讨论交流;

四、主要注意事项:

1.在C#向C++中传入参数时(CallCFunc.RegisterFunc(callwritelog);)一定要注意,使用属性,将函数保存到一个委托类型的属性中,否则(就像我上面初始化代码中的的一种写法CallCFunc.RegisterFunc(WriteLog);)会出现以下错误:

0x00000000 处(位于 CalltestForm.exe 中)引发的异常: 0xC0000005: 执行位置 0x00000000 时发生访问冲突。

如有适用于此异常的处理程序,该程序便可安全地继续运行。

或者:

对“MotionCapture!MotionCapture.EKFRenderCallback::Invoke”类型的已垃圾回收委托进行了回调。这可能会导致应用程序崩溃、损坏和数据丢失。向非托管代码传递委托时,托管应用程序必须让这些委托保持活动状态,直到确信不会再次调用它们。

 这个问题出现的主要原因是因为,在使用C#向C++传入函数时,C++记录了函数地址,但是,当C#的GC开始工作时,认为WriteLog函数并没有被引用,因此GC将回收函数地址,造成了C++调用C#传入的函数时出现地址冲突,或者调用异常;解决办法就是通过属性将函数地址记录下来,因为属性为主窗体属性(在form类中),所以只有当窗体关闭时,才会被GC回收;

2.在C++的函数指针中使用__stdcall标记,如果不使用这个标记会出现:

Run-Time Check Failure #0 - The value of ESP was not properly saved across a function call.
  This is usually a result of calling a function declared with one calling convention with a function pointer declared with a different calling convention.

下面是我从网络上找到的__cdecl和__stdcal的解释,觉得很有用:

(1)__cdecl

即所谓的C调用规则,按从右至左的顺序压参数入栈,由调用者把参数弹出栈。切记:对于传送参数的内存栈是由调用者来维护的。返回值在EAX中。因此,对于象printf这样变参数的函数必须用这种规则。编译器在编译的时候对这种调用规则的函数生成修饰名的饿时候,仅在输出函数名前加上一个下划线前缀,格式为_functionname。
(2)__stdcall

按从右至左的顺序压参数入栈,由被调用者把参数弹出栈。_stdcall是Pascal程序的缺省调用方式,通常用于Win32 Api中,切记:函数自己在退出时清空堆栈,返回值在EAX中。  __stdcall调用约定在输出函数名前加上一个下划线前缀,后面加上一个“@”符号和其参数的字节数,格式为_functionname@number。如函数int func(int a, double b)的修饰名是_func@12

所以,从C++ dll中回调函数给C#传递数据,必须由C#函数在使用完数据后(退出函数时)自己清空堆栈!

3.如果是数组,必须用 [MarshalAs(UnmanagedType.LPArray, SizeConst = 23)]标记参数,指定为数组且标记数组长度

代码:https://pan.baidu.com/s/1Xwvh8OSg_q240mS1vC9X3w   提取密码:otfy

发布了21 篇原创文章 · 获赞 17 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/xiazhipeng1000/article/details/89293311