包含独立源文件的 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。