支持库

摘要

本文档提供了一些关于 LLVM 支持库的详细信息,该库位于源代码的 lib/Supportinclude/llvm/Support 目录下。该库的目的是使 LLVM 免受少数 LLVM 需要操作系统提供的服务的操作系统差异的影响。LLVM 的大部分代码都是使用标准 C++ 的可移植性特性编写的。但是,在少数几个领域,需要使用系统相关的功能,而支持库则是这些系统调用的包装器。

通过集中 LLVM 对操作系统接口的使用,我们可以使 LLVM 工具链和运行时库更容易移植到新的平台,因为(理论上)只需要移植 lib/Support。该库还可以使 LLVM 的其余部分免受 #ifdef 使用和特定操作系统的特殊情况的影响。此类用法将替换为对 include/llvm/Support 中提供的接口的简单调用。

请注意,支持库并非旨在成为一个完整的操作系统包装器(例如自适应通信环境 (ACE) 或 Apache 可移植运行时 (APR)),而只是提供支持 LLVM 所需的功能。

支持库最初被称为系统库,由 Reid Spencer 编写,他基于源自可扩展编程系统 (XPS) 的类似工作制定了设计。许多人帮助完成了这项工作;特别是 Jeff Cohen 和 Henrik Bach 参与了 Win32 移植。

保持 LLVM 的可移植性

为了保持 LLVM 的可移植性,LLVM 开发人员应该遵守与支持库相关的一套可移植性规则。遵守这些规则应该有助于支持库实现其目标,即保护 LLVM 免受操作系统接口变化的影响,并以高效的方式实现这一目标。以下部分定义了实现此目标所需的规则。

不要包含系统头文件

除了 lib/Support 之外,任何 LLVM 源代码都不应直接 #include 系统头文件。在开发 lib/Support 的过程中,已经小心地从 LLVM 中删除了所有此类 #includes。具体来说,这意味着像“unistd.h”、“windows.h”、“stdio.h”和“string.h”这样的头文件被禁止包含在 lib/Support 实现之外的 LLVM 源代码中。

要获得系统相关的功能,应使用 include/llvm/Support 中找到的现有系统接口。如果合适的接口不可用,则应将其添加到 include/llvm/Support 中,并在 lib/Support 中为所有支持的平台实现它。

不要公开系统头文件

支持库必须保护 LLVM 免受**所有**系统头文件的影响。要获得系统级功能,LLVM 源代码必须 #include "llvm/Support/Thing.h" 且仅此而已。这意味着 Thing.h 不能公开任何系统头文件。这可以防止 LLVM 意外使用系统特定功能,并且仅允许通过 lib/Support 接口使用它。

使用标准 C 头文件

**标准** C 头文件(以“c”开头)允许通过 lib/Support 接口公开。这些头文件及其声明的内容被认为是平台无关的。LLVM 源文件可以直接包含它们,或者通过 lib/Support 接口包含它们。

使用标准 C++ 头文件

来自标准 C++ 库和标准模板库的**标准** C++ 头文件可以通过 lib/Support 接口公开。这些头文件及其声明的内容被认为是平台无关的。LLVM 源文件可以包含它们,或者通过 lib/Support 接口包含它们。

高级接口

lib/Support 接口中指定的入口点必须旨在完成 LLVM 所需的一些合理的高级任务。我们不想仅仅包装每个操作系统调用。最好是包装几个始终与 LLVM 结合使用的操作系统调用。

例如,考虑执行程序、等待其完成并返回其结果代码需要什么。在 Unix 上,这涉及以下操作系统调用:getenvforkexecvewait。对于 lib/Support 来说,正确的方法是提供一个函数,例如 ExecuteProgramAndWait,它完全实现了该功能。我们不希望的是对所涉及的操作系统调用的包装器。

操作系统调用与支持库的接口之间**不能**存在一对一的关系。任何此类接口函数都将是可疑的。

没有未使用的功能

lib/Support 接口中指定的任何功能都不应被 LLVM 实际使用。我们在这里不是编写一个通用的操作系统包装器,而只是满足 LLVM 的需求。而且,LLVM 的需求并不多。此设计目标旨在使 lib/Support 接口保持简洁易懂,这应该有助于其实际使用和采用。

没有重复的实现

给定平台的函数实现必须且只能编写一次。这意味着如果这些操作系统可以共享相同的实现,则可以将函数的实现应用于多个操作系统。此规则适用于给定操作系统类(例如 Unix、Win32)支持的操作系统集。

没有虚方法

