持续数十年的代码

2020-09-02 16:43:35

在与快速发展的编程语言接触了几年之后,我开始欣赏稳定性。我想让我的程序只需最少的调整就可以轻松地构建在各种各样的系统上。我希望随着环境的变化,他们能在未来很长一段时间内继续工作。

为了更清楚地考虑稳定性,让我们将一个正常运行的程序分成几个层。然后,我们可以一层一层地检查开发选择。

程序需要的功能越多,它就必须通过各层延伸到更远的地方。

每种语言都必须从某个地方开始,通常是作为单个人或小团队的实现。在这个阶段,语言发展迅速,公平地说,正是这个阶段推动了艺术的发展。

然而,在语言的单一实现阶段使用它意味着您将一定比例的精力投入到该语言本身的“研究项目”中。您将处理破坏性更改(包括工具)和实验死胡同。

如果你喜欢一门新语言背后的想法,或者相信它是赢家,相信你早期的熟悉会有回报,那么就去做吧!否则,请使用已超越单一实现的高级语言。这样你就可以专注于你的专业领域,而不是紧跟语言研究议程。

当一群人为新的情况和体系结构分叉语言时,语言就会进入下一个阶段。一些人添加了功能,另一些人发现了他们环境中的困难。然后,利益相关者通过标准化过程进行辩论并达成共识。最终结果是标准而不是特定的软件工件定义语言并拥有最终决定权。

当然,整个过程需要一段时间。标准化语言将变得相当陈旧。他们会错过最新的想法,但会被很好地理解。以下是一些具有标准的成熟语言:

我最近一直在使用C,因为它的可移植性、简单(但富有表现力)的抽象机器模型,以及与POSIX和基础库的深度兼容。

如果您使用的是一种有标准的语言,请充分利用它。首先,选择标准的特定版本。旧版本通常得到更广泛的支持,但功能较少。在C世界中,我通常选择C99,因为与C89相比,它有一些便利,而且几乎所有地方都支持C99(尽管只有部分在Windows上)。

请查阅编译器文档,了解编译器是否可以捕获非标准行为的意外使用。在clang或GCC中,将以下标志添加到生成文件中:

根据需要用另一个版本替换“C99”。Pedtic标志拒绝所有使用禁用扩展的程序,以及其他一些不遵循ISO C的程序。

如果您确实想要使用编译器扩展(如GCC或clang中的那些),请将它们包装在您自己的宏中,以便代码保持可移植性。PostgreSQL项目在C.H中做这类事情。下面是一个随机的例子:

/**对于我们*希望强制内联的函数,使用";PG_Attribute_Always_Inline";代替";INLINE";,即使编译器的启发式方法会*选择不这样做。但是,如果可能的话,不要在未经优化的*调试版本中强制内联。*/#IF(Defined(__GNUC__)&;&;__Gnuc__>;3&;&;Defined(__Optimize__))||Defined(__SUNPRO_C)||Defined(__IBMC__)/*GCC>;3、Sunpro和XLC通过__ATTRIBUTE__*/#DEFINE PG_ATTRIBUTE_ALWAYS_INLINE_ATTRIBUTE__((ALWAYS_INLINE))INLINE#ELIF DEFINED(_MSC_VER)/*MSVC为此提供了一个特殊的关键字*/#DEFINE PG_ATTRIBUTE_ALWAYS_INLINE__FORCES#ELSE/*,否则,我们最多只能说";INLINE";*/#DEFINE PG_ATTRIBUTE_ALWAYS_INLINE。

请注意它们如何适应各种编译器并提供最终的后备。当然,在可能的情况下,首先避免扩展是最简单的选择。

花点时间学习您的语言标准库。这是免费的,无论你的程序走到哪里,你都可以得到它。阅读语言标准中的库函数,因为将在那里介绍它们。

无论何时在C标准库之外使用系统调用,都要检查它们是否属于POSIX,以及它们的官方描述是否与本地手册页不同。Open Group提供POSIX.1的免费可搜索HTML版本。在撰写本文时,它是POSIX.1-2017(即POSIX.1-2008加上两个技术勘误)。

还有一个更复杂的问题:POSIX.1-2008(又名“问题7”)并不是所有地方都完全支持。(例如,我发现MacOS不支持pthread障碍、信号量或异步线程取消。)。我认为根本原因是2008需要线程和实时功能,而这在以前是可选的扩展。如果您坚持使用POSIX.1-2001(也就是第6版)中的功能,那么在所有相当新的平台上都应该是安全的。

要调用POSIX函数,必须在包含头文件之前定义_POSIX_C_SOURCE“功能测试”宏。使用下列值之一选择特定的POSIX版本:

头文件根据功能测试宏隐藏或显示功能。例如,第7期中的getline()函数分配内存并读取一行。

/*line.c*/#include<;stdio.h>;#include<;stdlib.h>;#include<;sys/tyes.h>;/*ssize_t*/int main(Void){char*line=null;size_t len=0;ssize_t read;while((read=getline(&;line,&;len,stdin))!=-1)printf(&。,读取,行);释放(行);返回0;}。

