在 Visual Studio 本地引用 Boost (MSBuild)

jopen 9年前發布 | 35K 次閱讀 .NET開發 Visual Studio

在 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 不需要別的附加信息來設置該文件。


 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!