LLVM 可以非常频繁地调用支持库接口。为了使这些调用尽可能高效,我们不鼓励使用虚方法。没有必要使用继承来实现差异,这只会增加复杂性。#include 机制可以很好地工作。

没有公开的函数

系统库定义的任何函数(即不是由 lib/Support 定义的)都不应通过 lib/Support 接口公开,即使该函数的头文件未公开也是如此。这可以防止无意中使用系统特定功能。

例如,stat 系统调用因其提供的数据存在差异而臭名昭著。lib/Support 不得声明 stat 也不得允许其被声明。相反,它应该提供自己的接口来发现有关文件和目录的信息。这些接口可以使用 stat 来实现,但这严格来说是一个实现细节。支持库提供的接口必须在所有平台上实现(即使那些没有 stat 的平台)。

没有公开的数据

系统库定义的任何数据(即不是由 lib/Support 定义的)都不应通过 lib/Support 接口公开,即使该函数的头文件未公开也是如此。与函数一样,这可以防止无意中使用可能并非所有平台都存在的数据。

最大限度地减少软错误

操作系统接口通常会为可能出错的每一件小事提供错误结果。在几乎所有情况下,您可以将这些错误结果分为两组:正常/好/软和异常/坏/硬。也就是说,一些错误只是信息,例如“文件未找到”、“权限不足”等,而其他错误则要困难得多,例如“空间不足”、“磁盘扇区错误”或“系统调用中断”。我们将第一组称为“”错误,第二组称为“”错误。

lib/Support 必须始终尝试最大限度地减少软错误。这是一个设计要求,因为软错误的最小化会影响接口的粒度和性质。一般来说,如果您发现自己想要抛出软错误,则必须检查接口的粒度,因为您可能正在尝试实现一些级别太低的错误。经验法则是提供**无法**失败的接口函数,除非遇到硬错误。

举一个简单的例子,假设我们要添加一个“OpenFileForWriting”函数。对于许多操作系统,如果文件不存在,尝试打开文件将产生错误。但是,lib/Support 不应在发生这种情况时简单地抛出该错误,因为这是一个软错误。问题在于接口函数 OpenFileForWriting 的级别太低。它应该是 OpenOrCreateFileForWriting。在软“不存在”错误的情况下,此函数将创建它,然后将其打开以进行写入。

这个设计原则需要在lib/Support中维护,因为它避免了软错误处理在 LLVM 其余部分的传播。硬错误通常会导致 LLVM 工具终止,所以不要犹豫地抛出它们。

经验法则

  1. 不要抛出软错误,只抛出硬错误。

  2. 如果你想抛出一个软错误,重新考虑一下接口。

  3. 在内部处理最常见的正常/良好/软错误情况,这样 LLVM 的其他部分就不必处理了。

无抛出规范

任何lib/Support接口函数都不能声明 C++ throw()规范。此要求确保编译器不会将额外的异常处理代码插入接口函数中。这是一个性能考虑因素:lib/Support函数位于许多调用链的底部,因此可能被频繁调用。我们需要它们尽可能高效。但是,系统库中的任何例程都不应该实际抛出异常。

代码组织

Support 库接口的实现按其操作系统的一般类别进行分离。目前仅定义了 Unix 和 Win32 类,但可以为其他操作系统分类添加更多类。为了区分要编译哪个实现,lib/Support中的代码使用了LLVM_ON_UNIX_WIN32 #defines。每个lib/Support中的源文件,在实现通用(与操作系统无关)功能后,需要使用一组#if defined(LLVM_ON_XYZ)指令包含正确的实现。例如,如果我们有lib/Support/Path.cpp,我们期望在该文件中看到

#if defined(LLVM_ON_UNIX)
#include "Unix/Path.inc"
#endif
#if defined(_WIN32)
#include "Windows/Path.inc"
#endif

lib/Support/Unix/Path.inc中的实现应该处理所有 Unix 变体。在lib/Support/Windows/Path.inc中的实现应该处理所有 Windows 变体。这样做的目的是快速增加将提供实现的操作系统基本类别。给定平台的具体细节仍然必须通过使用#ifdef来确定。

一致的语义

lib/Support接口的实现可能在不同平台之间差异很大。只要接口函数的最终结果相同,这就可以。例如,创建目录的函数在所有操作系统上都非常简单。另一方面,System V IPC 甚至并非所有平台都支持。lib/Support不应该“支持”System V IPC,而应该提供一个用于进程间通信基本概念的接口。如果可用,实现可以使用 System V IPC 或命名管道,或者任何可以有效地完成给定操作系统工作的方案。在所有情况下,接口和实现都必须在语义上保持一致。