驱动程序设计与内部机制¶
简介¶
本文档描述了 Clang 驱动程序。本文档的目的是描述驱动程序的动机和设计目标,以及内部实现的细节。
特性与目标¶
Clang 驱动程序旨在成为一个生产级编译器驱动程序,为 Clang 编译器和工具提供访问,并提供与 gcc 驱动程序兼容的命令行界面。
尽管驱动程序是 Clang 项目的一部分,并由其驱动,但它在逻辑上是一个独立的工具,与 Clang 具有许多相同目标。
GCC 兼容性¶
驱动程序的首要目标是简化 Clang 的采用,允许用户将 Clang 放入为调用 GCC 而设计的构建系统中。尽管这使得驱动程序比可能需要的要复杂得多,但我们认为,与 gcc 命令行界面高度兼容对于允许用户快速在他们的项目中测试 clang 是值得的。
灵活¶
驱动程序的设计目的是灵活且易于适应新的使用方式,随着 clang 和 LLVM 基础设施的增长。例如,驱动程序可以轻松支持引入具有集成汇编程序的工具,我们希望将来将其添加到 LLVM 中。
类似地,驱动程序的大多数功能都保留在一个库中,该库可用于构建想要实现或接受类似 gcc 的界面的其他工具。
低开销¶
驱动程序的开销应该尽可能低。在实践中,我们发现 gcc 驱动程序本身在编译许多小文件时会产生少量但有意义的开销。与编译相比,驱动程序的工作量并不大,但我们尝试通过遵循一些简单的原则来保持其尽可能高效。
尽可能避免内存分配和字符串复制。
不要多次解析参数。
提供一些简单的接口,以高效地搜索参数。
简单¶
最后,驱动程序的设计目的是在其他目标的基础上“尽可能简单”。值得注意的是,尝试完全兼容 gcc 驱动程序会增加大量的复杂性。但是,驱动程序的设计试图通过将流程划分为多个独立的阶段而不是单个整体任务来缓解这种复杂性。
内部设计与实现¶
内部机制简介¶
为了满足所述目标,驱动程序被设计为完全取代 gcc 可执行文件的全部功能;也就是说,驱动程序不应该需要委托给 gcc 来执行子任务。在 Darwin 上,这意味着 Clang 驱动程序也取代了 gcc 驱动程序驱动程序,该驱动程序用于实现对构建通用映像(二进制文件和目标文件)的支持。这也意味着驱动程序应该能够直接调用特定于语言的编译器(例如 cc1),这意味着它必须有足够的信息才能正确地将命令行参数转发给子进程。
设计概述¶
下图显示了驱动程序架构的重要组件以及它们之间的关系。橙色组件表示驱动程序构建的具体数据结构,绿色组件表示概念上不同的阶段,这些阶段操作这些数据结构,蓝色组件是重要的帮助类。
驱动程序阶段¶
驱动程序功能在概念上被划分为五个阶段
解析:选项解析
命令行参数字符串被分解为参数(
Arg
实例)。驱动程序希望理解所有可用的选项,尽管存在一些机制可以仅将某些类别的选项传递(例如-Wl,
)。每个参数对应于一个抽象的
Option
定义,该定义描述了选项是如何解析的以及一些额外的元数据。Arg 实例本身很轻量级,仅包含足够的信息供客户端确定它们对应于哪个选项以及它们的值(如果它们具有其他参数)。例如,一个类似于“ -Ifoo -I foo” 的命令行将解析为两个 Arg 实例(一个 JoinedArg 实例和一个 SeparateArg 实例),但两者都将引用同一个 Option。
选项是惰性创建的,以避免在加载驱动程序时填充所有 Option 类。大多数驱动程序代码只需要通过它们的唯一 ID 来处理选项(例如,
options::OPT_I
),Arg 实例本身通常不存储参数的值。在许多情况下,这只会导致创建不必要的字符串副本。相反,Arg 实例总是嵌入在一个 ArgList 结构中,该结构包含原始的 argument 字符串向量。每个 Arg 本身只需要包含一个指向此向量的索引,而不是直接存储其值。
Clang 驱动程序可以使用
-###
标记来转储此阶段的结果(该标记必须位于任何实际命令行参数之前)。例如$ clang -### -Xarch_i386 -fomit-frame-pointer -Wa,-fast -Ifoo -I foo t.c Option 0 - Name: "-Xarch_", Values: {"i386", "-fomit-frame-pointer"} Option 1 - Name: "-Wa,", Values: {"-fast"} Option 2 - Name: "-I", Values: {"foo"} Option 3 - Name: "-I", Values: {"foo"} Option 4 - Name: "<input>", Values: {"t.c"}
在此阶段完成后,命令行应该被分解为具有适当参数的定义明确的选项对象。后续阶段应该很少,甚至永远不需要进行任何字符串处理。
流水线:编译动作构建
解析完参数后,将构建用于所需编译序列的子进程作业树。这包括确定输入文件及其类型,对它们执行哪些操作(预处理、编译、汇编、链接等),并为每个任务构建一个 Action 实例列表。结果是多个顶级操作的列表,每个操作通常对应于单个输出(例如,对象文件或链接的可执行文件)。
大多数 Action 对应于实际任务,但是有两种特殊 Action。第一个是 InputAction,它只是为了将输入参数改造成可用于其他 Action 的输入。第二个是 BindArchAction,它在概念上改变了将用于所有输入 Action 的架构。
Clang 驱动程序可以使用
-ccc-print-phases
标记来转储此阶段的结果。例如$ clang -ccc-print-phases -x c t.c -x assembler t.s 0: input, "t.c", c 1: preprocessor, {0}, cpp-output 2: compiler, {1}, assembler 3: assembler, {2}, object 4: input, "t.s", assembler 5: assembler, {4}, object 6: linker, {3, 5}, image
在这里,驱动程序正在构建七个不同的操作,四个操作用于将“t.c” 输入编译成目标文件,两个操作用于汇编“t.s” 输入,一个操作用于将它们链接在一起。
这里显示了一个截然不同的编译流水线;在此示例中,有两个顶级操作用于将输入文件编译成两个单独的目标文件,其中每个目标文件都是使用
lipo
构建的,用于合并针对两个单独的架构构建的结果。$ clang -ccc-print-phases -c -arch i386 -arch x86_64 t0.c t1.c 0: input, "t0.c", c 1: preprocessor, {0}, cpp-output 2: compiler, {1}, assembler 3: assembler, {2}, object 4: bind-arch, "i386", {3}, object 5: bind-arch, "x86_64", {3}, object 6: lipo, {4, 5}, object 7: input, "t1.c", c 8: preprocessor, {7}, cpp-output 9: compiler, {8}, assembler 10: assembler, {9}, object 11: bind-arch, "i386", {10}, object 12: bind-arch, "x86_64", {10}, object 13: lipo, {11, 12}, object
在此阶段完成后,编译过程被划分为一系列简单的操作,这些操作需要执行以生成中间或最终输出(在某些情况下,例如
-fsyntax-only
,没有“真正的”最终输出)。阶段是众所周知的编译步骤,例如“预处理”、“编译”、“汇编”、“链接”等。绑定:工具和文件名选择
此阶段(与转换阶段结合)将 Action 树转换为要运行的实际子进程列表。在概念上,驱动程序执行自上而下的匹配,将 Action(s) 分配给工具。工具链负责选择执行特定操作的工具;选择完工具后,驱动程序会与该工具交互,查看它是否可以匹配其他操作(例如,通过具有集成预处理器)。
一旦为所有操作选择了工具,驱动程序就会确定这些工具应该如何连接(例如,使用进程内模块、管道、临时文件或用户提供的文件名)。如果需要输出文件,驱动程序还会计算适当的文件名(后缀和文件位置取决于输入类型和选项,例如
-save-temps
)。驱动程序与工具链交互以执行工具绑定。每个工具链都包含有关特定架构、平台和操作系统所需的所有工具的信息。单个驱动程序调用可能在一次编译期间查询多个工具链,以便与用于不同架构的工具交互。
此阶段的结果不是直接计算的,但驱动程序可以通过
-ccc-print-bindings
选项打印结果。例如$ clang -ccc-print-bindings -arch i386 -arch ppc t0.c # "i386-apple-darwin9" - "clang", inputs: ["t0.c"], output: "/tmp/cc-Sn4RKF.s" # "i386-apple-darwin9" - "darwin::Assemble", inputs: ["/tmp/cc-Sn4RKF.s"], output: "/tmp/cc-gvSnbS.o" # "i386-apple-darwin9" - "darwin::Link", inputs: ["/tmp/cc-gvSnbS.o"], output: "/tmp/cc-jgHQxi.out" # "ppc-apple-darwin9" - "gcc::Compile", inputs: ["t0.c"], output: "/tmp/cc-Q0bTox.s" # "ppc-apple-darwin9" - "gcc::Assemble", inputs: ["/tmp/cc-Q0bTox.s"], output: "/tmp/cc-WCdicw.o" # "ppc-apple-darwin9" - "gcc::Link", inputs: ["/tmp/cc-WCdicw.o"], output: "/tmp/cc-HHBEBh.out" # "i386-apple-darwin9" - "darwin::Lipo", inputs: ["/tmp/cc-jgHQxi.out", "/tmp/cc-HHBEBh.out"], output: "a.out"
这显示了已绑定到此编译序列的工具链、工具、输入和输出。这里,clang 用于在 i386 架构上编译 t0.c,并且正在使用 darwin 特定版本的工具来汇编和链接结果,但正在使用通用 gcc 版本的工具来处理 PowerPC。
转换:特定于工具的参数转换
一旦选择了工具来执行特定 Action,该工具必须构造在编译期间执行的具体命令。主要工作是将 gcc 风格的命令行选项转换为子进程期望的任何选项。
一些工具,如汇编程序,只与少量参数交互,并且只确定要调用的可执行文件路径,并传递它们的输入和输出参数。其他工具,如编译器或链接器,除了可能会转换大量参数。
ArgList 类提供了一些简单的帮助方法来协助转换参数;例如,仅传递与某些选项对应的最后一个参数,或传递选项的所有参数。
此阶段的结果是执行的命令列表(可执行文件路径和参数字符串)。
执行
最后,执行编译流水线。这大部分很简单,尽管与
-pipe
、-pass-exit-codes
和-time
等选项有一些交互。
其他说明¶
编译对象¶
驱动程序为每组命令行参数构建一个 Compilation 对象。驱动程序本身旨在在构建 Compilation 期间保持不变;例如,IDE 应该能够构建一个单一的长期存在的驱动程序实例,用于整个构建过程。
Compilation 对象保存特定于每个编译序列的信息。例如,已使用的临时文件列表(在编译完成后必须删除)和结果文件列表(如果编译失败,则应删除)。
统一解析和管道¶
解析和管道都在没有引用 Compilation 实例的情况下发生。这是设计的;驱动程序预计这两个阶段都是平台无关的,除了少数非常明确的例外情况,例如平台是否使用驱动程序驱动程序。
工具链参数转换¶
为了与 gcc 非常匹配,clang 驱动程序目前允许工具链对参数列表执行自己的转换(转换为新的 ArgList 数据结构)。尽管这允许 clang 驱动程序轻松地匹配 gcc,但也使驱动程序操作难以理解(因为工具不再看到用户提供的某些参数,而是看到新的参数)。
例如,在 Darwin 上,-gfull
会被转换为两个独立的参数,-g
和 -fno-eliminate-unused-debug-symbols
。尝试编写 Tool 逻辑来处理 -gfull
将不起作用,因为 Tool 参数转换是在参数被转换之后进行的。
一个长期目标是删除这种工具链特定转换,而是强迫每个工具更改其自身的逻辑,以便对未转换的原始参数执行正确操作。
未使用参数警告¶
驱动程序通过解析所有参数来运行,但给工具提供选择要传递的参数的机会。这种基础设施的一个缺点是,如果用户拼写错误某些选项,或者对使用哪些选项感到困惑,用户真正关心的某些命令行参数可能会被忽略。这个问题在使用 clang 作为编译器时尤为重要,因为 clang 编译器不支持 gcc 的所有选项,我们希望确保用户知道哪些选项正在使用。
为了支持这一点,驱动程序为每个参数维护一个位,指示它在编译过程中是否已被使用(无论是否)。通常不需要手动设置此位,因为关键的 ArgList 访问器会自动设置它。
当编译成功(没有错误)时,驱动程序会检查该位,并针对任何从未访问过的参数发出“未使用参数”警告。这是保守的(参数可能没有被用来做用户想要的事情),但仍然捕获了最明显的情况。
与 GCC 驱动程序概念的关系¶
对于熟悉 gcc 驱动程序的人来说,本节简要概述了 gcc 驱动程序中的内容如何映射到 clang 驱动程序。
驱动程序驱动程序
驱动程序驱动程序已完全集成到 clang 驱动程序中。驱动程序只需构建额外的操作,以便在Pipeline 阶段绑定体系结构。工具链特定参数转换负责处理
-Xarch_
。唯一需要注意的是,这种方法要求
-Xarch_
不用于更改编译本身(例如,不能提供-S
作为-Xarch_
参数)。驱动程序尝试拒绝此类调用,总体而言,没有充分的理由在实践中滥用-Xarch_
来实现此目的。好处是 clang 驱动程序效率更高,并且为了支持通用构建而无需做太多额外工作。它还提供了更好的错误报告和 UI 一致性。
规范
clang 驱动程序没有直接对应于“规范”。嵌入在规范中的大多数功能都包含在 Tool 特定参数转换例程中。控制编译管道的规范部分通常是Pipeline 阶段的一部分。
工具链
gcc 驱动程序对工具链没有直接了解。每个 gcc 二进制文件大致对应于嵌入单个 ToolChain 中的信息。
clang 驱动程序旨在是可移植的,并支持复杂的编译环境。所有平台和工具链特定代码应受到抽象或明确定义的接口的保护(例如,平台是否支持用作驱动程序驱动程序)。