客戶清單文件
一個客戶文件(程序或庫)能依靠于程序集中的某個文件來構建,但客戶文件只會依靠于程序集的某個特定版本來構建,Windows也只會加載所需的特定版本。為標出所需共享程序集的版本,一個可執行文件(程序或庫)也必須有一個清單文件(manifest)。鏈接器在可執行文件生成時,會為其創建一個包含清單信息的文件,因此,假如回過頭來看一下前面生成的庫的目錄,會找到一個名為"lib.dll.manifest"的文件,例3是其的內容。
例3:
<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
<assembly xmlns='urn:schemas-microsoft-com:asm.v1' manifestVersion='1.0'>
<dependency>
<dependentAssembly>
<assemblyIdentity type='win32' name='Microsoft.VC80.CRT'
version='8.0.50608.0'
publicKeyToken='1fc8b3b9a1e18e3b' />
</dependentAssembly>
</dependency>
</assembly>
正如大家所看見的,它說明托管程序集lib.dll依靠于Microsoft.VC80.CRT共享并列程序集中的某些文件。盡管這個文件位于庫lib.dll的同一目錄中,但Windows明顯不會用到它,而這與MSDN中寫明的有點背道而馳:
?。∕SDN):你可在應用程序二進制可執行頭文件中包含應用程序清單文件……,作為備選方案,也可把一個單獨的清單文件放在應用程序可執行文件的同一目錄中,
操作系統會首先從文件系統中加載此清單文件,并檢查可執行文件的資源節。文件系統的版本具有優先權。
然而,完全不是這么回事。對庫而言,Windows會忽略清單文件,盡管如此,文檔還是給出了怎樣解決這個問題的一個線索,清單文件一定要以資源ID為2的非托管資源RT_MANIFEST形式綁定到可執行文件。
在此有兩種方法:第一種方法是創建一個包含對清單文件引用的資源腳本文件:
#include <winuser.h>
2 RT_MANIFEST lib.dll.manifest
它會在以后通過Windows資源編譯器rc.exe編譯為一個資源,并通過鏈接器限定為一個非托管資源。這種方法的問題之處在于,是鏈接器創建了這個清單文件,因此必須運行兩次鏈接器:一次是為生成清單文件,一次是把資源鏈接到最終生成文件。例4是一個范例makefile,演示了如何進行操作:
例4:
# The main target
all: app.exe
# A C# process that depends upon a Managed C++ library
app.exe : app.cs lib.dll
csc app.cs /r:lib.dll
# This is the second invocation of the linker, so the object file and
# manifest will already exist, so they do not need to be rebuilt.
lib.dll : lib.cpp lib.res lib.obj
link /DLL /manifest:no /machine:x86 lib.res lib.obj
lib.res : lib.rc
rc lib.rc
# Create a temporary resource scr
ipt that binds the manifest file to the DLL
lib.rc : lib.dll.manifest
type <<$@
#include <winuser.h>
2 RT_MANIFEST lib.dll.manifest
<<KEEP
# Create the object file, and invoke the linker to create the manifest file
lib.dll.manifest lib.obj : lib.cpp
cl /LD /clr lib.cpp
另一個方法是使用mt.exe未公開的隱藏選項把資源綁定到最終生成文件上,這也是Visual Studio 2005創建加載的C++庫(托管混合模式或非托管模式)時所使用的方法。兩個隱藏選項分別為/manifest和/outputresource:前者用于指定清單文件名,而后者用于指出將要修改的PE文件及清單資源的資源ID。一般而言,對庫來說,資源ID應為2;對程序來說,應為1。請看以下示例:
mt /manifest lib.dll.manifest
/outputresource:lib.dll;#2
注重此處的區別:/manifest選項后跟的參數是用空格分隔的;而/outputresource選項后跟的參數是用冒號分隔的。明顯看得出,這兩個選項是由不同的程序員開發的。
一旦你把清單文章綁定到庫,Windows就可以判定需加載程序集的正確版本。假如在作出這些修改之后,運行前面的C#程序,也會發現程序運行正常。
假如你生成了一個混合模式(/clr)或純媒介語言模式(/clr:pure)的托管C++程序,來使用這個混合模式的庫,鏈接器也創建了一個相應的程序清單文件,當此程序運行時,Windows會查找資源ID為1的清單文件,或查找名為manifest的相應文件。因為混合模式或純媒介語言模式程序都用到了CRT,意味著將會在清單文件中提及CRT程序集,所以,在這個特例中,庫不需要清單文件。然而,你不應該依靠這個機制,因為在本例中,程序使用同一個非托管程序集作為庫是一個偶然情況。進入討論組討論。
版本重定向
回過頭來再看一下為庫創建的清單文件,注重程序集所需的版本號給定為8.0.50608.0,再次提醒,Visual Studio 2005安裝的程序集是8.0.50727.42,這個叫策略版本重定向。在并列緩存的同級Policies目錄中,可找到下面這個文件夾:
x86_policy.8.0.Microsoft.VC80.CRT_1fc8b3b9a1e18e3b_x-ww_77c24773
注重,除了版本部分,它有著程序集的全名。此文件夾中分別包含了一個策略及安全編目文件,文件名基于將要重定向至的版本號:
8.0.50727.42.policy
這是一個XML文件(見例5)。這個策略文件是針對版本8.0.50727.42的,其也是Visual Studio安裝程序所安裝的版本。它在<bindingRedirect>中指明了所有將要被重定向至本版本的版本號,例5中表明,對版本號8.0.41204.256至8.0.50608.0程序集的所有請求,都會被重定向至8.0.50727.42這個版本。與Fusion(混淆: .NET中的程序集加載技術)不同的是,對并列共享程序集的版本重定向只能是那些生成或修訂的版本值之間的變化,不能用于主、副版本值的變化。
例5:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!-- Copyright (r) 1981-2001 Microsoft Corporation -->
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity type="win32-policy" name="policy.8.0.Microsoft.VC80.CRT"
version="8.0.50727.42" processorArchitecture="x86"
publicKeyToken="1fc8b3b9a1e18e3b"/>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.VC80.CRT"
processorArchitecture="x86" publicKeyToken="1fc8b3b9a1e18e3b"/>
<bindingRedirect oldVersion="8.0.41204.256-8.0.50608.0"
newVersion="8.0.50727.42"/>
</dependentAssembly>
</dependency>
</assembly>
那就又帶出了一個問題:那為什么需要重定向呢?為什么鏈接器不在清單文件中直接指定由安裝程序安裝的程序集版本呢?原因在于,鏈接器是從導入靜態庫中獲得所需的程序集版本。這又引出了另外一個問題:為什么鏈接器要為DLL的不同版本使用導入庫,而不是安裝的那個?原因是,這些安裝的都是重要的庫。
目前為止的討論都是針對托管C++編譯器(C++/CLI及舊式語法),然而,即便本地C++開發技巧再高,也有可能被這些新"特性"所影響。假如你的代碼使用了某個共享的Visual Studio本地庫(MFC、ATL或CRT),那么,必須有一個單獨的.manifest清單文件,要么綁定至可執行文件,要么只綁定至一個 .exe文件。
結論
以前Microsoft C++編譯器及鏈接器的各個版本所生成的庫,都能被Windows加載并運行,但Visual Studio 2005中的版本14,生成的庫卻無法運行。
此處有兩個解決方法:第一種方法是運行鏈接器兩次,一次是生成清單文件,其能編譯進非托管資源,接著一次是把這個清單綁定至PE文件。這也是本文所推薦的方法,因為假如在構造一個具有"強名稱"的程序集,在第二次調用時,就能提供密鑰文件或容器的名稱。
另一個方法是,使用mt.exe未公開的選項來修改程序集,然而,假如使用鏈接器來生成一個"強名稱"的程序集,mt.exe的動作會使強名稱簽名無效,且程序集也不會加載。進入討論組討論。