當(dāng)前位置:首頁(yè)文章首頁(yè) IT學(xué)院 VC(VC++)

C++和C#之間互相調(diào)用經(jīng)驗(yàn)詳談

作者:  來源:  發(fā)布時(shí)間:2011-4-23 6:27:38  點(diǎn)擊:
先說說程序大概組織邏輯。主程序有一套公用接口(其實(shí)就是純虛類),在加載DLL時(shí)候?qū)⒋私涌趥鞯紻LL中,這樣子模塊在需要的時(shí)候就可以調(diào)用父的邏輯了,至于父調(diào)子,那就更簡(jiǎn)單了,主程序有一個(gè)純虛類,子模塊都繼承此接口,并進(jìn)行重寫,主程序按照一定的順序分別調(diào)用,這樣父與子的邏輯交互就完成了,這些對(duì)都是C++程序來說,這當(dāng)然沒問題。現(xiàn)在問題是,要嵌入.NET的類庫(kù),由此引發(fā)一系列問題。。。。。
軟件是以C++為父,DLL作為子的項(xiàng)目。
開發(fā)環(huán)境:WIN7 64BIT+VS2010+MFC+ATL+COM。
.NET環(huán)境下先以C#為例,其他的大部分一樣下,不排除做一些簡(jiǎn)單或者復(fù)雜的修改。
下面正式開始把。
1. 動(dòng)態(tài)加載 即父調(diào)子。
COM確實(shí)是好東西(他的褒與貶我們無作評(píng)論),她的語言無關(guān)性,不僅是我們實(shí)現(xiàn)動(dòng)態(tài)加載的關(guān)鍵,更是實(shí)現(xiàn)加載其他.NET類庫(kù)的核心。如VB.NET。有了她,才是這一切皆有可能。
由于.NET下的類庫(kù)(DLL),和傳統(tǒng)的WIN DLL 不太一樣,畢竟托管的東西。她一些函數(shù)對(duì)外是不可見的,但對(duì)COM可見。因?yàn)槲覀兙鸵訡OM方式定義一套接口,并把此接口當(dāng)成普通C++的純虛接口,來完成父到子的調(diào)用。
這一點(diǎn)不論在理論上、代碼上都比較簡(jiǎn)單,而且網(wǎng)上大多也是這樣子,所以我們直接上代碼。
如下為COM接口定義。
[ComVisible(true),
Guid("B86D71F4-FE07-4B60-8246-F5AE283ED2A3"),
InterfaceType(ComInterfaceType.InterfaceIsDual)
]
public interface IHMI
{
[PreserveSig, DispId(1)]
void OnCreate(int a);
[PreserveSig, DispId(2)]
void SetRect(int left, int top, int width, int height);
//其他接類似
}
[ ComVisible(true),
ClassInterface(ClassInterfaceType.AutoDual),
ProgId("xxxxxxx.xxxxxxx") //ProgId 主程序根據(jù)此,運(yùn)行時(shí)動(dòng)態(tài)創(chuàng)建。
]
C#在使用時(shí)要繼承并實(shí)現(xiàn)接口邏輯,如下類似。
public class CustomCOMClient : IHMI
{
public CustomCOMClient()
{
}
[DispId(1)]
public void OnCreate(int a)
{
//邏輯
}
[DispId(2)]
public void SetRect(int left, int top, int width, int height)
{
//邏輯
}
}
當(dāng)然了,在建項(xiàng)目時(shí),項(xiàng)目類型要為類庫(kù)。至此類庫(kù)部分已經(jīng)完畢。接下來再看看主程序如何加載,以及如何調(diào)用把。
其中在動(dòng)態(tài)創(chuàng)建時(shí),ProgId是關(guān)鍵。這一部分對(duì)搞過COM,在加上ATL的人來說,可能太簡(jiǎn)單了,‘可能’這個(gè)詞也許用的不太恰當(dāng),因?yàn)樗皇?lsquo;可能’,她確實(shí)簡(jiǎn)單。不信看代碼。
::CoInitialize(NULL);
const OLECHAR lpszProgID[]=OLESTR("xxxxxxx.xxxxxxx"); //ProgID
CComPtr m_NetCustomer;
HRESULT hr = m_NetCustomer.CoCreateInstance(lpszProgID);
if(SUCCEEDED(hr))
{
const LPCOLESTR szMember=OLESTR("OnCreate");
VARIANT v;
v.vt = VT_I4; v.lVal = 1024;
hr = m_NetCustomer.Invoke1(szMember,&v);
if(SUCCEEDED(hr))
{
}
}
::CoUninitialize();
怎么樣?沒有撒謊把,幾行代碼就把創(chuàng)建、調(diào)用搞定了。
郁悶,從C++拷出來代碼沒有格式,還的手工加。。。。
2. 回調(diào) 即子調(diào)父。
主程序肯定按照自己的邏輯順序依次調(diào)用子模塊的接口,如先創(chuàng)建、子的相關(guān)邏輯、最后銷毀。如果說在實(shí)際運(yùn)用中,子模塊完全不會(huì)在調(diào)用父的相關(guān)功能,那么此時(shí)框架已經(jīng)完全實(shí)現(xiàn)了,我們之前做的工作就是。難道不是嗎?,但應(yīng)用程序往往也有父與子相互調(diào)用,下面就來看看,子如何回調(diào)父的功能把
前面也說過,子調(diào)父往往是這樣,從父身上分離出部分代碼,重新封裝一個(gè)dll,由子靜態(tài)綁定,這步最簡(jiǎn)單、最方便。不過這顯然不是正道,讓人覺得別扭。
同時(shí)維護(hù)兩份相同功能代碼? 也許你會(huì)說,主程序從此也可以調(diào)用DLL啊,那不就一致了,你要真這樣說,我的回答是,“我只是在說明問題,不涉及到架構(gòu)問題”
還有每個(gè)子模塊都靜態(tài)綁定這個(gè)DLL?
還有你在分離這個(gè)DLL時(shí),如果依賴主程序太多,你怎么辦?
還有你能保證分離后的穩(wěn)定性嗎?回帶來其他的問題嗎?
還有你僅僅是為了滿足功能,才這樣做的?
你覺得這樣看著順眼嗎?
等等。反正我覺得是古怪之急。
接下來就要需找其他替代方案了。
先考慮下在C++中這一部分是如何實(shí)現(xiàn)的把。 父?jìng)鹘o子一個(gè)虛接口(虛類),子在適當(dāng)?shù)臅r(shí)候調(diào)用。僅此而已。讓我們把調(diào)用函數(shù)想的深入一點(diǎn)。直接看匯編代碼把。
看代碼之前,還要先簡(jiǎn)單說一下函數(shù)調(diào)用相關(guān)信息。在匯編層調(diào)用一個(gè)函數(shù)無非也就是JMP、CALL 之類的指令,若函數(shù)還有參數(shù)就是一些PUSH指令。好了知道這些就足夠了,下面看看在VC中的偽代碼。
__asm
{//類虛函數(shù)的匯編模擬調(diào)用,函數(shù)無參數(shù)、無返回值。
mov eax,xxxxx //存放函數(shù)地址
mov ecx,xxxxx //this指針
call eax //調(diào)用
}
這樣調(diào)用就完成了,其實(shí)真正的調(diào)用也如此,只不過指令多幾條而已。因?yàn)樗玫侥承┬畔ⅰ?br />好了,如果說.NET支持內(nèi)斂匯編,那我們完全可以自己模擬虛函數(shù)調(diào)用,不用在封裝什么DLL,這所有的一切都可以搞定,但可惜的時(shí),常規(guī)下內(nèi)斂匯編是不支持的。不錯(cuò),我說的是常規(guī),那非常規(guī)呢?答案是肯定的。
關(guān)于內(nèi)斂匯編網(wǎng)上也是一大片,底層思想是,在內(nèi)存開辟一段空間,并放入相應(yīng)指令,到時(shí)侯執(zhí)行這一部分邏輯即可,這樣就可以完成內(nèi)斂匯編了。
其中網(wǎng)上有一個(gè)封裝好的DLL(AsmClassLibrary.dll),提供接口編寫匯編代碼,用Reflector 查看了發(fā)現(xiàn)其最后執(zhí)行采用遠(yuǎn)程線程注入方式,對(duì)于嵌入一兩個(gè)模塊的,可以這樣做,但如果模塊很多的話,畢竟注入涉及到安全的問題,這一點(diǎn)不太好,當(dāng)然這也太另類了,我可不想應(yīng)用程序到處以這種方式來執(zhí)行。
所以我們采用Marshal.GetDelegateForFunctionPointer方式。
因?yàn)閺牡讓由现v,是不分什么語言編寫,只認(rèn)機(jī)器指令的,因此只要我們模擬的合理、正確,這一點(diǎn)是沒有問題的。
好了,現(xiàn)在我們目標(biāo)很明確,用內(nèi)斂方式在C#模擬虛函數(shù)的調(diào)用。
在給出代碼之前,也先說下思路。
根據(jù)之前所講以及常規(guī)知識(shí),以下幾點(diǎn)是必須的。
A 類對(duì)象指針,因?yàn)槲覀円獙⒋酥到oECX。
B 成員函數(shù)地址,當(dāng)然了,我們要CALL嘛。
C 參數(shù),這值是在C#中使用的。
這就是主要內(nèi)容,實(shí)現(xiàn)他們方式有很多種,以下是我的方案。
因?yàn)榻涌跁?huì)很多,因此我將this指針、函數(shù)地址都放到數(shù)組中,然后在傳遞給C#中,其實(shí)按道理說,只傳遞一個(gè)this指針就夠了,其他部分應(yīng)該在C#中實(shí)現(xiàn),但操作指針C++中比較簡(jiǎn)便,所以這部分代碼就在C++中做了。
得到this指針 太簡(jiǎn)單啦,根據(jù)虛表布局得到其地址也很簡(jiǎn)單。如下。
接口定義如下。
class CInterface
{
public:
virtual void test1( LPSTR p)
virtual void test2();
virtual void test3( int a);
};
得到this指針及成員函數(shù)地址。
CInterface *pInterface = new CInterface;
DWORD base_proc = (*((DWORD *)(pInterface))); //虛表指針
DWORD f1 = *(( DWORD *)base_proc); //第1個(gè)
DWORD f2 = *(( DWORD *)(base_proc + 4)); //第2個(gè)
DWORD f3 = *(( DWORD *)(base_proc + 8)); //第三個(gè)
到時(shí)將值賦值到SAFEARRAY 安全數(shù)組中,在傳遞到C#中。
看看在C#中時(shí)如何使用的把。當(dāng)然這一部分的內(nèi)斂、委托、開辟內(nèi)存、托管到非托管轉(zhuǎn)換時(shí)少不了的,老規(guī)矩,看代碼把。
先定義委托和內(nèi)斂。
//委托 參數(shù)分別為 this指針 成員函數(shù)地址 參數(shù)
delegate void testcall(int pthis, int pfun, int param);
byte[] codetest = {
// 0xCC,
0x8B, 0x5C, 0x24, 0x0C, //mov ebx,[esp+0Ch] 第三個(gè)參數(shù) @@
0x8B, 0x44, 0x24, 0x08, //mov eax,[esp+08h] 函數(shù)地址
0x8B, 0x4C, 0x24, 0x04, //mov ecx,[esp+04h] this 指針
0x53, //push ebx 參數(shù)入棧 @@
0xFF, 0xD0, //call eax
0xC3 // ret
};
書寫內(nèi)斂匯編當(dāng)然可以考研我們的功底啦,看看你知道不知道底層是如何實(shí)現(xiàn)的、如何入棧、出棧、傳值、傳指針、傳引用、堆棧平衡等。還有一點(diǎn),書寫匯編雖容易,但是機(jī)器指令我們并不都知道,山人自有妙計(jì),匯編代碼貼到VC中,ALT+8看反匯編,在拷貝回來即可。
以上代碼中,完成接口第三個(gè)函數(shù)調(diào)用,帶有一個(gè)整形參數(shù),并且傳值。
注釋掉@@部分完成接口第二個(gè)函數(shù)調(diào)用,無參數(shù)。
為了簡(jiǎn)便都寫在一個(gè)里面,實(shí)際運(yùn)用中,你可以按照不同格式分開。
接下來看看如何調(diào)用,,主要代碼如下。
VirtualAlloc。。。。。。之前肯定得先開辟內(nèi)存啊
Marshal.Copy(codetest, 0, handle, codetest.Length);
testcall Customer = Marshal.GetDelegateForFunctionPointer(handle, typeof(testcall)) as testcall;
int bb = 22;
Customer (fun[0], fun[4], bb);
不錯(cuò),這就是子模塊調(diào)用父相關(guān)邏輯的主要實(shí)現(xiàn)。

相關(guān)軟件

相關(guān)文章

文章評(píng)論

軟件按字母排列: A B C D E F G H I J K L M N O P Q R S T U V W X Y Z