先說說程序大概組織邏輯。主程序有一套公用接口(其實就是純虛類),在加載DLL時候將此接口傳到DLL中,這樣子模塊在需要的時候就可以調用父的邏輯了,至于父調子,那就更簡單了,主程序有一個純虛類,子模塊都繼承此接口,并進行重寫,主程序按照一定的順序分別調用,這樣父與子的邏輯交互就完成了,這些對都是C++程序來說,這當然沒問題。現在問題是,要嵌入.NET的類庫,由此引發一系列問題。。。。。
軟件是以C++為父,DLL作為子的項目。
開發環境:WIN7 64BIT+VS2010+MFC+ATL+COM。
.NET環境下先以C#為例,其他的大部分一樣下,不排除做一些簡單或者復雜的修改。
下面正式開始把。
1. 動態加載 即父調子。
COM確實是好東西(他的褒與貶我們無作評論),她的語言無關性,不僅是我們實現動態加載的關鍵,更是實現加載其他.NET類庫的核心。如VB.NET。有了她,才是這一切皆有可能。
由于.NET下的類庫(DLL),和傳統的WIN DLL 不太一樣,畢竟托管的東西。她一些函數對外是不可見的,但對COM可見。因為我們就以COM方式定義一套接口,并把此接口當成普通C++的純虛接口,來完成父到子的調用。
這一點不論在理論上、代碼上都比較簡單,而且網上大多也是這樣子,所以我們直接上代碼。
如下為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 主程序根據此,運行時動態創建。
]
C#在使用時要繼承并實現接口邏輯,如下類似。
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)
{
//邏輯
}
}
當然了,在建項目時,項目類型要為類庫。至此類庫部分已經完畢。接下來再看看主程序如何加載,以及如何調用把。
其中在動態創建時,ProgId是關鍵。這一部分對搞過COM,在加上ATL的人來說,可能太簡單了,‘可能’這個詞也許用的不太恰當,因為她不是‘可能’,她確實簡單。不信看代碼。
::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();
怎么樣?沒有撒謊把,幾行代碼就把創建、調用搞定了。
郁悶,從C++拷出來代碼沒有格式,還的手工加。。。。
2. 回調 即子調父。
主程序肯定按照自己的邏輯順序依次調用子模塊的接口,如先創建、子的相關邏輯、最后銷毀。如果說在實際運用中,子模塊完全不會在調用父的相關功能,那么此時框架已經完全實現了,我們之前做的工作就是。難道不是嗎?,但應用程序往往也有父與子相互調用,下面就來看看,子如何回調父的功能把
前面也說過,子調父往往是這樣,從父身上分離出部分代碼,重新封裝一個dll,由子靜態綁定,這步最簡單、最方便。不過這顯然不是正道,讓人覺得別扭。
同時維護兩份相同功能代碼? 也許你會說,主程序從此也可以調用DLL啊,那不就一致了,你要真這樣說,我的回答是,“我只是在說明問題,不涉及到架構問題”
還有每個子模塊都靜態綁定這個DLL?
還有你在分離這個DLL時,如果依賴主程序太多,你怎么辦?
還有你能保證分離后的穩定性嗎?回帶來其他的問題嗎?
還有你僅僅是為了滿足功能,才這樣做的?
你覺得這樣看著順眼嗎?
等等。反正我覺得是古怪之急。
接下來就要需找其他替代方案了。
先考慮下在C++中這一部分是如何實現的把。 父傳給子一個虛接口(虛類),子在適當的時候調用。僅此而已。讓我們把調用函數想的深入一點。直接看匯編代碼把。
看代碼之前,還要先簡單說一下函數調用相關信息。在匯編層調用一個函數無非也就是JMP、CALL 之類的指令,若函數還有參數就是一些PUSH指令。好了知道這些就足夠了,下面看看在VC中的偽代碼。
__asm
{//類虛函數的匯編模擬調用,函數無參數、無返回值。
mov eax,xxxxx //存放函數地址
mov ecx,xxxxx //this指針
call eax //調用
}
這樣調用就完成了,其實真正的調用也如此,只不過指令多幾條而已。因為她要得到某些信息。
好了,如果說.NET支持內斂匯編,那我們完全可以自己模擬虛函數調用,不用在封裝什么DLL,這所有的一切都可以搞定,但可惜的時,常規下內斂匯編是不支持的。不錯,我說的是常規,那非常規呢?答案是肯定的。
關于內斂匯編網上也是一大片,底層思想是,在內存開辟一段空間,并放入相應指令,到時侯執行這一部分邏輯即可,這樣就可以完成內斂匯編了。
其中網上有一個封裝好的DLL(AsmClassLibrary.dll),提供接口編寫匯編代碼,用Reflector 查看了發現其最后執行采用遠程線程注入方式,對于嵌入一兩個模塊的,可以這樣做,但如果模塊很多的話,畢竟注入涉及到安全的問題,這一點不太好,當然這也太另類了,我可不想應用程序到處以這種方式來執行。
所以我們采用Marshal.GetDelegateForFunctionPointer方式。
因為從底層上講,是不分什么語言編寫,只認機器指令的,因此只要我們模擬的合理、正確,這一點是沒有問題的。
好了,現在我們目標很明確,用內斂方式在C#模擬虛函數的調用。
在給出代碼之前,也先說下思路。
根據之前所講以及常規知識,以下幾點是必須的。
A 類對象指針,因為我們要將此值給ECX。
B 成員函數地址,當然了,我們要CALL嘛。
C 參數,這值是在C#中使用的。
這就是主要內容,實現他們方式有很多種,以下是我的方案。
因為接口會很多,因此我將this指針、函數地址都放到數組中,然后在傳遞給C#中,其實按道理說,只傳遞一個this指針就夠了,其他部分應該在C#中實現,但操作指針C++中比較簡便,所以這部分代碼就在C++中做了。
得到this指針 太簡單啦,根據虛表布局得到其地址也很簡單。如下。
接口定義如下。
class CInterface
{
public:
virtual void test1( LPSTR p)
virtual void test2();
virtual void test3( int a);
};
得到this指針及成員函數地址。
CInterface *pInterface = new CInterface;
DWORD base_proc = (*((DWORD *)(pInterface))); //虛表指針
DWORD f1 = *(( DWORD *)base_proc); //第1個
DWORD f2 = *(( DWORD *)(base_proc + 4)); //第2個
DWORD f3 = *(( DWORD *)(base_proc + 8)); //第三個
到時將值賦值到SAFEARRAY 安全數組中,在傳遞到C#中。
看看在C#中時如何使用的把。當然這一部分的內斂、委托、開辟內存、托管到非托管轉換時少不了的,老規矩,看代碼把。
先定義委托和內斂。
//委托 參數分別為 this指針 成員函數地址 參數
delegate void testcall(int pthis, int pfun, int param);
byte[] codetest = {
// 0xCC,
0x8B, 0x5C, 0x24, 0x0C, //mov ebx,[esp+0Ch] 第三個參數 @@
0x8B, 0x44, 0x24, 0x08, //mov eax,[esp+08h] 函數地址
0x8B, 0x4C, 0x24, 0x04, //mov ecx,[esp+04h] this 指針
0x53, //push ebx 參數入棧 @@
0xFF, 0xD0, //call eax
0xC3 // ret
};
書寫內斂匯編當然可以考研我們的功底啦,看看你知道不知道底層是如何實現的、如何入棧、出棧、傳值、傳指針、傳引用、堆棧平衡等。還有一點,書寫匯編雖容易,但是機器指令我們并不都知道,山人自有妙計,匯編代碼貼到VC中,ALT+8看反匯編,在拷貝回來即可。
以上代碼中,完成接口第三個函數調用,帶有一個整形參數,并且傳值。
注釋掉@@部分完成接口第二個函數調用,無參數。
為了簡便都寫在一個里面,實際運用中,你可以按照不同格式分開。
接下來看看如何調用,,主要代碼如下。
VirtualAlloc。。。。。。之前肯定得先開辟內存啊
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);
不錯,這就是子模塊調用父相關邏輯的主要實現。
3. 后話
這就是相互調用的所有部分嗎?這次答案是否定,實際上遠遠不至于此,我們此次實現的,只是最最基本的部分,尤其在參數上,我們用的最簡單的類型 int,實際使用中,對于兩者之間都存在的基本類型,還好說一點,當涉及到字符串、數組、結構體等這些類型時,真的會讓你很麻煩的,尤其是字符串,兩邊還不一樣。。。。。
其中對參數類型來說,我們用的是傳值方式,直接將值push,對于引用或者指針要把其地址push,就可以實現了,當然還是針對最基本的類型來說的。
對于字符串參數的,我用全局函數實現了一個接口(具體的可以看代碼),這樣其中大部分轉換操作,對我們就透明了,為何不自己搞?我有時間在補充進去把,這些就留給你們了,同樣你們搞出來之后要告訴我啊,這里給大家一個建議,處理字符串時,在C#中最好使用char數組,但在書寫內斂匯編時要注意,數組前面可有數組的大小,要偏移過去。
。。。。
。。。。
等把這一切都搞定之后,動態創建、嵌入VB的、C#的、WPF的以及她3D部分、硬件加速部分。。。。。。。。。
不錯,如此看來,現在才剛剛開始。。。。。。。。。。。。。
希望能給大家起到一個拋磚引玉作用。
最后附一個類型轉換的帖,供使用參考,類型轉換我就不啰嗦了。
http://topic.csdn.net/u/20090225/15/a6bc50ad-9721-4749-b189-dc4a4bc045a1.html
再附效果圖一張,圖中部分為嵌入C#的類型。
為了嵌入到父窗口上,使用了API SetParent 并且我有建了一個項目,就是封裝一些常用功能,具體看代碼把。