托管代碼調用非托管代碼,.NET提供了P/Invoke(平台調用)方式,它作為.NET的基礎出現在各類書籍和網絡資源上,這裡不再討論。那麼非托管代碼如何去調用托管代碼呢?我們知道,一個托管應用程序首先被操作系統啟動,然後由操作系統調用CLR來托管該程序。那麼.NET框架到底以什麼方式讓操作系統來認識它並且可以啟動它呢?微軟實際將其作為COM服務器實現在一個DLL中,並提供了標准的COM接口。既然是COM服務,也就意味著普通的非托管程序也可以調用CLR來運行托管代碼,把這種調用方式叫做寄宿,把調用CLR的非托管程序叫做宿主。宿主程序不僅可以調用CLR,還可以通過它來進行內存管理、垃圾回收管理、策略管理、事件管理以及線程控制等高級管理。
1.1 核心組件MSCOREE.DLL
MSCOREE.DLL負責選擇.NET版本、調用和初始化CLR等工作。非托管程序想要啟動CLR也必須引用MSCOREE.DLL,利用它的導出函數加載托管代碼和進行定制CLR等操作。既然現在的焦點集中在MSCOREE.DLL,下面一同來看它的廬山真面目。
查看MSCOREE.DLL的頭文件
對於MSCOREE.DLL文件,我們沒有辦法查看它的源代碼,但是FrameWork SDK提供了mscoree.h文件,可以到相關版本的SDK安裝目錄中查看該文件。由於該文件很大,這裡就不貼出源代碼進行分析了。接下來會對相關的接口和函數做介紹。
MSCOREE.DLL的導出函數
在命令行啟動DumpBin.exe,執行以下代碼查看MSCOREE.DLL的導出函數列表:
C:\Windows\ystem32>dumpbin /exports MSCOREE.DLL
結果如圖1-3所示。
圖1-3 輸出MSCOREE.DLL的導出函數列表
從運行的結果可以知道,MSCOREE.DLL中的導出函數有100多個,所有這些函數都可以被公開調用,而且在MSDN上可以找到每個函數的解釋。這裡只介紹其中幾個常用的函數。
CorBindToRuntimeEx函數
該函數由宿主調用來加載CLR,該函數的定義如代碼清單1-4所示。
代碼清單1-4 CorBindToRuntimeEx函數定義
HRESULT CorBindToRuntimeEx (
[in] LPWSTR pwszVersion,
[in] LPWSTR pwszBuildFlavor,
[in] DWORD startupFlags,
[in] REFCLSID rclsid,
[in] REFIID riid,
[out] LPVOID* ppv
);
代碼中的CorBindToRuntimeEx函數需要6個參數,每個參數的定義如下:
pwszVersion。一個字符串,描述要加載的 CLR 的版本。
pwszBuildFlavor。一個字符串,指定是加載CLR 的服務器版本還是工作站版本。有效值為:svr 和 wks。服務器版本已經過優化,可以利用多個處理器進行垃圾回收,而工作站版本則針對單處理器計算機上運行的客戶端應用程序進行了優化。
startupFlags。STARTUP_FLAGS 枚舉值的組合。這些標志控制並發垃圾回收、非特定於域的代碼以及 pwszVersion 參數的行為。如果未設置標志,則默認值為一個域。
rclsid。實現 ICorRuntimeHost 接口的 coclass 的 CLSID。支持的值為 CLSID_CorRuntimeHost或 CLSID_CLRRuntimeHost。
riid。請求自 rclsid 接口的 IID。支持的值為 IID_ICorRuntimeHost 或 IID_ICLRRuntimeHost。
ppv。返回的指向 riid 的接口指針。
提示使用CorBindToCurrentRuntime函數並使用存儲在 XML文件中的版本信息可以將公共語言運行庫 (CLR) 加載到進程中,還可以使用CorBindToRuntimeByCfg 函數從 XML文件中讀取的版本信息將公共語言運行庫 (CLR) 加載到進程中。
CorExitProcess函數
該函數用來關閉當前的非托管進程,定義如代碼清單1-5所示。
代碼清單1-56 CorExitProcess 函數定義
void STDMETHODCALLTYPE CorExitProcess (
[out] int exitCode
);
其中,參數exitCode表示一個指定進程退出代碼的整數。
ClrCreateManagedInstance 函數
該函數用來創建指定托管類型的實例,定義如代碼清單1-6所示。
代碼清單1-6 ClrCreateManagedInstance 函數定義
STDAPI ClrCreateManagedInstance (
[in] LPCWSTR pTypeName,
[in] REFIID riid,
[out] void **ppObject
);
代碼中函數ClrCreateManagedInstance的各參數定義如下:
pTypeName。一個指向所請求的實例類型名稱的指針。
riid。所請求實例類型的 IID。
**ppObject。一個指向指針的指針,它指向的指針指向調用方請求的托管類型實例。
CallFunctionShim函數
該函數用來調用指定庫中具有指定名稱和參數的函數。定義如代碼清單1-7所示。
代碼清單1-7 CallFunctionShim 函數定義
HRESULT CallFunctionShim (
[in] LPCWSTR szDllName,
[in] LPCSTR szFunctionName,
[in] LPVOID lpvArgument1,
[in] LPVOID lpvArgument2,
[in] LPCWSTR szVersion,
[in] LPVOID pvReserved
);
代碼中函數CallFunctionShim的各參數定義如下:
szDllName。包含函數的庫的名稱。
szFunctionName。函數的名稱。
lpvArgument1。要傳遞到函數的第一個參數。
lpvArgument2。要傳遞到函數的第二個參數。
szVersion。包含函數的庫的版本。
pvReserved。留作未來使用。在此參數中傳遞零。
更多的導出函數讀者可自行操作獲得它們的名稱,然後查閱MSDN來進一步了解。
MSCOREE.DLL宿主接口
宿主接口使宿主能夠對運行庫的更多方面進行控制,從而能夠在CLR 和宿主的執行模型之間進行更緊密的集成。在.NET Framework 1 版中,宿主模型使非托管宿主能夠將CLR 加載到進程中、配置某些設置以及接收事件通知。但在通常情況下,宿主和CLR 可以在該進程中獨立運行。通過.NET Framework 2.0 版及更高版本中新的抽象層,宿主可以提供當前由 Win32 程序集中的類型提供的多種資源,並擴展了宿主可以配置的功能集。
MSCOREE.DLL同樣提供了多個宿主接口,這裡只介紹較為重要的幾個,更多的內容請讀者參考MSDN。
ICorRuntimeHost 接口
該接口是調用CLR要用到的第一個接口,它負責初始化工作。該接口由上面提到的CorBindToRuntimeEx函數返回,其定義如代碼清單1-8所示。
代碼清單1-8 ICorRuntimeHost接口定義
interface ICLRRuntimeHost : IUnknown {
HRESULT ExecuteApplication (
[in] LPCWSTR pwzAppFullName,
[in] DWORD dwManifestPaths,
[in] LPCWSTR *ppwzManifestPaths,
[in] DWORD dwActivationData,
[in] LPCWSTR *ppwzActivationData,
[out] int *pReturnValue
);
HRESULT ExecuteInAppDomain (
[in] DWORD appDomainId,
[in] FExecuteInDomainCallback pCallback,
[in] void* cookie
);
HRESULT ExecuteInDefaultAppDomain (
[in] LPCWSTR pwzAssemblyPath,
[in] LPCWSTR pwzTypeName,
[in] LPCWSTR pwzMethodName,
[in] LPCWSTR pwzArgument,
[out] DWORD *pReturnValue
);
HRESULT GetCLRControl (
[out] ICLRControl **pCLRControl
);
HRESULT GetCurrentAppDomainId (
[out] DWORD *pdwAppDomainId
);
HRESULT SetHostControl (
[in] IHostControl *pHostControl
);
HRESULT Start();
HRESULT Stop();
HRESULT UnloadAppDomain (
[in] DWORD dwAppDomainId
[in] BOOL fWaitUntilDone
);
};
對代碼清單1-8中的各方法說明如下:
q ICLRRuntimeHost::ExecuteApplication方法
一個字符串,描述要加載基於清單的 ClickOnce 部署方案中用於指定要在新域中激活的應用程序。
q ICLRRuntimeHost::ExecuteInAppDomain 方法
指定要在其中執行指定托管代碼的 AppDomain。
q ICLRRuntimeHost::ExecuteInDefaultAppDomain 方法
調用指定程序集中屬於指定類型的指定方法。
q ICLRRuntimeHost::GetCLRControl 方法
獲取一個ICLRControl類型的接口指針,宿主可以使用該類型自定義公共語言運行庫 (CLR) 的各個方面。
q ICLRRuntimeHost::GetCurrentAppDomainId 方法
獲取當前正在執行的AppDomain的數字標識符。
q ICLRRuntimeHost::SetHostControl 方法
設置主機控制接口。在調用 Start 之前必須調用SetHostControl。
q ICLRRuntimeHost::Start 方法
將CLR 初始化到進程中。
q ICLRRuntimeHost::Stop 方法
使運行庫停止代碼的執行。
q ICLRRuntimeHost::UnloadAppDomain 方法
卸載與指定的數字標識符對應的 AppDomain。
ICLRGCManager接口
該接口提供允許宿主與公共語言運行庫的垃圾回收系統進行交互的方法。該接口的定義如代碼清單1-9所示。
代碼清單1-9 ICLRGCManager接口定義
interface ICLRGCManager : IUnknown {
HRESULT Collect (
[in] LONG Generation
);
HRESULT GetStats (
[in, out] COR_GC_STATS *pStats
);
HRESULT SetGCStartupLimits (
[in] DWORD SegmentSize,
[in] DWORD MaxGen0Size
);
};
對代碼清單1-10中ICLRGCManager接口的各方法說明如下:
q ICLRGCManager::Collect方法
為指定的生成強制執行垃圾回收。
q ICLRGCManager::GetStats方法
獲取有關垃圾回收系統的一組當前統計信息。
q ICLRGCManager::SetGCStartupLimits方法
設置垃圾回收段的大小和垃圾回收系統零代的最大大小。
IHostControl接口
該接口提供一些方法,以配置程序集的加載和確定宿主支持的宿主接口。該接口的定義如代碼清單1-10所示。
代碼清單1-10 IHostControl接口定義
interface IHostControl : IUnknown {
HRESULT GetHostManager (
[in] REFIID riid,
[out, iid_is(riid)] IUnknown** ppObject
);
HRESULT SetAppDomainManager (
[in] DWORD dwAppDomainID,
[in] IUnknown* pUnkAppDomainManager
);
};
對代碼清單1-10中IHostControl接口中的方法說明如下:
q IHostControl::GetHostManager 方法
獲取一個接口指針,該指針指向宿主對具有指定 IID 接口的實現。
q IHostControl::SetAppDomainManager 方法
通知宿主已創建了一個應用程序域。
其他接口
其他接口這裡不詳細介紹,讀者可根據需要按照下面的簡介自行查閱相關的文檔。
IActionOnCLREvent提供為已注冊的事件執行回調的方法。
IApartmentCallback提供用於在單元內進行回調的方法。
IAppDomainBinding提供用於設置運行時配置的方法。
ICatalogServices提供用於編錄服務的方法。(此接口支持.NET Framework 基礎結構,但不應在代碼中直接使用。)
ICLRAssemblyIdentityManager提供支持宿主和 CLR 之間就程序集問題進行通信的方法。
ICLRAssemblyReferenceList管理由CLR(而非宿主)加載的程序集的列表。
ICLRControl提供一些方法,以便宿主可以獲取對CLR的訪問權限並對CLR的各個方面進行配置。
ICLRDebugManager提供使宿主能夠將一組任務與某個標識符及友好名稱關聯起來的方法。
ICLRErrorReportingManager提供使宿主能夠為錯誤報告配置自定義堆轉儲的方法。
ICLRHostBindingPolicyManager提供允許宿主計算並傳達程序集策略信息中的更改的方法。
ICLRHostProtectionManager使宿主能夠阻止特定的托管類、方法、屬性和字段在部分受信任的代碼中運行。
ICLRIoCompletionManager實現使宿主能夠向 CLR 通知指定 I/O 請求的狀態的回調方法。
ICLRMemoryNotificationCallback使宿主能夠使用與 Win32 CreateMemoryResourceNotification 函數方法類似的方法報告內存壓力情況。
ICLROnEventManager提供使宿主能夠為 CLR 事件注冊和注銷回調的方法。
ICLRPolicyManager提供使宿主能夠指定在出現故障和超時的情況下采取的策略操作的方法。
ICLRProbingAssemblyEnum提供方法,這些方法使宿主能夠使用 CLR 內部的程序集標識信息來獲取該程序集的探測標識,而無需創建或了解該標識。
ICLRReferenceAssemblyEnum提供方法,這些方法使宿主能夠對文件或流通過 CLR 內部的程序集標識數據引用的一組程序集進行操作,而無需創建或了解這些標識。
ICLRSyncManager提供方法,以便讓宿主在其同步實現中獲取有關請求任務的信息並進行死鎖檢測。
ICLRTask提供方法,這些方法使宿主能夠向 CLR 發出請求,或者向 CLR 提供與關聯的任務有關的通知。
ICLRTaskManager提供方法,這些方法使宿主能夠顯式請求 CLR 創建一個新任務,獲取當前正在執行的任務,以及設置該任務的地理語言和區域性。
ICLRValidator提供用於驗證可移植可執行 (PE) 映像和報告驗證錯誤的方法。
ICorConfiguration提供用於配置 CLR 的方法。
ICorThreadpool提供用於訪問線程池的方法。
IDebuggerInfo提供用於獲取調試服務狀態信息的方法。
IDebuggerThreadControl提供方法,用於向宿主發出有關通過調試服務阻止和取消阻止線程的通知。
IGCHostControl提供使垃圾回收器能夠請求宿主更改虛擬內存限制的方法。
IGCThreadControl提供用於參與線程調度的方法,以防止因阻塞而執行垃圾回收。
IHostAssemblyManager提供方法,這些方法使宿主能夠指定應由 CLR 或宿主加載的多組程序集。
IHostAssemblyStore提供方法,這些方法使宿主能夠獨立於 CLR 加載程序集和模塊。
IHostAutoEvent提供由宿主實現的自動重置事件的表示形式。
IHostCrst用做線程臨界區的宿主表示形式。
IHostIoCompletionManager提供方法,這些方法使 CLR 能夠與宿主提供的 I/O 完成端口進行交互。
IHostMAlloc提供一些方法,以便 CLR 可以請求從堆到宿主的細化分配。
IHostManualEvent提供宿主的手動重置事件的表示形式的實現。
IHostMemoryManager提供方法,以便 CLR 可以通過宿主而不是使用標准 Win32 虛擬內存函數來請求虛擬內存。
IHostPolicyManager提供一些方法,以便通知宿主 CLR 在中止、超時或失敗時所執行的操作。
IHostSecurityContext使 CLR 能夠維護由宿主實現的安全性上下文信息。
IHostSecurityManager提供允許訪問和控制當前正在執行的線程的安全性上下文的方法。
IHostSemaphore提供由宿主實現的信號量的表示形式。
IHostSyncManager提供方法,以便 CLR 可以通過調用宿主而不是使用 Win32 同步函數來創建同步基元。
IHostTask提供使 CLR 能夠與宿主通信以管理任務的方法。
查看本欄目
1.2 MSCOREE.DLL接口層次模型
所有的CLR Hosting API提供的主要功能包括:CLR的啟動和關閉、App Domain相關、自定義錯誤處理、編程模型的執行、對調試器的支持、Assembly的Load相關、CLR的內部事件、CLR Engine相關、內存管理和垃圾回收、Threading、同步和I/O的支持等。圖1-4展示了MSCOREE.DLL中接口的層次模型。
圖1-4 MSCOREE.DLL中的接口層次模型
和圖1-4對應的是MSCOREE.DLL各個接口在應用過程中的功能模型,如圖1-5所示。
圖1-5 MSCOREE.DLL各個接口在應用過程中的功能模型
現在看代碼清單1-8中ICLRRuntimeHost接口的定義裡面的
HRESULT STDMETHODCALLTYPE SetHostControl方法和HRESULT STDMETHODCALLTYPE GetCLRControl方法。這兩個方法根據功能實現者(CLR或Host)的不同對控制權進行了選擇。將控制權交給CLR的起始是SetCLRControl方法。
注意 如果某一個特定的功能是由CLR來實現的,調用這個功能的相應的接口就用ICLR來開頭,如果這個功能是Host實現的,就調用IHost開頭的接口定義的函數。
1.3 加載CLR與執行代碼實例
下面展示一下如何采用一個非托管的宿主來加載CLR並且執行裡面的代碼。請看代碼清單1-11。
代碼清單1-11 加載CLR與執行代碼實例
//首先,在非托管宿主裡面加載CLR並且啟動
ICLRRuntimeHost *pCLRHost = NULL;
HRESULT hr = CorBindToRuntimeEx(
L"v2.0.40103", //需要加載的CLR版本,Null表示最新的
L"wks", //GC的風格,Null表示默認的工作站模式
STARTUP_CONCURRENT_GC,
CLSID_CLRRuntimeHost, //CLR的CLSID
IID_ICLRRuntimeHost, //ICLRRuntimeHost的IID
(PVOID*) &pCLRHost); //返回的COM接口
// 初始化並且啟動CLR
pCLRHost->Start();
//然後執行一段托管代碼
hr = pCLRHost ->ExecuteInDefaultAppDomain(L"test.exe",
L" test.Program",
L"Start",
NULL,
&retVal);
代碼清單1-11只是一個最簡單的示例,在實際應用中可以實施更多的配置,由於篇幅所限,本書就不更多地去講解相關內容了,可以明確的是,一切操作都不會脫離上述接口。
-----------------------注:本文部分內容改編自《.NET安全揭秘》
作者:玄魂
出處:http://www.cnblogs.com/xuanhun/