在 Visual Studio 本地引用 Boost (MSBuild)

jopen 10年前

在 Visual Studio 本地引用 Boost (MSBuild)

介绍

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

基本原理

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

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

在 Visual Studio 本地引用 Boost (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

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

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, &apos; &apos;) --show-libraries" ... />            <ReadLinesFromFile Condition="Exists(&apos;$(TempFile)&apos;)" File="$(TempFile)">        <Output TaskParameter="Lines" ItemName="RawOutput" />      </ReadLinesFromFile>      <Delete Condition="Exists(&apos;$(TempFile)&apos;)" 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="&apos;$(boost-link)&apos;==&apos;DynamicLibrary&apos;" >    <ItemGroup>      <BoostDlls Include="$(OutputDir)\lib\*boost*%(BuiltLibs.Identity)*.dll" />      <DLLToCopy Include="@(BoostDlls)" Condition="&apos;%(BoostDlls.Identity)&apos;!=&apos;&apos;" >        <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 (MSBuild)

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

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

在 Visual Studio 本地引用 Boost (MSBuild)

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

增量构建

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

在 Visual Studio 本地引用 Boost (MSBuild)

当这个选项是 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="&apos;@(ExistingLibs->Count())&apos;!=&apos;@(BuiltLibs->Count())&apos;" />      <BoostOutputs Include="%(BoostOutputs.RootDir)%(BoostOutputs.Directory)%(BoostOutputs.Filename).dll"                  Condition="&apos;@(BoostOutputs0>Filename->StartsWith(&#34;boost_&#34;))&apos;==&apos;true&apos; And                                &apos;%(BoostOutputs.Library)&apos;!=&apos;&apos; And &apos;$(boost-link)&apos;==&apos;DynamicLibrary&apos;" />    </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 不需要别的附加信息来设置该文件。