針對包含獨立原始碼的 Boost 函式庫作者的指南
這些指南是為需要編譯才能使用函式庫的 Boost 函式庫作者所設計。在整份指南中,我們都以虛構的「whatever」函式庫為例,因此在複製範例時,請將所有出現的「whatever」或「WHATEVER」取代為您自己函式庫的名稱。
目錄
影響原始碼的變更
避免編譯器 ABI 衝突
有些編譯器(尤其是 Microsoft Windows 編譯器!)具有一系列會改變 C++ 類別和函式 ABI 的編譯器選項。舉例來說,考慮 Borland 的編譯器,它有以下選項:
-b (on or off - effects enum sizes). -Vx (on or off - empty members). -Ve (on or off - empty base classes). -aX (alignment - 5 options). -pX (Calling convention - 4 options). -VmX (member pointer size and layout - 5 options). -VC (on or off, changes name mangling). -Vl (on or off, changes struct layout).
這些選項是除了那些影響所使用執行階段函式庫的選項之外提供的(稍後會詳細說明);選項的組合總數可以透過將上述個別選項相乘得出,因此總共為 2*2*2*5*4*5*2*2 = 3200 種組合!
問題在於,使用者通常希望能夠建置 Boost 函式庫,然後直接連結它們,並且讓一切都能正常運作,無論他們的專案設定是什麼。無論這是否是合理的期望,如果沒有任何方法可以管理這個問題,使用者可能會發現他們的程式在執行階段會出現奇怪且難以追蹤的崩潰,除非他們連結的函式庫是以與他們的專案相同的選項建置的(變更預設對齊設定是一個主要原因)。管理這個問題的一種方法是使用「前綴和後綴」標頭:這些標頭會調用編譯器特定的 #pragma 指令,指示編譯器後續的任何程式碼都以特定的編譯器 ABI 設定建置(或將要建置)。
Boost.config 提供了巨集 BOOST_HAS_ABI_HEADERS,只要有適用於所使用編譯器的前綴和後綴標頭時,就會設定此巨集,在標頭中的典型用法如下:
#ifndef BOOST_WHATEVER_HPP #define BOOST_WHATEVER_HPP #include <boost/config.hpp> // this must occur after all of the includes and before any code appears: #ifdef BOOST_HAS_ABI_HEADERS # include BOOST_ABI_PREFIX #endif // // this header declares one class, and one function by way of examples: // class whatever { // details. }; whatever get_whatever(); // the suffix header occurs after all of our code: #ifdef BOOST_HAS_ABI_HEADERS # include BOOST_ABI_SUFFIX #endif #endif
如果您願意,也可以將此程式碼包含在您的函式庫原始碼檔案中,但您可能不需要這麼做。
- 如果您沒有在函式庫原始碼檔案中使用這些標頭(但在函式庫的標頭中有使用),並且使用者嘗試使用非預設 ABI 設定編譯函式庫原始碼,那麼如果發生任何衝突,他們就會收到編譯器錯誤。
- 如果您有將它們包含在函式庫的標頭和函式庫原始碼檔案中,那麼無論使用什麼編譯器設定,程式碼都應該始終可以編譯,但結果可能與使用者預期的不符:因為我們已將 ABI 強制恢復為預設模式。
原理
如果沒有任何方法可以管理這個問題,使用者通常會回報類似「當我嘗試呼叫時,您的愚蠢函式庫總是崩潰」之類的問題。這些問題可能非常難以追蹤且耗時,最後才發現是編譯器設定變更了程式類別和/或函式類型的 ABI,與預先編譯的函式庫中的 ABI 不同。使用前綴/後綴標頭可以盡量減少這個問題,但可能無法完全消除。
反駁論點 #1
信任使用者,如果他們想要 13 位元組對齊 (!) 就讓他們擁有。
反駁論點 #2
前綴/後綴標頭往往會「擴散」到其他 Boost 函式庫 - 例如,如果 boost::shared_ptr<> 形成您類別 ABI 的一部分,那麼在您的程式碼中包含前綴/後綴標頭將無濟於事,除非 shared_ptr.hpp 也使用它們。僅限標頭的 Boost 函式庫作者可能不太喜歡這個解決方案 - 這有一定道理 - 因為他們不會面臨相同的問題。
靜態或動態函式庫
當使用者的執行階段為動態連結時,Boost 函式庫可以建置為動態函式庫(Unix 平台上的 .so,Windows 上的 .dll)或靜態函式庫(Unix 上的 .a,Windows 上的 .lib)。因此,我們可以選擇預設支援哪一種。
- 在 Unix 平台上,程式碼通常沒有差別:使用者只需在他們的 makefile 中選擇他們喜歡連結的函式庫即可。
- 在 Windows 平台上,程式碼必須經過特別的註解才能支援 DLL,因此我們需要選擇一個選項作為預設值,另一個作為替代方案。
- 在 Windows 平台上,我們可以插入特殊程式碼來自動選擇要連結的函式庫變體:因此,我們再次需要決定哪個是預設值(請參閱以下關於自動連結的部分)。
建議預設選擇靜態連結。
原理
這裡沒有一個適用於所有情況的策略。
目前行為的原理繼承自 Boost.Regex(及其前身 regex++):這個函式庫最初在執行階段為動態時,預設使用動態連結。如果您例如從 dll 使用 regex,實際上這樣做更安全。然而,這種行為帶來了持續不斷的使用者抱怨:主要是關於部署,都詢問是否可以將靜態連結作為預設值。在 regex 變更行為後,抱怨停止了,作者也沒有收到任何關於預設選擇靜態連結是錯誤選擇的抱怨。
請注意,其他函式庫可能需要做出其他選擇:例如,旨在用於實作 dll 外掛的函式庫在幾乎所有情況下都需要使用動態連結。
支援 Windows DLL
在大多數類似 Unix 的平台上,不需要對原始碼進行特殊註解,以便將該原始碼編譯為共享函式庫,因為所有外部符號都會公開。然而,大多數 Windows 編譯器要求要從 dll 匯入或匯出的符號,必須加上 __declspec(dllimport) 或 __declspec(dllexport) 的前綴。如果沒有對原始碼進行此修改,就不可能在 Windows 上正確建置共享函式庫(歷史記錄 - 最初這些宣告修飾符在 16 位元 Windows 上是必需的,因為匯出類別的記憶體配置與「本機」類別的記憶體配置不同 - 雖然這不再是一個問題,但仍然沒有辦法指示連結器「匯出所有內容」,64 位元 Windows 是否會重新開啟導致此問題的區段式架構仍有待觀察。另請注意,匯出符號的修改名稱與非匯出符號的名稱不同,因此需要 __declspec(dllimport) 才能連結到 dll 中的程式碼)。
為了支援在 MS Windows 上建置共享函式庫,您的程式碼必須為函式庫匯出的所有符號加上巨集的前綴(讓我們稱之為 BOOST_WHATEVER_DECL),該巨集將根據您的函式庫的建置或使用方式,定義為擴充為 __declspec(dllexport) 或 __declspec(dllimport) 或不擴充。典型的用法如下所示:
#ifndef BOOST_WHATEVER_HPP #define BOOST_WHATEVER_HPP #include <boost/config.hpp> #ifdef BOOST_HAS_DECLSPEC // defined in config system // we need to import/export our code only if the user has specifically // asked for it by defining either BOOST_ALL_DYN_LINK if they want all boost // libraries to be dynamically linked, or BOOST_WHATEVER_DYN_LINK // if they want just this one to be dynamically liked: #if defined(BOOST_ALL_DYN_LINK) || defined(BOOST_WHATEVER_DYN_LINK) // export if this is our own source, otherwise import: #ifdef BOOST_WHATEVER_SOURCE # define BOOST_WHATEVER_DECL __declspec(dllexport) #else # define BOOST_WHATEVER_DECL __declspec(dllimport) #endif // BOOST_WHATEVER_SOURCE #endif // DYN_LINK #endif // BOOST_HAS_DECLSPEC // // if BOOST_WHATEVER_DECL isn't defined yet define it now: #ifndef BOOST_WHATEVER_DECL #define BOOST_WHATEVER_DECL #endif // // this header declares one class, and one function by way of examples: // class BOOST_WHATEVER_DECL whatever { // details. }; BOOST_WHATEVER_DECL whatever get_whatever(); #endif然後,在這個函式庫的原始碼中,將使用:
// // define BOOST_WHATEVER SOURCE so that our library's // setup code knows that we are building the library (possibly exporting code), // rather than using it (possibly importing code): // #define BOOST_WHATEVER_SOURCE #include <boost/whatever.hpp> // class members don't need any further annotation: whatever::whatever() { } // but functions do: BOOST_WHATEVER_DECL whatever get_whatever() { return whatever(); }
匯入/匯出相依性
除了匯出主要類別和函式(實際記載的那些)之外,如果您嘗試匯入/匯出相依性未匯出的類別,Microsoft Visual C++ 也會大聲且頻繁地發出警告。相依性包括:任何基底類別、任何用作資料成員的使用者定義類型,以及所有相依性的相依性等等。當相依性是範本類別時,這會造成特殊問題,因為雖然在技術上可以匯出這些類別,但並不容易,尤其是如果範本本身具有特定於實作細節的相依性。在大多數情況下,最好使用以下程式碼來抑制警告:
#ifdef BOOST_MSVC # pragma warning(push) # pragma warning(disable : 4251 4231 4660) #endif // code here #ifdef BOOST_MSVC #pragma warning(pop) #endif
如果沒有相依性是具有非常數靜態資料成員的(範本)類別,那麼這是安全的,這些類別確實需要匯出,否則程式中將有多個靜態資料成員的副本,那真的非常糟糕。
歷史記錄:在 16 位元 Windows 上,您確實必須匯出所有相依性,否則程式碼無法運作,但由於最新的 Visual Studio .NET 支援匯入/匯出個別成員函式,因此可以合理推斷 Windows 編譯器在匯入/匯出類別時不會做任何惡意的事情 - 例如變更類別的 ABI。
原理
何必如此 - 匯入/匯出機制不是佔用的程式碼比類別本身更多嗎?
這是一個好問題,而且可能是真的,但在某些情況下,函式庫程式碼必須放在共享函式庫中 - 例如,當應用程式由多個 dll 以及可執行檔組成時,並且有多個 dll 連結到同一個 Boost 函式庫 - 在這種情況下,如果函式庫不是動態連結的,並且它包含任何全域資料(即使該資料是函式庫內部私有的),那麼真的會發生很糟糕的事情 - 即使沒有全域資料,我們仍然會得到程式碼膨脹的影響。順帶一提,對於較大的應用程式,將應用程式分割成多個 dll 可能非常有益 - 透過使用 Microsoft 的「延遲載入」功能,應用程式只會載入在任何時候真正需要的部分,給人留下應用程式響應速度更快且載入速度更快的印象。
為什麼預設選擇靜態連結?
在上述的範例中,程式碼假設除非使用者另有要求,否則程式庫將會被靜態連結。大多數使用者似乎比較喜歡這種方式(沒有需要散佈的獨立 DLL,而且整體散佈大小通常也顯著較小:也就是說,您只需為您使用的部分付費,不多也不少),但這是一個主觀的選擇,而且某些程式庫甚至可能只有動態版本(例如 Boost.threads)。
使用 auto_link.hpp 自動選擇與連結程式庫
許多 Windows 編譯器都附帶多個執行階段程式庫 - 例如,Microsoft Visual Studio .NET 就有 6 個版本的 C 和 C++ 執行階段。使用者連結的 Boost 程式庫必須與程式所建置的 C 執行階段相同,這一點至關重要。如果情況並非如此,則使用者最糟的情況下會遇到連結器錯誤,最糟的情況下則會發生執行階段崩潰。Boost 建置系統透過提供不同的建置變體來管理這個問題,每個變體都是針對不同的執行階段建置的,並且會根據其針對哪個執行階段建置而獲得略有不同的混雜名稱。例如,當使用 Visual Studio .NET 2003 建置時,regex 程式庫的命名方式如下:
boost_regex-vc71-mt-1_31.lib boost_regex-vc71-mt-gd-1_31.lib libboost_regex-vc71-mt-1_31.lib libboost_regex-vc71-mt-gd-1_31.lib libboost_regex-vc71-mt-s-1_31.lib libboost_regex-vc71-mt-sgd-1_31.lib libboost_regex-vc71-s-1_31.lib libboost_regex-vc71-sgd-1_31.lib
現在的困難在於選擇使用者應將其程式碼連結到哪個程式庫。
相反地,大多數 Unix 編譯器通常只有一個執行階段(或者,如果有一個單獨的執行緒安全選項,則有時會有兩個)。對於這些系統而言,選擇正確程式庫變體的唯一選擇是他們是否想要除錯資訊,以及可能有的執行緒安全。
在過去,Microsoft Windows 編譯器透過提供 #pragma 選項來管理這個問題,該選項允許程式庫的標頭自動選擇要連結的程式庫。這讓一切都變成自動化的,並且對最終使用者來說非常容易:只要他們包含具有獨立原始碼的標頭檔,正確程式庫建置變體的名稱就會嵌入物件檔中,並且只要該程式庫位於連結器搜尋路徑中,它就會被連結器提取,而無需任何使用者干預。
透過包含標頭 <boost/config/auto_link.hpp>,即可為 Boost 程式庫啟用自動程式庫選擇和連結,首先要定義 BOOST_LIB_NAME,如果適用,則定義 BOOST_DYN_LINK。
// // Automatically link to the correct build variant where possible. // #if !defined(BOOST_ALL_NO_LIB) && !defined(BOOST_WHATEVER_NO_LIB) && !defined(BOOST_WHATEVER_SOURCE) // // Set the name of our library, this will get undef'ed by auto_link.hpp // once it's done with it: // #define BOOST_LIB_NAME boost_whatever // // If we're importing code from a dll, then tell auto_link.hpp about it: // #if defined(BOOST_ALL_DYN_LINK) || defined(BOOST_WHATEVER_DYN_LINK) # define BOOST_DYN_LINK #endif // // And include the header that does the work: // #include <boost/config/auto_link.hpp> #endif // auto-linking disabled
程式庫的使用者文件應註明,可以透過定義 BOOST_ALL_NO_LIB 或 BOOST_WHATEVER_NO_LIB 來停用此功能。
如果出於任何原因您需要除錯此功能,如果您先定義 BOOST_LIB_DIAGNOSTIC,標頭 <boost/config/auto_link.hpp> 將會輸出一些有用的診斷訊息。
影響建置系統的變更
建立程式庫 Jamfile
用於建置程式庫「whatever」的 Jamfile 通常位於 boost-root/libs/whatever/build 中,唯一需要額外執行的步驟是將 <define> 需求新增至程式庫目標,以便您的程式碼知道它正在建置 DLL 還是靜態程式庫,典型的 Jamfile 看起來會像這樣
lib boost_regex : ../src/whatever.cpp : <link>shared:<define>BOOST_WHATEVER_DYN_LINK=1 ;
測試自動連結
測試自動連結功能有些複雜,並且需要存取支援此功能的編譯器:請參閱 libs/config/test/link/test/Jamfile.v2 以取得範例。