含有独立源代码的 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 ライブラリを構築してリンクするだけで、プロジェクトの設定に関係なく、すべてが正常に動作することを期待しています。これが妥当な期待であるかどうかとは無関係に、この問題に対処する方法がなければ、ユーザーは、リンク先のライブラリがプロジェクトと同じオプションで構築されていない限り、実行時に奇妙で追跡が困難なクラッシュが発生する可能性があります(デフォルトの配置設定の変更が主な原因です)。これを管理する方法の 1 つは、「プレフィックスおよびサフィックス」ヘッダーです。これらのヘッダーはコンパイラ固有の #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 以及类和功能类型,使其与预编译库中的类型不同。前缀/后缀头的使用可以最大程度减少此问题,尽管可能无法完全消除它。
反对论点 1
相信用户,如果他们需要 13 字节对齐,那就给他们吧。
反对论点 2
前缀/后缀头容易“蔓延”到其他 boost 库 - 例如,如果 boost::shared_ptr<> 构成您类的 ABI 的一部分,那么在您的代码中包含前缀/后缀头将没有任何用处,除非 shared_ptr.hpp 也使用它们。头部仅限 boost 库的作者可能不太愿意接受此解决方案(有其合理性),因为他们不必面对同样的问题。
静态或动态库
当用户运行时动态链接时,Boost 库既可以构建为动态库(在 Unix 平台上的 .so,在 Windows 上的 .dll),也可以构建为静态库(在 Unix 上的 .a,在 Windows 上的 .lib)。因此我们有权选择默认支持哪一种库
- 在 Unix 平台上,它通常不会改变代码:用户只需在其 makesfile 中选择他们倾向于链接到哪个库。
- 在 Windows 平台上,必须专门注释代码才能支持 DLL,因此我们需要选择一个选项作为默认选项,另一个选项作为备选选项。
- 在 Windows 平台上,我们可以注入特殊代码来自动选择要链接到哪个库变体:因此我们再次需要确定哪个应该是默认库(请参阅有关自动链接的章节)。
建议默认选择静态链接。
基本原理
这里没有任何一项政策能够完全合适。
当前行为背后的原理源自 Boost.Regex(以及其前辈 regex++):此库最初默认使用动态链接(只要运行时具有动态性)。实际上,如果你正在从 dll 中使用正则表达式,在这种情况下这种方式更安全。但是,该行为带来了持续的用户投诉:主要关于部署问题,都询问静态链接是否可以作为默认值。在 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 运行时是同一个 C 运行时至关重要。如果不相同,那么在最好的情况下,用户将遇到链接器错误,在最坏的情况下将发生运行时崩溃。Boost 构建系统通过提供不同的构建变体来管理此问题,每种构建变体针对不同的运行时进行构建,并根据其构建所针对的运行时而获得略有不同的整理名称。例如,使用 Visual Studio .NET 2003 构建时,正则表达式库被命名如下
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/config/auto_link.hpp> 将在您首先定义 BOOST_LIB_DIAGNOSTIC 后输出一些有帮助的诊断消息。
影响构建系统的更改
创建库 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。