vlambda博客
学习文章列表

性能提升:使用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”类型的已垃圾回收委托进行了回调。这可能会导致应用程序崩溃、损坏和数据丢失。向非托管代码传递委托时,托管应用程序必须让这些委托保持活动状态,直到确信不会再次调用它们。


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