objc系列译文(6.2):编译器

2296 查看

编译器做些什么?

本文主要探讨一下编译器主要做些什么,以及如何有效的利用编译器。

简单的说,编译器有两个职责:把 Objective-C 代码转化成低级代码,以及对代码做分析,确保代码中没有任何明显的错误。

现在,Xcode 的默认编译器是 clang。本文中我们提到的编译器都表示 clang。clang 的功能是首先对 Objective-C 代码做分析检查,然后将其转换为低级的类汇编代码:LLVM Intermediate Representation(LLVM 中间表达码)。接着 LLVM 会执行相关指令将 LLVM IR 编译成目标平台上的本地字节码,这个过程的完成方式可以是即时编译 (Just-in-time),或在编译的时候完成。

LLVM 指令的一个好处就是可以在支持 LLVM 的任意平台上生成和运行 LLVM 指令。例如,你写的一个 iOS app, 它可以自动的运行在两个完全不同的架构(Inter 和 ARM)上,LLVM 会根据不同的平台将 IR 码转换为对应的本地字节码。

LLVM 的优点主要得益于它的三层式架构 — 第一层支持多种语言作为输入(例如 C, ObjectiveC, C++ 和 Haskell),第二层是一个共享式的优化器(对 LLVM IR 做优化处理),第三层是许多不同的目标平台(例如 Intel, ARM 和 PowerPC)。在这三层式的架构中,如果你想要添加一门语言到 LLVM 中,那么可以把重要精力集中到第一层上,如果想要增加另外一个目标平台,那么你没必要过多的考虑输入语言。在书 The Architecture of Open Source Applications 中 LLVM 的创建者 (Chris Lattner) 写了一章很棒的内容:关于LLVM 架构

在编译一个源文件时,编译器的处理过程分为几个阶段。要想查看编译 hello.m 源文件需要几个不同的阶段,我们可以让通过 clang 命令观察:

本文我们将重点关注第一阶段和第二阶段。在文章 Mach-O Executables 中,Daniel 会对第三阶段和第四阶段进行阐述。

预处理

每当编源译文件的时候,编译器首先做的是一些预处理工作。比如预处理器会处理源文件中的宏定义,将代码中的宏用其对应定义的具体内容进行替换。

例如,如果在源文件中出现下述代码:

预处理器对这行代码的处理是用 Foundation.h 文件中的内容去替换这行代码,如果 Foundation.h 中也使用了类似的宏引入,则会按照同样的处理方式用各个宏对应的真正代码进行逐级替代。

这也就是为什么人们主张头文件最好尽量少的去引入其他的类或库,因为引入的东西越多,编译器需要做的处理就越多。例如,在头文件中用:

代替:

这么写是告诉编译器 MyClass 是一个类,并且在 .m 实现文件中可以通过 import MyClass.h 的方式来使用它。

假设我们写了一个简单的 C 程序 hello.c:

然后给上面的代码执行以下预处理命令,看看是什么效果:

接下来看看处理后的代码,一共是 401 行。如果将如下一行代码添加到上面代码的顶部::

再执行一下上面的预处理命令,处理后的文件代码行数暴增至 89,839 行。这个数字比某些操作系统的总代码行数还要多。

幸好,目前的情况已经改善许多了:引入了模块 – modules功能,这使预处理变得更加的高级。

自定义宏

我们来看看另外一种情形定义或者使用自定义宏,比如定义了如下宏:

那么,凡是在此行宏定义作用域内,输入了 MY_CONSTANT,在预处理过程中 MY_CONSTANT 都会被替换成 4。我们定义的宏也是可以携带参数的, 比如:

鉴于本文的内容所限,就不对强大的预处理做更多、更全面的展开讨论了。但是还是要强调一点,建议大家不要在需要预处理的代码中加入内联代码逻辑。

例如,下面这段代码,这样用没什么问题:

但是如果换成这么写:

用clang的max.c编译一下,结果是:

用 clang -E max.c 进行宏展开的预处理结果是如下所示:

本例是典型的宏使用不当,而且通常这类问题非常隐蔽且难以 debug 。针对本例这类情况,最好使用 static inline:

这样改过之后,就可以输出正常的结果 (i:201)。因为这里定义的代码是内联的 (inlined),所以它的效率和宏变量差不多,但是可靠性比宏定义要好许多。再者,还可以设置断点、类型检查以及避免异常行为。

基本上,宏的最佳使用场景是日志输出,可以使用 __FILE__ 和 __LINE__ 和 assert 宏。

词法解析标记

预处理完成以后,每一个 .m 源文件里都有一堆的声明和定义。这些代码文本都会从 string 转化成特殊的标记流。

例如,下面是一段简单的 Objective-C hello word 程序:

利用 clang 命令 clang -Xclang -dump-tokens hello.m 来将上面代码的标记流导出: