转载

在 Visual Studio 本地引用 Boost

介绍

这是关于将第三方工具和库集成到 Visual Studio 系列中的第四篇文章。在 第一篇文章 中我解释了如何创建 Visual Studio 属性对话框的自定义属性页。 第二篇文章 涵盖了属性表的内部结构和元素。第三篇文章通过构建 Boost 库的例子解释了如何创建自定义构建。本文是第四篇,我将解释如何集成自定义构建到 Visual Studio 项目的引用系统。

基本原理

每个 C++ 项目由几个较小的子项目和库组成。它们在编译或运行时被用于链接,需要被适当地引用。如果所有的项目都是在 Visual Studio(MSBuild)中被创建的,那么引用是由 MSBuild 来负责。但当一个项目或库来自外部时,我们不得不通过手动配置来适当地集成。

理想情况下,我们应该能通过在 Visual Studio 中添加对某个项目的引用来将其集成到 MSBuild 中:

在 Visual Studio 本地引用 Boost

如果我们对任何库都能这么做,那不是很好吗?如果所有的 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

我将简要地解释它们分别做了什么。

GetTargetPath

这个目标(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)设置。

GetNativeManifest

如果由于某种原因,子项目必须重新发布Manifest文件以及库文件,这个目标(Target)会返回manifest文件的列表信息。父工程会简单的拷贝这些manifest文件到输出目录。

Boost无需任何manifest文件,所以它不用做任何设置:

<Target Name="GetNativeManifest" />

GetResolvedLinkLibs

这个目标(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>

当库列表信息返回后,我们几乎不用为每个项目设置元属性。

GetCopyToOutputDirectoryItems

这个目标(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工程作为引用添加进来,它将会注册为有效的,同时提供父工程在正确构建过程中可能需要的所有信息。

基于 Boost 进行构建

我们开始用一个非常原始的名字( Sample )来创建一个简单控制台应用程序。鉴于每个人都知道如何在 Visual Studio 中创建一个控制台应用程序,我将跳过相关的步骤说明。

boost 项目添加至解决方案。

前往 Sample 项目的属性页,添加对 boost 项目的引用。你会看到像这样的界面:

在 Visual Studio 本地引用 Boost

如图所示, boost 项目已被正确地引用并指向 Boost 库安装的 D:/Boost 目录。由于 Boost 不是一个托管程序集,程序集标识(Assembly Name)、区域性(Culture)、版本号(Version)、描述(Description)都不可用。

值得注意的是, Copy Local 属性用来确定库是否要被复制到引用它的项目的输出目录。如果子项目生成的是托管程序集或只是一个lib 文件,那是不会出问题的。但如果子项目生成的是原生 DLL 或多个库的话,整个过程就会中断。我们通过重新定义 GetCopyToOutputDirectoryItems 来修复它。我们现在要来控制是否将 DLL 复制到主项目的输出目录,那就需要向 Boost 属性页的常规选项卡添加额外的属性:

在 Visual Studio 本地引用 Boost

将这个属性设置为 No 可以禁用复制。这个设定只在 Boost 库是以共享的方式被构建时才起作用,对生成静态库是无效的。

增量构建

每当我们构建的时候, b2 会检查配置并决定它是否要构建组件的一部分。当 Boost 被用来开发其他项目时,它本身不大会发生什么变动。所以检查是否发生变动基本上是多余的。我已经在属性页的常规选项卡中增加了一个禁用这种检查的选项:

在 Visual Studio 本地引用 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)  (确认环境变量被设置过)。

调试Boost

写这篇文章的目的之一就是演示集成的 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

正文到此结束
Loading...