$cc-std=c99-pedtic-Werror-D_POSIX_C_SOURCE=200112L line e.c-o line line。c:10:17:错误:在C99[-Werror,-WImplative-Function-Designation]While((read=getline(&;line,&;len,stdin))!=-1)^1中,函数';getline';的隐式声明无效。

重要注意事项:设置_POSIX_C_SOURCE将在标准标头中隐藏非POSIX操作系统附加文件。最佳实践是将源文件分为符合POSIX的源文件和不符合POSIX的源文件(希望不太多),编译后的源文件不带特性宏,最后将它们链接在一起。

POSIX不仅为前面讨论的库函数定义了接口,还为shell和常用工具定义了接口。如果您使用这些工具进行构建,则不需要在目标计算机上安装任何额外的软件来编译您的项目。

意外锁定的最常见来源可能是要进行的bashism和GNU扩展。对于脚本,请使用sh,并使用(POSIX)make for Makefiles。太多项目不必要地使用GNU特性。事实上,学习make特性的可移植子集会带来更干净、更可靠的构建。

这是一整篇文章的主题。克里斯·韦隆(Chris Wellons)就此写了一篇很好的教程。安德鲁·奥拉姆的“用Make管理项目”(ISBN0-937175-90-0)也是一本小书,里面有很多好的建议。

操作系统包含POSIX之外的有用功能。例如对PTHREADS的扩展(设置读取器-写入器偏好或线程处理器亲和性)、对专用硬件(如音频或图形)的访问、替代I/O接口和语义、以及诸如StrcPy或保证的安全功能。

构建静态填充库(“libcompat”)作为项目的一部分,以便在缺少功能时使用,或者。

我们稍后将讨论第三方库。现在让我们来看一下选项一。

考虑一下生成随机数据的示例。它需要操作系统的帮助,因为POSIX只提供伪随机数。

我们将使用配置脚本生成config.mk。开发人员将在第一次构建之前运行该脚本以检测环境选项。要使Configure正常工作,最原始的方法是尝试解析、未命名,并根据它看到的操作系统或发行版做出决定。更准确的方法是尝试直接探测所需的OS C函数。

要查看是否存在C函数,我们只需尝试编译测试代码片段,看看它们是否成功。您可能认为这很笨拙,或者需要用测试代码将您的项目搞得乱七八糟,但实际上它相当优雅。

编译(){Stage=";$(mktemp-d)";ECHO";$2";>;";$Stage/test.c";(cc-Werror";$1";-o";$Stage/test";";$Stage/test.c";>;/dev/null 2>;&;1)cc_SUCCESSRM-RF";$Stage";返回$cc_SUCCESS}。

函数的作用是:接受两个参数:一个可选的编译器标志和要尝试编译的源代码。

让我们使用帮助器来检查操作系统随机数生成器。BSD世界提供arc4Random_buf来获取随机字节,而Linux提供getRandom。配置脚本可以检查每个功能,如下所示:

如果编译";";&#include<;stdint.h>;#include<;stdlib.h>;int main(Void){void(*p)(void*,size_t)=arc4dom_buf;return(Intptr_T)p;}";则ECHO";CFLAGS+=-DHAVE_ARC4RANDOM";>";#include<;stdint.h>;#include<;sys/tyes.h>;#include<;sys/随机.h>;int main(Void){ssize_t(*p)(void*,size_t,unsign int)=getRandom;return(Intptr_T)p;}";然后ECHO";CFLAGS+=-DHAVE_GETRANDOM";&。

看见?。还不算太糟。这些代码片段不仅测试函数是否存在,还检查它们的类型签名。注意第二个示例是如何使用ssize_t类型的POSIX编译的,而第一个示例故意没有标记为符合POSIX,因为这样做会隐藏BSD放在stdlib.h中的额外函数arc4Random_buf。

将不可移植功能的使用隔离在不同的翻译单元中,并在上面导出您自己的接口是很有帮助的。这样一来,在一个地方设置条件编译或在将来重构就更简单了。

让我们继续上一节生成随机字节的示例。有了操作系统功能检测的辛勤工作,我们可以将不同的操作系统接口封装在我们自己的函数后面:

#include<;stdint.h>;#include<;stdlib.h>;#ifdef Have_GETRANDOM#include<;sys/随机性.h>;#endif void get_Random_bytes(void*buf,size_t n){#if Defined Have_ARC4RANDOM/*BSD*/arc4Random_BuF(BUF,n);#Elif Defined Have_GUF

当相应的函数存在时,Makefile使用CFLAGS定义HAVE_ARC4RANDOM或HAVE_GETRANDOM。代码只能使用ifdefs。请注意#Else案例中的#错误,即在不支持的平台上编译失败,并显示一条清晰的消息。

我们努力实现的可移植性程度导致了权衡。示例:我们可以向read/dev/Random添加一个后备。上一节中的配置脚本可以检查设备是否存在:

