CLR Interop簡而言之是讓非託管代碼與託管代碼之間可以相互調用的技術。這項技術可以使開發人員重用已有的託管或非託管組建,並根據自己的需要,權衡託管代碼的簡易性與非託管代碼的靈活性,選擇適合自己實際情況的編程語言,而不用過多考慮重用的組件是用哪種語言開發的。Interop中文的意思是互通性,既然是互通,代碼的調用就有兩種不同的方向。本文所要講述的是使用COM Interop技術在非託管代碼方如何調用託管代碼。
1. 創建託管服務器
首先讓我們在Visual Studio 2008創建一個C#的Class Library(類庫)項目,取名爲MyManagedServer,在該項目中,我們要聲明並實現一個接口。
接口聲明代碼如下:
爲了說明簡單,該接口中只有一個方法,用於打印一些信息。其中的ComVisible屬性至關重要,當它的值爲true時,該接口才對COM可見。
using System; using System.Runtime.InteropServices; namespace MyManagedServer { [ComVisible(true), Guid("79EDDA1C-F243-47C5-8954-5DEF01FA3D44"), InterfaceType(ComInterfaceType.InterfaceIsDual)] public interface IManagedFooClass { [PreserveSig, DispId(1)] void PrintFoo(); } }
接下來是實現該接口的類:
using System; using System.Runtime.InteropServices; namespace MyManagedServer { [ComVisible(true), ClassInterface(ClassInterfaceType.AutoDual), ProgId("MyManagedServer.ManagedFooClass") ] public class CustomCOMClient : IManagedFooClass, IManagedBarClass { public CustomCOMClient() { } #region IManagedFooClass Members [DispId(1)] public void PrintFoo() { Console.WriteLine("in MyManagedServer: CustomCOMClient.PrintFoo()"); } #endregion } }
這裏我們給這個類的ProgId屬性賦一個值。等會兒在註冊組件的時候,註冊表中將會增加一個鍵值,將ProgId和runtime爲我們自動生成的CLSID關聯起來。
2. 爲COM Interop註冊託管服務組件
註冊組件可以用Visual Studio幫我們自動註冊,也可以在命令行下手動輸入命令。若要使用Visual Studio來幫我們註冊組件,只需在項目屬性頁(鼠標右鍵項目名稱,在下拉菜單中選擇“Properties(屬性)”)的Build標籤頁中把Register for COM Interop項打上勾,然後再build項目就可以了。如下圖所示:
此外,我們也可以先build項目,然後通過命令行的方式註冊組件。只需要使用regasm.exe在VS2008命令行下輸入如下命令即可:
regasm assemblyname.dll /tlb /codebase
該命令會爲我們註冊組件,生成並註冊對應的type library文件。其中assemblyname.dll是項目構建生成的程序集文件。
3. 創建非託管客戶端
使用託管語言創建並註冊了組建之後,我們就要使用非託管語言來嘗試通過COM Interop調用組建中的方法了。首先,在Visual Studio 2008中創建一個Visual C++ Win32 Console Application,取名爲MyNatvieClient,並將組建生成tlb文件拷貝至該項目的源代碼目錄中。然後在MyNativeClient.cpp中輸入如下代碼:
#include "stdafx.h" #import "mscorlib.tlb" no_namespace #import "MyManagedServer.tlb" no_namespace int _tmain(int argc, _TCHAR* argv[]) { ::CoInitialize(NULL); // Get CLSID for CoCreateInstance const OLECHAR lpszProgID[] = OLESTR("MyManagedServer.ManagedFooClass"); CLSID clsid; HRESULT hr = CLSIDFromProgID(lpszProgID, &clsid); if(SUCCEEDED(hr)) { printf("CLSIDFromProgID Succeeded /n"); IDispatch* ppv = 0; HRESULT hr = CoCreateInstance(clsid, NULL, CLSCTX_ALL, IID_IDispatch, (void**)&ppv); if(SUCCEEDED(hr)) { printf("CoCreateInstance Succeeded /n"); // Get DispId for Invoke DISPID dispid; const LPOLESTR szMember = OLESTR("PrintFoo"); HRESULT hr = ppv->GetIDsOfNames(IID_NULL, (LPOLESTR*)&szMember,1,LOCALE_SYSTEM_DEFAULT,&dispid); if(SUCCEEDED(hr)) { printf("GetIDsOfNames Succeeded /n"); // There's no parameter to pass DISPPARAMS dispParams = {0}; VARIANT vtResult; UINT dwArgErr; HRESULT hr = ppv->Invoke(dispid,IID_NULL,NULL,DISPATCH_METHOD,&dispParams,&vtResult,NULL,&dwArgErr); if(SUCCEEDED(hr)) { printf("Invoke Succeeded /n"); } } ppv->Release(); } } return 0; }
該代碼主要做了以下幾件事情:
a. 調用CoInitialize進行初始化。
b. 調用CLSIDFromProgId獲得對象的CLSID,因爲接下來的函數將通過CLSID來創建實例。
c. 通過CoCreateInstance創建對象實例。這裏創建的是一個IDispatch的對象實例。
d. 調用IDispatch::GetIDsOfNames以獲得將要調用的方法的DispID,供接下來的函數使用。
e. 使用IDispatch::Invoke來調用方法。
在import type library的時候我們不僅import了組建的tlb文件,同時還import了mscorlib.tlb以避免生成的臨時的tlh文件中一些類型找不到的情況。(有關此方面的問題可以參考我們團隊開發人員張羿撰寫的《#import從.NET DLL生成的tlb的神祕錯誤》)
編譯通過後運行結果,可看到命令行中打印出如下信息:
CLSIDFromProgID Succeeded
CoCreateInstance Succeeded
GetIDsOfNames Succeeded
in MyManagedServer: CustomCOMClient.PrintFoo()
Invoke Succeeded
注:本文所示代碼只作爲實例使用。本文作者不對因代碼使用不當而造成的問題負責。