包含独立源代码的 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 库,然后只需链接到它们,一切就能正常工作,无论他们的项目设置是什么。无论这是否是一个合理的期望,如果没有一些方法来管理这个问题,用户可能会发现,除非他们链接到的库是用与他们的项目相同的选项构建的(对默认对齐设置的更改是罪魁祸首),否则他们的程序可能会在运行时遇到奇怪且难以追踪的崩溃。
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 使用正则表达式,那么这样实际上更安全。但是,这种行为带来了持续不断的用户投诉:主要关于部署,所有用户都询问静态链接是否可以作为默认值。在正则表达式更改行为后,投诉停止了,作者没有收到过关于静态链接作为默认值是错误选择的投诉。
请注意,其他库可能需要做出其他选择:例如,旨在用于实现 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 构建时,正则表达式库的命名如下:
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 获取示例。