在 Visual Studio (MSBuild) 中引用原生项目。






4.40/5 (6投票s)
将 Boost 库集成到 Visual Studio (MSBuild) 的构建和引用系统中。
引言
这是关于将第三方工具和库集成到 Visual Studio 系列的第四篇文章。在我第一篇文章中,我解释了如何为 Visual Studio 属性对话框创建自定义属性页。 第二篇文章介绍了属性页的内部结构和元素。 第三篇文章通过构建 Boost 库的示例,解释了如何创建自定义构建。在这第四篇文章中,我将解释如何将自定义构建集成到 Visual Studio 项目引用系统中。
原理
每个 C++ 项目都由几个小的子项目和库组成。这些项目和库在编译或运行时被链接,并且应该被正确地引用。如果所有项目都在 Visual Studio (MSBuild) 中创建,那么引用就由 MSBuild 处理。但是,当项目或库来自外部时,我们必须 resort 到手动配置才能正确集成。
理想情况下,我们应该能够通过在 Visual Studio 中添加对项目的引用来将项目集成到 MSBuild 中。
如果我们能对任何库做到这一点,那不是很好吗?所有 lib 文件都会自动添加到 LINK 命令中,所有 DLL 都会被复制到输出目录,以便在运行时链接?能不能不仅调试我们自己的代码,还能调试库代码?
在这篇文章中,我将向您展示如何实现这一点。我将使用 Boost 库来演示如何将其集成到任何项目中,而无需手动操作库或设置路径。我假设您已经知道如何构建 Boost。如果不知道,请阅读这篇文章。
背景
当 Visual Studio 将一个项目引用到另一个项目时,它会在主项目中添加一个记录,如下所示:
<ProjectReference Include="...\boost.vcxproj">
<Project>{9cd23c68-ba74-4c50-924f-2a609c25b7a0}</Project>
...
</ProjectReference>
有关引用如何添加的详细信息,请遵循此链接。
在主项目构建期间,MSBuild 会尝试解析并构建 `ProjectReference` 部分中列出的所有项目依赖项。它会定位列出的子项目,并对每个子项目调用以下 Target 来收集所需信息:
GetTargetPath GetNativeManifest GetResolvedLinkLibs GetCopyToOutputDirectoryItems
我将简要解释每个 Target 的作用。
GetTargetPath
此 Target 返回项目构建的程序集/库的完整路径。在设计时,Visual Studio 使用此文件来确定引用是否正确以及是否可以找到输出文件。如果程序集是托管类型的,它还会查询它以获取额外信息。
理论上,只要此路径指向一个存在的文件,引用系统就会认为它是有效的,并报告引用有效。我们可以使用它并返回任意文件的路径来指示引用是 OK 的。我决定返回 Jamroot 文件的路径来指示此构建使用哪个源来创建库。
<Target Name="GetTargetPath" DependsOnTargets="GetNativeTargetPath" Returns="@(NativeTargetPath)" > <ItemGroup> <NativeTargetPath Include="$(BoostRoot)\Jamroot" Condition="'$(DesignTimeBuild)' == 'true'" /> </ItemGroup> </Target>
GetNativeTargetPath
原生项目必须实现 `GetNativeTargetPath` 来返回输出文件的列表。在运行时会调用此 Target,并返回项目构建的 DLL 库列表以及它绑定的所有依赖 DLL。
<Target Name="GetNativeTargetPath" DependsOnTargets="GetBuiltLibs" Returns="@(NativeTargetPath)" >
<MSBuild Projects="@(ProjectReference)" Targets="GetNativeTargetPath" >
<Output ItemName="NativeTargetPath" TaskParameter="TargetOutputs"/>
</MSBuild>
<ItemGroup>
<NativeTargetPath Include="$(OutputDir)\lib\*boost*%(BuiltLibs.Identity)*.dll" />
</ItemGroup>
</Target>
代码会为它引用的每个项目调用 GetNativeTargetPath,并添加在构建过程中创建的自己的 DLL。
值得注意的是,依赖 DLL 可以与 Boost 文件一起本地复制到主项目的输出目录,这取决于在“添加引用”对话框中设置的选项。
将 **Copy Local** 设置为 `true` 也会复制依赖项。
GetNativeManifest
如果由于某种原因,子项目必须单独分发 Manifest 文件及其库,则此 Target 会返回清单文件的列表。父项目将简单地将这些清单复制到输出目录。
Boost 不需要任何清单,所以不会执行任何操作。
<Target Name="GetNativeManifest" />
GetResolvedLinkLibs
此 Target 返回项目公开的所有链接库以及它链接到的所有库的列表。此列表将被添加到 LINK 命令中,以便主项目可以解析和链接这些 lib 文件。Boost 库的每个模块构建都会有一个 lib 文件。
为了返回正确的库列表,我们需要获取当前配置所需的库列表,然后将它们解析为实际的 lib 文件。我们分两步完成:
- 使用当前选项调用 **b2** 并附带 **--show-libraries** 命令 (
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>
<ProjectType Condition="'$(boost-link)'!=''">$(boost-link)</ProjectType>
<FileType>lib</FileType>
<ResolveableAssembly>false</ResolveableAssembly>
</LibFullPath>
</ItemGroup>
<MSBuild Projects="@(ProjectReference)" Targets="GetResolvedLinkLibs" >
<Output ItemName="LibFullPath" TaskParameter="TargetOutputs"/>
</MSBuild>
</Target>
重要的是要注意,返回的项目应该添加元数据。此元数据用于构建引擎以确定如何链接到这些文件。每个项目至少应设置 `ProjectType` 和 `FileType`。
ProjectType 可以是 **StaticLibrary** 或 **DynamicLibrary**。 FileType 可以包含 **lib** 或 **dll**。
GetCopyToOutputDirectoryItems
此 Target 返回所有需要复制到主项目输出文件夹的内容文件的列表。它可以是任何类型的文件。对于 Boost 库,我们没有任何内容。
<Target Name="GetCopyToOutputDirectoryItems" />
现在,如果我们添加 boost 项目作为引用,它将注册为有效,并提供父项目可能需要的所有信息。
使用 Boost 构建
我们从创建一个名为 **Sample** 的简单的控制台应用程序开始。大家都知道如何在 Visual Studio 中创建控制台应用程序,所以我将跳过说明。
将项目 **boost** 添加到解决方案。
转到 Sample 项目的属性,然后添加对 boost 项目的引用。您应该看到类似这样的内容:
正如您在此图中所见,项目 **boost** 已正确引用,并指向安装在 D:\Boost 目录中的 Boost 库。程序集名称、区域性、版本和描述不可用,因为 Boost 不是托管程序集。
值得注意的是,**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="%(RootDir)%(Directory)%(Filename).dll"
Condition="'@(BoostOutputs0>Filename->StartsWith("boost_"))'=='true' And
'%(BoostOutputs.Library)'!='' And '$(boost-link)'=='DynamicLibrary'" />
</ItemGroup>
</Target>
如上所示,我们从 `GetBuiltLibs` Target 获取库列表,并搜索所有名称遵循以下模式的 lib 文件:*boost*<library-name>*.lib
。它返回一个列表,其中包含 DLL 的链接库以及静态库。
在下一步中,我们在库列表与已构建文件之间创建一个内连接,并使用它来过滤掉缺失的库。
接下来,我们将缺失的库添加到 `BoostOutputs` 以触发构建(如果需要)。
在下一步中,我们添加 dll 库。
此列表将由 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;
}
在 (Sample.cpp) 的第 59 行的 `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()` 将在第 445 行打开 thread.cpp。
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 不需要任何额外信息来解析该文件。
使用代码
我提供了两个下载:**Sample** 和 **Source**。
**Source download** 包含构建 Boost 库所需的三个文件(项目、目标和 xml)。将其复制到一个目录中。指定 Boost 库的根目录并构建。
**Sample download** 包含一个包含两个项目的解决方案:Boost-MSBuild 和 Sample。构建并执行后,它们将允许您实际看到本文所述的功能。
历史
03/18/2015 - 发布
03/20/2015 - 修正了一些遗漏
03/31/2015 - 添加了原生项目的信息