使用私有 MFC、ATL 和 CRT 程序集轻松创建项目






4.95/5 (32投票s)
一种从应用程序本地文件夹使用 CRT、MFC 和 ATL 库 DLL 轻松创建程序的方法
引言
直到 VC++ 7.1 (VS.NET 2003),部署使用 CRT-DLL 或 MFC 共享 DLL 的程序都很容易。只需将运行时 DLL 复制到目标目录,即可与可执行文件一起运行。
VC++ 8.0 (VS 2005) 突然停止了这种做法。使用共享 DLL 版本的 CRT 和 MFC 的程序无法轻松地将运行时 DLL 复制到本地应用程序文件夹并运行应用程序。这在 Windows 2000 上可以运行,但在 Windows XP 和 Windows Vista 上不行。原因是 VC++ 8.0 将 CRT 和 MFC(如果使用 ATL)与清单绑定到 EXE。此清单告诉加载程序从当前 Windows 系统的并排 (SxS) 存储中提取文件。
因此,如果运行时文件未安装在 SxS 存储中,程序将无法运行,即使所需文件在当前应用程序文件夹中也是如此。您必须使用适当的 MSI 合并模块或安装程序将运行时库文件安装到计算机上。有关更多详细信息,请参阅关于 vcredist_x86.exe 的文章。
因此,许多开发人员决定静态链接 MFC 和 CRT 以避免这种情况。然而,在许多情况下,不可能使用库的静态链接版本,例如在使用 MFC 扩展 DLL 时。将 MFC 和 CRT 作为私有程序集使用的方法是众所周知的,您可能会发现一些讨论此问题的帖子、论坛、文章或博客。
为运行时文件使用私有程序集可以轻松实现 xcopy 部署。在这种情况下,不需要引导程序或 MSI 安装程序。要使用运行时文件的私有程序集,需要提取运行时文件并手动修改创建的清单。我不喜欢编辑清单。您可能会犯太多错误。此外,您必须修改创建 C++ 项目的标准方式。同样,我也不喜欢项目设置中有太多智能。我喜欢尽可能多地由代码控制项目。
所以,我问自己是否有办法重用当前代码并改变其行为,使其自动创建支持私有应用程序本地程序集的清单。结果就是这个项目。
背景
当您使用 VC++ 8.0 和 MFC 及 CRT 的共享 DLL 创建标准程序时,会创建以下清单:
<?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.50727.762' processorArchitecture='x86'
publicKeyToken='1fc8b3b9a1e18e3b'/>
</dependentAssembly>
</dependency>
<dependency>
<dependentAssembly>
<assemblyIdentity type='win32' name='Microsoft.VC80.MFC'
version='8.0.50727.762' processorArchitecture='x86'
publicKeyToken='1fc8b3b9a1e18e3b'/>
</dependentAssembly>
</dependency>
<dependency>
<dependentAssembly>
<assemblyIdentity type='win32' name='Microsoft.Windows.Common-Controls'
version='6.0.0.0' processorArchitecture='x86'
publicKeyToken='6595b64144ccf1df'language='*' />
</dependentAssembly>
</dependency>
</assembly>
publicKeyToken
属性告诉 Windows XP/2003/Vista 系统中的加载程序在 SxS 存储中查找所需的 DLL。因此,如果我们从清单中删除 publicKeyToken
条目,程序将不再使用公共安装的文件。它已更改为使用请求的私有和应用程序本地程序集。
现在我们移除令牌。要使用的手动创建的清单仍必须包含在您的可执行文件中。我们不能使用自动创建的清单。我们也不能使用项目中自动收集源代码中所有清单依赖项、将其编译成单个清单并将其附加到应用程序的自动机制。稍后,当我描述代码时,我将回到这个问题。
创建 MFC 和 CRT 程序集的私有副本
如果我们要将 MFC 和 CRT 作为私有副本使用,我们必须准备程序集。仅仅复制所需的文件是不够的。您必须为 CRT 文件、MFC 文件、MFC 区域设置文件以及(如果适用)ATL DLL 进行此准备。这还不是全部。您必须进行两次:一次用于发布模式文件,一次用于调试模式文件。请记住,您只允许重新分发发布模式文件。
文件已准备好在 Visual Studio 目录结构中复制。您可以在 C:\Program Files\Microsoft Visual Studio 8\VC\redist\x86 文件夹中找到发布模式文件。调试模式文件位于 C:\Program Files\Microsoft Visual Studio 8\VC\redist\Debug_NonRedist\x86 文件夹中。我现在将描述涉及 CRT 发布模式文件的过程。在我的情况下,这些是 VS-2005 SP1 文件,版本 8.0.50727.762。
首先,我们将 CRT 文件复制到我们的应用程序文件夹中。我们使用 C:\Program Files\Microsoft Visual Studio 8\VC\redist\x86\Microsoft.VC80.CRT 中的文件。然后我们找到文件 msvcr80.dll、msvcp80.dll、msvcm80.dll 和 Microsoft.VC80.CRT.manifest。现在我们需要对这个清单文件进行一些小的更改,使其成为私有应用程序本地程序集。我们必须从中删除 publicKeyToken
条目,但我们不需要修改任何其他部分。
<?xml version="1.0" encoding="UTF-8"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<noInheritable />
<assemblyIdentity name="Microsoft.VC80.CRT" version="8.0.50727.312"
processorArchitecture="x86" publicKeyToken="1fc8b3b9a1e18e3b"
type="win32" />
<file hash="58985c4d847f19365d93fb8703e78c2b1ae57500" hashalg="SHA1"
name="msvcr80.dll" xmlns:cmiv2="urn:schemas-microsoft-com:asm.v3"
cmiv2:sourceName="">
<asmv2:hash xmlns:asmv2="urn:schemas-microsoft-com:asm.v2">
<dsig:Transforms xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
<dsig:Transform
Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
</dsig:Transforms>
<dsig:DigestMethod xmlns:dsig="http://www.w3.org/2000/09/xmldsig#"
Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
<dsig:DigestValue
xmlns:dsig="http://www.w3.org/2000/09/xmldsig#"
>zUUe9BK+jgwQPjesMcOhEi690wHyaXcfFCajzryLpYo=</dsig:DigestValue>
</asmv2:hash>
</file>
<file hash="3bf3838264659c30b657fd2176c9cbf4adb5b067"
hashalg="SHA1" name="msvcp80.dll"
xmlns:cmiv2="urn:schemas-microsoft-com:asm.v3" cmiv2:sourceName="">
<asmv2:hash xmlns:asmv2="urn:schemas-microsoft-com:asm.v2">
<dsig:Transforms xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
<dsig:Transform
Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
</dsig:Transforms>
<dsig:DigestMethod xmlns:dsig="http://www.w3.org/2000/09/xmldsig#"
Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
<dsig:DigestValue
xmlns:dsig="http://www.w3.org/2000/09/xmldsig#"
>SdVtn3h0M4cTqWshracoyu1oJNmwweuYpi+hhf9rnzY=</dsig:DigestValue>
</asmv2:hash>
</file>
<file hash="aacedb76716864fbcadae5b167eb7ab8dc809fe8" hashalg="SHA1"
name="msvcm80.dll" xmlns:cmiv2="urn:schemas-microsoft-com:asm.v3"
cmiv2:sourceName="">
<asmv2:hash xmlns:asmv2="urn:schemas-microsoft-com:asm.v2">
<dsig:Transforms xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
<dsig:Transform
Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
</dsig:Transforms>
<dsig:DigestMethod xmlns:dsig="http://www.w3.org/2000/09/xmldsig#"
Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
<dsig:DigestValue
xmlns:dsig="http://www.w3.org/2000/09/xmldsig#"
>0CXjg8zPg3wsAPUtF5D0iSGO2MxppEkANMI9WqEcjEQ=</dsig:DigestValue>
</asmv2:hash>
</file>
</assembly>
如果你愿意,你可以通过删除其中的 schema 提示来简化清单。最终简化的清单确实看起来非常简单。
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<noInheritable></noInheritable>
<assemblyIdentity type="win32"
name="Microsoft.VC80.CRT" version="8.0.50727.762"
processorArchitecture="x86"></assemblyIdentity>
<file name="msvcr80.dll" hash="10f4cb2831f1e9288a73387a8734a8b604e5beaa"
hashalg="SHA1"/>
<file name="msvcp80.dll" hash="b2082dfd3009365c5b287448dcb3b4e2158a6d26"
hashalg="SHA1"/>
<file name="msvcm80.dll" hash="542490d0fcf8615c46d0ca487033ccaeb3941f0b"
hashalg="SHA1"/>
</assembly>
现在对 MFC 和 ATL DLL 重复这些步骤。如果您愿意,也可以对调试模式文件重复此操作。我将私有程序集用于发布模式和调试模式文件。出于测试目的,将调试模式 DLL 嵌入为私有程序集很有意义。这是因为您无法在没有安装 Visual Studio 2005 的情况下在第二台测试计算机上安装调试模式运行时文件。没有官方的 MSI 包可以直接将调试模式文件安装到计算机上。
您将在本文顶部的单独 ZIP 文件中找到修改后的发布模式文件。我无法上传调试文件,因为 EULA 不允许这样做。然而,有了这个描述,创建自己的副本应该很简单。获取上述文件的另一种方法是直接从并排存储中使用它们。以下是并排存储 (C:\Windows\winsxs) 中文件夹名称的完整列表:
CRT 调试 | x86_microsoft.vc80.debugcrt_1fc8b3b9a1e18e3b_ 8.0.50727.762_none_24c8a196583ff03b |
CRT 发布 | x86_microsoft.vc80.crt_1fc8b3b9a1e18e3b_ 8.0.50727.762_none_10b2f55f9bffb8f8 |
MFC 调试 | x86_microsoft.vc80.debugmfc_1fc8b3b9a1e18e3b_ 8.0.50727.762_none_29a8a38855141f6e |
MFC 发布 | x86_microsoft.vc80.mfc_1fc8b3b9a1e18e3b_ 8.0.50727.762_none_0c178a139ee2a7ed |
MFC 区域设置 | x86_microsoft.vc80.mfcloc_1fc8b3b9a1e18e3b_ 8.0.50727.762_none_43efccf17831d131 |
ATL | x86_microsoft.vc80.atl_1fc8b3b9a1e18e3b_ 8.0.50727.762_none_11ecb0ab9b2caf3c |
匹配的清单位于 C:\Windows\winsxs\Manifests 文件夹中,名称与上表中的相同。如果您想使用这些文件,您必须将清单文件重命名为纯程序集名称,即发布模式 CRT 程序集的 microsoft.vc80.crt.manifest。
运行时库文件的放置位置
最简单、最兼容的方法是将所有 DLL 和清单文件放在与您的可执行文件相同的文件夹中。这适用于 Windows 2000、Windows XP 和 Vista。但是,如果您愿意,可以将一个程序集的所有文件放置在一个以所需清单命名的子目录中。如果您查看相关的 ZIP 文件,您会发现所有库文件都以这种方式排列。如果加载程序被请求搜索名为 microsoft.vc80.mfc 的清单,它也会在应用程序文件夹中搜索具有此清单名称的目录。所有属于此程序集的文件(包括清单文件)都必须存储在此类子目录中。
请记住,这种文件布局在 Windows 2000 上不起作用。如果您计划进行也适用于 Windows 2000 的 xcopy 分发,则应将所有运行时库文件放在与可执行文件相同的文件夹中。
到目前为止的结果
至此,我们已经成功创建了一个从应用程序本地存储加载运行时库文件的应用程序。我们只需要这样做一次。剩下的唯一问题是:我们总是需要修改自动创建的清单忽略的项目。我们必须删除 publicKeyToken
条目,并且必须将此新清单嵌入到我们的应用程序中。正如我上面提到的,如果我们能够只用一些代码修改清单,并且仍然使用相同的技术而无需触及项目设置,那就太好了。
使用代码
如果您在包含任何其他 CRT 或 MFC 头文件之前包含头文件 UseMSPrivateAssemblies.h,则所有操作都将自动完成。您会发现您的项目会编译并创建一个不使用公共并排程序集的清单。
注意:在使用任何其他 CRT 和 MFC 头文件之前包含此文件至关重要。包含此文件的最佳位置是您项目的 stdafx.h 文件,就在顶部,在任何定义或其他包含文件之前使用。
另请注意,ATL 和 MFC 区域设置定义在此头文件中已被注释掉。您可以随意在它们周围放置 #ifdef
语句。ATL.DLL 的使用很少见。此外,MFC 区域设置 DLL 不必通过程序集清单加载。MFC 可以在当前应用程序文件夹中找到它们,无需任何进一步操作。
它是如何工作的?
代码做的第一件事是定义预处理器变量 _STL_NOFORCE_MANIFEST
、_CRT_NOFORCE_MANIFEST
、_AFX_NOFORCE_MANIFEST
和 _ATL_NOFORCE_MANIFEST
(仅当您使用它时)。这些定义可以防止 MS 头文件弹出带有使用的 CRT 和 MFC 的清单信息的 #pragma comment
语句。在库文件中使用这些定义是非常明智的。在这种情况下,不会从我们的对象代码创建清单条目,并且使用您的库的代码可以自由定义所需的程序集。
然而,这还不够。如果您只是定义预处理器变量,您不会从编译后的代码中创建清单条目。当您链接到 CRT 或 MFC 文件时,会从库中拉出两个对象文件,它们会再次创建一些清单条目。通过定义一些变量,可以很容易地强制链接器不包含这些对象文件。
int _forceCRTManifest;
int _forceMFCManifest;
int _forceAtlDllManifest;
变量必须定义为 extern "C"
。如您在原始源代码中看到的,我用 __declspec(selectany)
声明了它们。这是为了防止链接器由于头文件在多个模块中使用时链接器会发现这些变量的多个定义而引发错误。
最后,我们想要创建我们的清单,将程序集定义为私有。同样,只要所有运行时文件的基本版本相同,这很容易。我只是使用 CRT 代码中的头文件 crtassem.h 来获取正确的版本和名称。创建的清单条目反映了调试和发布模式,如以下源代码所示。
#ifdef _DEBUG
#define __LIBRARIES_SUB_VERSION "Debug"
#else
#define __LIBRARIES_SUB_VERSION ""
#endif
// Manifest for the CRT
#pragma comment(linker,"/manifestdependency:\"type='win32' " \
"name='" __LIBRARIES_ASSEMBLY_NAME_PREFIX ".
" __LIBRARIES_SUB_VERSION "CRT' " \
"version='" _CRT_ASSEMBLY_VERSION "' " \
"processorArchitecture='x86' \"")
// Manifest for the MFC
#pragma comment(linker,"/manifestdependency:\"type='win32' " \
"name='" __LIBRARIES_ASSEMBLY_NAME_PREFIX ".
" __LIBRARIES_SUB_VERSION "MFC' " \
"version='" _CRT_ASSEMBLY_VERSION "' " \
"processorArchitecture='x86'\"")
#pragma comment(linker,"/manifestdependency:\"type='win32' " \
"name='" __LIBRARIES_ASSEMBLY_NAME_PREFIX ".MFCLOC' " \
"version='" _CRT_ASSEMBLY_VERSION "' " \
"processorArchitecture='x86'\"")
请随意在此头文件中添加 #ifdef
和其他内容,以使其更具灵活性。也许这是 1.1 版的工作。
为什么我们不应该使用原始清单?
如果您在应用程序目录中使用未更改的清单会发生什么?它不会工作吗?
是的!它会工作。不幸的是,您无法控制加载哪些程序集。如果所需的程序集也在 SxS 存储中,那么操作系统的加载程序将从那里选择文件。如果文件未安装在 SxS 存储中,那么将使用应用程序文件夹中的本地文件。如果在并排存储中安装了新的服务包,例如 SP2,这些文件将被加载并执行。应用程序本地文件将被忽略。因此,以这种方式使用文件是一种回退解决方案。但是,通过这种方式可以实现某种形式的 xcopy 安装。
感谢我的同事 MVP Hans Dietrich 指出这一点。
运行示例
即使您已经编译了示例,只要您没有将所需的运行时文件复制到应用程序目录或子目录中,它就无法运行!所以,如果您收到错误,请不要感到惊讶。您可以在单独的 ZIP 文件中找到发布模式运行时库文件。
最后提示
MFC 区域设置文件不需要清单。您只需将它们复制到应用程序文件夹中即可。这很好用。MFC 总是首先尝试通过内部清单加载文件。如果找不到文件,它会从应用程序目录加载它们。MFC 文件总是包含 UNICODE 和 MCBS 版本。您可能只需要一个。随意修改 MFC DLL 的清单文件,只需发布您真正需要的版本。
就是这样;祝你玩得开心。祝您编程愉快,上帝保佑大家。
历史
- 2007 年 6 月 12 日 -- 1.0 版:VC++ 8.0 的第一个基本版本
- 2007 年 6 月 13 日 -- 1.1 版:简化了描述并添加了当前讨论中的一些注释