其中大多数都与确保跨所有 POSIX 和类 POSIX 系统的可移植性有关。 广泛的可移植性不仅仅是一种值得称道的专业精神和黑客式的礼貌,它还是应对 Linux 未来变化的宝贵保障。
最后,其他人会尝试在非 Linux 系统上构建你的代码; 可移植性减少了你收到的支持邮件数量。
选择一种开发语言,使其能够最大限度地减少将在其中运行的底层环境的差异。 C/C++ 程序可能比 Fortran 更具可移植性。 Java 比 C/C++ 更可取,而像 Perl、Python 或 Ruby 这样的高级脚本语言是最佳选择(因为脚本语言只有一个跨平台实现)。
符合条件的脚本语言包括 Python、Perl、Tcl、Emacs Lisp 和 PHP。 历史悠久的 Bourne shell (/bin/sh) 不符合条件; 存在太多具有细微差异的不同实现,并且 shell 环境容易受到用户自定义(例如 shell 别名)的干扰。
如果你选择 C/C++ 这样的编译型语言,请不要使用编译器扩展(例如在堆栈上分配可变长度数组,或通过 void 指针进行间接寻址)。 定期使用尽可能多的不同编译器进行构建,并进行测试。
开源发行版的一个显着优势是它们允许每个源代码包在编译时适应它所发现的环境。 你需要做出的选择之一是“构建系统”,即你(和其他人)将依赖的工具包,用于将你的源代码转换为可执行文件。
你的构建脚本不能做的一件事是在编译时向用户询问系统信息。 安装软件包的用户不一定知道你问题的答案,因此这种方法从一开始就注定要失败。 你的软件(调用构建系统工具)必须能够自行确定在编译时或安装时可能需要的任何信息。
社区对构建系统中最佳实践的看法目前(2010 年初)正处于某种动荡状态。
本 HOWTO 的先前版本敦促使用 GNU autotools 来处理可移植性问题、进行系统配置探测以及定制你的 makefile。 这仍然是标准且最流行的方法,但它正变得越来越成问题,因为 GNU autotools 正在显现其老态。 Autotools 一直是一堆建立在 hacks 之上的 crocks 之上的 kluges,以混乱的语言混合实现,并存在一些严重的设计缺陷。 由此产生的混乱在许多年里是可以容忍的,但随着项目变得更加复杂,它正变得越来越失败。
尽管如此,从 C 源代码构建的人们仍然希望能够键入“configure; make; make install”并获得干净的构建。 假设你选择一个非 autotools 系统,你可能需要模拟这种行为(这应该很容易)。
这里有一个关于 autotools 的优秀教程:这里。
取代 autotools 的竞赛尚未决出胜负,但它有一个领跑者:SCons。 SCons 废除了 makefile; 它将 autotools 构建序列的“configure”和“make”部分合并为一个步骤。 它在 Unix/Linux、Maoc OS X 和 Windows 上提供使用单个配方的跨平台构建。 它用 Python 编写,可以用 Python 扩展,并且在某种程度上搭上了该语言日益流行的顺风车。
SCons 仍然是一个少数派选择,并且面临来自其他几个竞争者的激烈竞争,其中 CMake 和 WAF 可能是最突出的。 关于公平的跨平台比较,考虑到来源,可以在 SCons wiki 上找到。
一个好的测试套件允许团队购买廉价的硬件进行测试,然后在发布前轻松运行回归测试。 创建一个强大、可用的测试框架,以便你可以逐步向你的软件添加测试,而无需培训开发人员掌握测试套件的复杂性。
分发测试套件允许用户社区在将他们的移植贡献回组之前测试他们的移植。
鼓励你的开发人员使用各种各样的平台作为他们的桌面和测试机器,以便在正常开发过程中不断测试代码的可移植性缺陷。
如果你正在使用 GCC 编写 C/C++,请使用 -Wall 进行测试编译,并在每次发布前清理所有警告消息。 使用你能找到的每个编译器编译你的代码——不同的编译器通常会发现不同的问题。 特别是,在真正的 64 位机器上编译你的软件。 底层数据类型可能会在 64 位机器上发生变化,你通常会在那里发现新的问题。 找到 UNIX 供应商的系统并在你的软件上运行 lint 实用程序。
运行用于内存泄漏和其他运行时错误的工具; Electric Fence 和 Valgrind 是开源领域中两个不错的选择。
对于 Python 项目,PyChecker 程序可能是一个有用的检查工具。 它尚未脱离 beta 版,但仍然经常捕获重要的错误。
如果你正在编写 Perl,请使用 perl -c(如果适用,可以使用 -T)检查你的代码。 虔诚地使用 perl -w 和 'use strict'。(有关进一步讨论,请参阅 Perl 文档。)
拼写检查你的文档、README 文件和软件中的错误消息。 粗心的代码、编译时产生警告消息的代码以及 README 文件或错误消息中的拼写错误,会让用户认为其背后的工程也是随意而马虎的。
如果你正在编写 C,请随意使用完整的 ANSI 功能。 特别是,请使用函数原型,这将帮助你发现跨模块的不一致性。 旧式的 K&R 编译器已成为历史。
不要假定可以使用编译器特定的功能,例如 GCC "-pipe" 选项或嵌套函数。 当有人移植到非 Linux、非 GCC 系统时,这些功能会反过来咬你一口。
可移植性所需的代码应隔离到单个区域和一组源文件(例如,“os”子目录)。 编译器、库和具有可移植性问题的操作系统接口应抽象到此目录中的文件中。 这包括诸如“errno”之类的变量、诸如“malloc”之类的库接口以及诸如“mmap”之类的操作系统接口。
可移植性层使进行新的软件移植变得更容易。 通常情况下,开发团队中没有人了解移植平台(例如,实际上有数百种不同的嵌入式操作系统,没有人了解其中任何重要的部分)。 通过创建单独的可移植性层,了解移植平台的人员可以在不必理解你的软件的情况下移植你的软件。
可移植性层简化了应用程序。 软件很少需要 mmap 或 stat 等更复杂的系统调用的全部功能,程序员通常会错误地配置此类复杂的接口。 具有抽象接口(“__file_exists”而不是调用 stat)的可移植性层允许你仅从系统中导出有限的、必要的功能,从而简化应用程序中的代码。
始终编写你的可移植性层以基于特性进行选择,而不是基于平台。 尝试为每个受支持的平台创建单独的可移植性层会导致多重更新问题的维护噩梦。 “平台”始终在至少两个轴上选择:编译器和库/操作系统版本。 在某些情况下,存在三个轴,例如 Linux 供应商独立于操作系统版本选择 C 库时。 借助 M 个供应商、N 个编译器和 O 个操作系统版本,“平台”的数量很快就会扩展到任何但最大的开发团队都无法企及的程度。 通过使用语言和系统标准(例如 ANSI 和 POSIX 1003.1),特性的集合相对受限。
可移植性选择可以沿着代码行或编译文件进行。 在平台上选择备用代码行还是选择几个不同的文件都没有关系。 经验法则是,当不同平台的实现差异很大时(UNIX 与 Windows 上的共享内存映射),将不同平台的可移植性代码移动到单独的文件中,而当差异很小时(使用 gettimeofday、clock_gettime、ftime 或 time 来找出当前时间),则将可移植性代码保留在单个文件中。
避免使用诸如“off_t”和“size_t”之类的复杂类型。 它们的大小因系统而异,尤其是在 64 位系统上。 将你对“off_t”的使用限制在可移植性层中,并将你对“size_t”的使用仅限于表示内存中字符串的长度,而不是其他任何含义。
永远不要踏入系统任何其他部分的命名空间(包括文件名、错误返回值和函数名)。 在命名空间共享的地方,记录你使用的命名空间部分。
选择一种编码标准。 关于标准选择的争论可以永远持续下去——无论如何,维护使用多种编码标准构建的软件都太困难和昂贵了,因此必须选择某种编码标准。 严格执行你的编码标准,因为代码的一致性和整洁性是最高优先级; 编码标准本身的细节则退居其次。