这是关于将第三方工具和库集成到 Visual Studio 系列中的第四篇文章。在 第一篇文章 中我解释了如何创建 Visual Studio 属性对话框的自定义属性页。 第二篇文章 涵盖了属性表的内部结构和元素。第三篇文章通过构建 Boost 库的例子解释了如何创建自定义构建。本文是第四篇,我将解释如何集成自定义构建到 Visual Studio 项目的引用系统。
每个 C++ 项目由几个较小的子项目和库组成。它们在编译或运行时被用于链接,需要被适当地引用。如果所有的项目都是在 Visual Studio(MSBuild)中被创建的,那么引用是由 MSBuild 来负责。但当一个项目或库来自外部时,我们不得不通过手动配置来适当地集成。
理想情况下,我们应该能通过在 Visual Studio 中添加对某个项目的引用来将其集成到 MSBuild 中:
如果我们对任何库都能这么做,那不是很好吗?如果所有的 lib 文件会自动添加到 LINK 命令,同时所有的 DLL 文件都复制到输出目录,那么他们不就可以在运行时被链接上吗?如果既能调试我们的代码,还能调试库的代码,那又会怎样呢?
在这篇文章中,我将告诉你这该如何做到。我会用库来演示如何将它集成到任何项目,却无需手动操作库或设置路径。我假设你已经知道该如何构建 Boost,不知道的话就请读 这篇文章 。
当 Visual Studio 从一个项目添加引用到另一个项目时,它会像下面这样将一条记录添加到主项目中:
<ProjectReference Include=".../boost.vcxproj"> <Project>{9cd23c68-ba74-4c50-924f-2a609c25b7a0}</Project> ... </ProjectReference>
关于引用是如何被添加的详细信息,参见 这个链接 。
在构建主项目的过程中,MSBuild 会对 ProjectReference 段中列出的所有依赖进行解析和构建。它会定位列出的子项目,并通过对每个子项目调用下列 Target 来收集必要的信息:
GetTargetPath GetNativeManifest GetResolvedLinkLibs GetCopyToOutputDirectoryItems
我将简要地解释它们分别做了什么。
这个目标(Target)返回项目构建的程序集/库的完整路径。在设计阶段,Visual Studio使用这个文件来判断引用是否正确,以及是否可以找到输出文件。如果程序集是托管类型,Visual Studio也会查询它以获取更多的信息。
理论上讲,只要这个路径指向已经存在的文件,引用系统都会正常报告引用是有效的。
对于Boost库而言,没有单一的库文件。它依据配置,构建任意数量的库文件,或者根本就不构建库文件。我们可以使用这些来进行引用校验。我们可以返回指向任意文件的路径,来表明引用是有效的。我决定返回文件Jamroot的路径,用来表明,本次构建是使用哪个源代码来创建的库文件:
<Target Name="GetTargetPath" Returns="@(TargetPath)" > <ItemGroup> <TargetPath Include="$(BoostRoot)/Jamroot"> <Private>true</Private> <FileType>info</FileType> <ResolveableAssembly>false</ResolveableAssembly> </TargetPath> </ItemGroup> </Target>
它需要在项目(Item)上设置如上所示的一些元数据( metadata)属性。FileType通常包含如lib或dll为拓展名的文件,由于在这里不适用,所以我返回了假的类型。ResolveableAssembly表明,它是托管程序集或者是原生的。Private包含了本地复制(Local Copy)设置。
如果由于某种原因,子项目必须重新发布Manifest文件以及库文件,这个目标(Target)会返回manifest文件的列表信息。父工程会简单的拷贝这些manifest文件到输出目录。
Boost无需任何manifest文件,所以它不用做任何设置:
<Target Name="GetNativeManifest" />
这个目标(Target)返回所有链接库的列表信息。它们将会添加到LINK命令,这样这些lib文件就可以链接了。Boost库针对它创建的每个模块都有一个lib文件。
对我们来说,要返回正确的列表信息,首先要获取创建的库文件的列表信息,然后链接到实际的lib文件。我们需要完成两个步骤:
使用当前 选项 以及 --show-libraries 命令调用b2(GetBuiltLibs)
处理链接库的引用,并将它们加入返回列表(GetResolvedLinkLibs)
<Target Name="GetBuiltLibs" DependsOnTargets="BuildJamTool" Returns="@(BuiltLibs)" > <Exec Command="b2.exe @(boost-options, ' ') --show-libraries" ... /> <ReadLinesFromFile Condition="Exists('$(TempFile)')" File="$(TempFile)"> <Output TaskParameter="Lines" ItemName="RawOutput" /> </ReadLinesFromFile> <Delete Condition="Exists('$(TempFile)')" Files="$(TempFile)"/> <ItemGroup> <BuiltLibs Include="$([Regex]::Match(%(RawOutput.Identity), (?<=/-/s)(.*) ))" /> </ItemGroup> </Target>
请注意:为了清晰起见,文中所有的示例代码都做了简化处理。
<Target Name="GetResolvedLinkLibs" DependsOnTargets="GetBuiltLibs" Returns="@(LibFullPath)"> <ItemGroup> <LibFullPath Include="$(OutputDir)/lib/*boost*%(BuiltLibs.Identity)*.lib"> <ProjectType>StaticLibrary</ProjectType> <FileType>lib</FileType> <ResolveableAssembly>false</ResolveableAssembly> </LibFullPath> </ItemGroup> </Target>
当库列表信息返回后,我们几乎不用为每个项目设置元属性。
这个目标(target)返回内容文件的列表信息,这些文件需要拷贝到主工程的输出目录。 它们可以是任意类型的文件。对于Boost库来说,它们是构建过程中创建的所有dll文件。我们使用,与之前一样的算法,来列出这些文件:
<Target Name="GetCopyToOutputDirectoryItems" DependsOnTargets="GetBuiltLibs" Returns="@(DLLToCopy)" Condition="'$(boost-link)'=='DynamicLibrary'" > <ItemGroup> <BoostDlls Include="$(OutputDir)/lib/*boost*%(BuiltLibs.Identity)*.dll" /> <DLLToCopy Include="@(BoostDlls)" Condition="'%(BoostDlls.Identity)'!=''" > <TargetPath>%(FileName).dll</TargetPath> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </DLLToCopy> </ItemGroup> </Target>
在上面的代码中,每个项目需要设置两个元数据(Metadata)属性:TargetPath和CopyToOutputDirectory。
TargetPath包含文件名和拓展名。当拷贝到目标文件夹,类似这样:$(DestinationFolder)$(TargetPath)的时候,它被用来指定文件名。
CopyToOutputDirectory包含两个可能的值:Always和PreserveNewest,其中之一。
它告知构建系统,要么总是拷贝文件,要么只拷贝源文件比目标文件新的文件。
对Boost库来说,如果是最新的,就无需拷贝DLL文件。
现在,如果我们将boost工程作为引用添加进来,它将会注册为有效的,同时提供父工程在正确构建过程中可能需要的所有信息。
我们开始用一个非常原始的名字( Sample )来创建一个简单控制台应用程序。鉴于每个人都知道如何在 Visual Studio 中创建一个控制台应用程序,我将跳过相关的步骤说明。
将 boost 项目添加至解决方案。
前往 Sample 项目的属性页,添加对 boost 项目的引用。你会看到像这样的界面:
如图所示, boost 项目已被正确地引用并指向 Boost 库安装的 D:/Boost 目录。由于 Boost 不是一个托管程序集,程序集标识(Assembly Name)、区域性(Culture)、版本号(Version)、描述(Description)都不可用。
值得注意的是, Copy Local 属性用来确定库是否要被复制到引用它的项目的输出目录。如果子项目生成的是托管程序集或只是一个lib 文件,那是不会出问题的。但如果子项目生成的是原生 DLL 或多个库的话,整个过程就会中断。我们通过重新定义 GetCopyToOutputDirectoryItems
来修复它。我们现在要来控制是否将 DLL 复制到主项目的输出目录,那就需要向 Boost 属性页的常规选项卡添加额外的属性:
将这个属性设置为 No 可以禁用复制。这个设定只在 Boost 库是以共享的方式被构建时才起作用,对生成静态库是无效的。
每当我们构建的时候, b2
会检查配置并决定它是否要构建组件的一部分。当 Boost 被用来开发其他项目时,它本身不大会发生什么变动。所以检查是否发生变动基本上是多余的。我已经在属性页的常规选项卡中增加了一个禁用这种检查的选项:
当这个选项是 Yes 或者空白时,对重新构建的检查会被委派给 Visual Studio。它检查时会比对输出库的列表、已配置库的列表以及项目文件本身。若有任何库被删除或项目设置发生变更,它便会进行构建。否则它会跳过构建,使得每次构建的耗时节省大概半分钟。要重新启用这个检查,请将此选项设为 No 。
将这些检查委派给 Visual Studio 需具备以下要素:
通过测试输出命令: b2 --show-libraries ,可以推断出构建的库列表。一旦我们有了列表,通过调用Target GetBoostOutputs来验证库中所呈现的东西。
<Target Name="GetBoostOutputs" DependsOnTargets="GetBuiltLibs" Returns="@(BoostOutputs)" > <ItemGroup> <BoostOutputs Include="$(OutputDir)/lib/*boost*%(BuiltLibs.Identity)*.lib" > <Library>%(BuiltLibs.Identity)</Library> </BoostOutputs> <ExistingLibs Include="%(BoostOutputs.Library)" /> <BoostOutputs Include="@(BuiltLibs)" Exclude="@(ExistingLibs)" Condition="'@(ExistingLibs->Count())'!='@(BuiltLibs->Count())'" /> <BoostOutputs Include="%(BoostOutputs.RootDir)%(BoostOutputs.Directory)%(BoostOutputs.Filename).dll" Condition="'@(BoostOutputs0>Filename->StartsWith("boost_"))'=='true' And '%(BoostOutputs.Library)'!='' And '$(boost-link)'=='DynamicLibrary'" /> </ItemGroup> </Target>
正如你上面所看到的那样,我们从GetBuiltLibs目标中得到了库的列表,而且查找到所有*boost*<library-name>*.lib样子的lib文件。既返回了包含动态链接库也返回了包含了静态库。
下一步我们将在构建的库文件的列表上创建内部连接,使用它来查找漏掉的库。
紧接着,我们添加漏掉的库到BoostOutputs为了在需要是使用。
然后我们添加动态链接库。
列表将由Build Target来确定是否需要执行检查。
我们仍然需要在应用程序使用的Boost库里指定一个设置。我们需要告诉应用程序这些头文件在哪里。应用程序只要在额外包含目录的列表里通过添加 $(BOOST_BUILD_PATH) (确认环境变量被设置过)。
写这篇文章的目的之一就是演示集成的 Visual Studio 不仅仅在应用程序本身,而且同样在 Boost 库允许无缝调试。
我用一个从 boost/libs/lockfree/examples/queue.cpp 例子来演示这种功能。
boost::atomic_int producer_count(0); boost::atomic_int consumer_count(0); boost::lockfree::queue<int> queue(128); const int iterations = 10000000; const int producer_thread_count = 4; const int consumer_thread_count = 4; void producer(void) { for (int i = 0; i != iterations; ++i) { int value = ++producer_count; while (!queue.push(value)) ; } } boost::atomic<bool> done(false); void consumer(void) { int value; while (!done) { while (queue.pop(value)) ++consumer_count; } while (queue.pop(value)) ++consumer_count; } int _tmain(int argc, _TCHAR* argv[]) { using namespace std; cout << "boost::lockfree::queue is "; if (!queue.is_lock_free()) cout << "not "; cout << "lockfree" << endl; boost::thread_group producer_threads, consumer_threads; for (int i = 0; i != producer_thread_count; ++i) producer_threads.create_thread(producer); for (int i = 0; i != consumer_thread_count; ++i) consumer_threads.create_thread(consumer); producer_threads.join_all(); done = true; consumer_threads.join_all(); cout << "produced " << producer_count << " objects." << endl; cout << "consumed " << consumer_count << " objects." << endl; return 0; }
在例子的59行设置断点 on producer_threads.join_all(); 允许回调我们的下一步 join_all (thread_group.hpp)
void join_all() { BOOST_THREAD_ASSERT_PRECONDITION( ! is_this_thread_in() ... ); boost::shared_lock<shared_mutex> guard(m); for(std::list<thread*>::iterator it=threads.begin(),end=threads.end(); it!=end; ++it) { if ((*it)->joinable()) (*it)->join(); } }
这一步进入117行 ( *it)->joinable() ,将进入 thread.cpp 的445行:
bool thread::joinable() const BOOST_NOEXCEPT { detail::thread_data_ptr local_thread_info = (get_thread_info)(); if(!local_thread_info) { return false; } return true; }
你的调试也通过了。从被嵌入的调试信息到进入库来推断 cpp 文件的当前位置。因为项目的存储路径都是当前和相对的,Visual Studio 不需要别的附加信息来设置该文件。
Download sample - 1.2 MB
Download Source - 28.1 KB