客户清单文件
一个客户文件(程序或库)能依靠于程序集中的某个文件来构建,但客户文件只会依靠于程序集的某个特定版本来构建,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'
PRocessorArchitecture='x86'
publicKeyToken='1fc8b3b9a1e18e3b' />
</dependentAssembly>
</dependency>
</assembly>
正如大家所看见的,它说明托管程序集lib.dll依靠于Microsoft.VC80.CRT共享并列程序集中的某些文件。尽管这个文件位于库lib.dll的同一目录中,但Windows明显不会用到它,而这与MSDN中写明的有点背道而驰:
(MSDN):你可在应用程序二进制可执行头文件中包含应用程序清单文件……,作为备选方案,也可把一个单独的清单文件放在应用程序可执行文件的同一目录中,
操作系统会首先从文件系统中加载此清单文件,并检查可执行文件的资源节。文件系统的版本具有优先权。
然而,完全不是这么回事。对库而言,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的动作会使强名称签名无效,且程序集也不会加载。进入讨论组讨论。