使用该信息,我们可以在GET_RANDOM_BYTES()中添加另一个#elif,这样它就可以潜在地在更多的系统上工作。然而,在这种情况下,增加的可移植性将需要更改界面。由于/dev/Random上的fopen()或fread()可能会失败,因此我们的函数需要返回bool。目前,我们调用的操作系统函数不会失败,所以返回一个void就可以了。

当然,对可移植性的真正测试是在多个操作系统、编译器和硬件体系结构上构建和运行。看看这能揭示出什么假设,可能会让人感到惊讶。及早测试可移植性,通常会使保持程序井然有序变得更容易。

例如,PostgreSQL项目维护一系列被称为“构建场”的完全不同的机器。构建场成员每个都有自己的操作系统、编译器和体系结构。该团队编译了这些机器上的每一个新功能,并在那里运行测试套件。

即使您不打算在这些架构上运行,在那里进行测试也会产生更好的代码。(请参阅我的文章C“来自怪异机器的可移植性课程”。)。

许多语言都有自己的应用级包管理器,但C语言没有专有的包管理器。这门语言有太多的历史,跨越了太多的环境,不可能锁定在那里。取而代之的是,人们从源代码构建依赖项,或者使用操作系统包管理器。

链接到库需要知道它们的路径、名称和编译器设置。此外,我们还想知道安装的是哪个版本,以及它是否在边界内。因为C没有应用程序级的包管理器,所以我们需要使用另一个工具来发现已安装的库。

查找和构建依赖性库的最具跨平台的方式是pkg-config。该工具允许您查询系统包,而不管它们是如何安装的。为了与pkg-config兼容,每个库foo都提供一个包含如下键和值的libfoo.pc文件:

Pkg-config可执行文件可以查询元数据并为Makefile提供标志。从您的配置脚本调用它,如下所示:

#检查是否安装了足够的版本pkg-config--print-error';libfoo>;=1.0';#将标志保存到config.mk cat>;>;config.mk<;<;-EOF CFLAGS+=$(pkg-config--cflag libfoo)LDFLAGS+=$(pkg-config--libs-only-L libfoo)LDLIBS。

请注意LDLIBS与LDFLAGS的区别。LDLIB是需要放在构建行的最后的选项。默认的POSIX make后缀规则没有提到LDLIBS,但是您可以使用下面的规则:

有时,操作系统会包含额外的功能,并将其打包为可在其他操作系统上使用的便携程序库。在这种情况下,您可以有条件地使用pkg-config。

例如,OpenBSD剥离了LibreSSL项目(一个更易用的OpenSSL)。OpenBSD在内部包含该功能。在配置脚本中,只需执行操作系统检查:

操作系统ECHO';LDLIBS+=-ltls';>;config.mk;*)#附带的#LibreSSL case";$(uname-s)&34;#需要软件包pkg-config--print-error';libtls>;=2.5.0';cat>;config.mk<;<;-EOF CFLAGS+=$(pkg-config--cflag libtls)LDFLAGS+=$(pkg-config--libs-only-L libtls)LDLIBS+=$(pkg-config--libs-only-l libtls)EOFesac。

C标准库没有泛型集合。您必须编写自己的链表、树和哈希表。真正的程序员™可能会喜欢这样,但我不喜欢。

二叉搜索树。虽然twalk()不包含将辅助数据传递给回调的参数,但这个接口对我来说是有效的。为此,回调需要咨询全局或线程局部变量。实施的质量也可能有所不同,可能与树的平衡方式/是否平衡有关。

排队。从双向链接(可能是循环)列表中插入或删除的非常基本的功能。它接受void*,但需要一个结构,该结构的前两个成员是指向相同结构类型(向前和向后指针)的指针。

哈希表。不必要的限制接口。它在隐藏内存中创建单个哈希表。您可以销毁该表,然后再创建另一个表,但在调用堆栈中的任何位置,一次都不能有多个活动的表。显然不是线程安全的,但这似乎是它最小的问题。

要超越这一点,您必须使用第三方库。许多知名的库看起来相当臃肿(Glib、Tbox、Apache Portable Runtime)。我发现了一个更小、更干净的库,名为简单的C算法。虽然还没有在项目中使用过,但是它看起来很稳定并且经过了很好的测试。我也在本地构建了这个库,添加了迂腐的C99标志,没有收到任何警告。

另外两个稳定的库(代码片段?)。这些年来得到了大量使用的是Uthash和BSD的Queue(3)(从OpenBSD浏览Quee.h,或者FreeBSD变体)。

任何C结构都可以使用uthash存储在哈希表中。只需将UT_HASH_HANDLE添加到结构中,然后在结构中选择一个或多个字段作为键即可。然后使用这些宏来存储、检索或删除哈希表中的项目。

BSD队列代码早在20世纪90年代就一直在使用和改进。它提供用于创建和操作单链接列表、简单队列、列表和尾部队列的宏。手册页相当不错。

OpenBSD和FreeBSD的代码库功能不同。我使用的是OpenBSD版本,但是它的功能稍少一些。特别是,FreeBSD添加了STAILQ(单链接尾队列)和列表交换操作。曾经有一个用于循环队列的CIRCLEQ,但它使用了不可靠的编码实践,并被删除。