天天看點

效率提升: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++傳遞函數問題一、情況描述二、開發環境三、主要問題四、主要注意事項:

界面中的時間值是通過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調用約定在輸出函數名前加上一個下劃線字首,後面加上一個“@”符号和其參數的位元組數,格式為[email protected]。如函數int func(int a, double b)的修飾名是[email protected]

是以,從C++ dll中回調函數給C#傳遞資料,必須由C#函數在使用完資料後(退出函數時)自己清空堆棧!

3.如果是數組,必須用 [MarshalAs(UnmanagedType.LPArray, SizeConst = 23)]标記參數,指定為數組且标記數組長度

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