本文主要探討一下編譯器主要做些什么,以及如何有效的利用編譯器。
簡單的說,編譯器有兩個(gè)職責(zé):把 Objective-C 代碼轉(zhuǎn)化成低級代碼,以及對代碼做分析,確保代碼中沒有任何明顯的錯(cuò)誤。
現(xiàn)在,Xcode 的默認(rèn)編譯器是 clang。本文中我們提到的編譯器都表示 clang。clang 的功能是首先對 Objective-C 代碼做分析檢查,然后將其轉(zhuǎn)換為低級的類匯編代碼:LLVM Intermediate Representation(LLVM 中間表達(dá)碼)。接著 LLVM 會執(zhí)行相關(guān)指令將 LLVM IR 編譯成目標(biāo)平臺上的本地字節(jié)碼,這個(gè)過程的完成方式可以是即時(shí)編譯 (Just-in-time),或在編譯的時(shí)候完成。
LLVM 指令的一個(gè)好處就是可以在支持 LLVM 的任意平臺上生成和運(yùn)行 LLVM 指令。例如,你寫的一個(gè) iOS app, 它可以自動(dòng)的運(yùn)行在兩個(gè)完全不同的架構(gòu)(Inter 和 ARM)上,LLVM 會根據(jù)不同的平臺將 IR 碼轉(zhuǎn)換為對應(yīng)的本地字節(jié)碼。
LLVM 的優(yōu)點(diǎn)主要得益于它的三層式架構(gòu) -- 第一層支持多種語言作為輸入(例如 C, ObjectiveC, C++ 和 Haskell),第二層是一個(gè)共享式的優(yōu)化器(對 LLVM IR 做優(yōu)化處理),第三層是許多不同的目標(biāo)平臺(例如 Intel, ARM 和 PowerPC)。在這三層式的架構(gòu)中,如果你想要添加一門語言到 LLVM 中,那么可以把重要精力集中到第一層上,如果想要增加另外一個(gè)目標(biāo)平臺,那么你沒必要過多的考慮輸入語言。在書 The Architecture of Open Source Applications 中 LLVM 的創(chuàng)建者 (Chris Lattner) 寫了一章很棒的內(nèi)容:關(guān)于 LLVM 架構(gòu)。
在編譯一個(gè)源文件時(shí),編譯器的處理過程分為幾個(gè)階段。要想查看編譯 hello.m 源文件需要幾個(gè)不同的階段,我們可以讓通過 clang 命令觀察:
% clang -ccc-print-phases hello.m
0: input, "hello.m", objective-c
1: preprocessor, {0}, objective-c-cpp-output
2: compiler, {1}, assembler
3: assembler, {2}, object
4: linker, {3}, image
5: bind-arch, "x86_64", {4}, image
本文我們將重點(diǎn)關(guān)注第一階段和第二階段。在文章 Mach-O Executables 中,Daniel 會對第三階段和第四階段進(jìn)行闡述。
每當(dāng)編源譯文件的時(shí)候,編譯器首先做的是一些預(yù)處理工作。比如預(yù)處理器會處理源文件中的宏定義,將代碼中的宏用其對應(yīng)定義的具體內(nèi)容進(jìn)行替換。
例如,如果在源文件中出現(xiàn)下述代碼:
#import <Foundation/Foundation.h>
預(yù)處理器對這行代碼的處理是用 Foundation.h 文件中的內(nèi)容去替換這行代碼,如果 Foundation.h 中也使用了類似的宏引入,則會按照同樣的處理方式用各個(gè)宏對應(yīng)的真正代碼進(jìn)行逐級替代。
這也就是為什么人們主張頭文件最好盡量少的去引入其他的類或庫,因?yàn)橐氲臇|西越多,編譯器需要做的處理就越多。例如,在頭文件中用:
@class MyClass;
代替:
#import "MyClass.h"
這么寫是告訴編譯器 MyClass 是一個(gè)類,并且在 .m 實(shí)現(xiàn)文件中可以通過 import MyClass.h
的方式來使用它。
假設(shè)我們寫了一個(gè)簡單的 C 程序 hello.c
:
#include <stdio.h>
int main() {
printf("hello world\n");
return 0;
}
然后給上面的代碼執(zhí)行以下預(yù)處理命令,看看是什么效果:
clang -E hello.c | less
接下來看看處理后的代碼,一共是 401 行。如果將如下一行代碼添加到上面代碼的頂部::
#import <Foundation/Foundation.h>
再執(zhí)行一下上面的預(yù)處理命令,處理后的文件代碼行數(shù)暴增至 89,839 行。這個(gè)數(shù)字比某些操作系統(tǒng)的總代碼行數(shù)還要多。
幸好,目前的情況已經(jīng)改善許多了:引入了模塊 - modules功能,這使預(yù)處理變得更加的高級。
我們來看看另外一種情形定義或者使用自定義宏,比如定義了如下宏:
#define MY_CONSTANT 4
那么,凡是在此行宏定義作用域內(nèi),輸入了 MY_CONSTANT
,在預(yù)處理過程中 MY_CONSTANT
都會被替換成 4
。我們定義的宏也是可以攜帶參數(shù)的, 比如:
#define MY_MACRO(x) x
鑒于本文的內(nèi)容所限,就不對強(qiáng)大的預(yù)處理做更多、更全面的展開討論了。但是還是要強(qiáng)調(diào)一點(diǎn),建議大家不要在需要預(yù)處理的代碼中加入內(nèi)聯(lián)代碼邏輯。
例如,下面這段代碼,這樣用沒什么問題:
#define MAX(a,b) a > b ? a : b
int main() {
printf("largest: %d\n", MAX(10,100));
return 0;
}
但是如果換成這么寫:
#define MAX(a,b) a > b ? a : b
int main() {
int i = 200;
printf("largest: %d\n", MAX(i++,100));
printf("i: %d\n", i);
return 0;
}
用 clang max.c
編譯一下,結(jié)果是:
largest: 201
i: 202
用 clang -E max.c
進(jìn)行宏展開的預(yù)處理結(jié)果是如下所示:
int main() {
int i = 200;
printf("largest: %d\n", i++ > 100 ? i++ : 100);
printf("i: %d\n", i);
return 0;
}
本例是典型的宏使用不當(dāng),而且通常這類問題非常隱蔽且難以 debug 。針對本例這類情況,最好使用 static inline
:
#include <stdio.h>
static const int MyConstant = 200;
static inline int max(int l, int r) {
return l > r ? l : r;
}
int main() {
int i = MyConstant;
printf("largest: %d\n", max(i++,100));
printf("i: %d\n", i);
return 0;
}
這樣改過之后,就可以輸出正常的結(jié)果 (i:201
)。因?yàn)檫@里定義的代碼是內(nèi)聯(lián)的 (inlined),所以它的效率和宏變量差不多,但是可靠性比宏定義要好許多。再者,還可以設(shè)置斷點(diǎn)、類型檢查以及避免異常行為。
基本上,宏的最佳使用場景是日志輸出,可以使用 __FILE__
和 __LINE__
和 assert 宏。
預(yù)處理完成以后,每一個(gè) .m
源文件里都有一堆的聲明和定義。這些代碼文本都會從 string 轉(zhuǎn)化成特殊的標(biāo)記流。
例如,下面是一段簡單的 Objective-C hello word 程序:
int main() {
NSLog(@"hello, %@", @"world");
return 0;
}
利用 clang 命令 clang -Xclang -dump-tokens hello.m
來將上面代碼的標(biāo)記流導(dǎo)出:
int 'int' [StartOfLine] Loc=<hello.m:4:1>
identifier 'main' [LeadingSpace] Loc=<hello.m:4:5>
l_paren '(' Loc=<hello.m:4:9>
r_paren ')' Loc=<hello.m:4:10>
l_brace '{' [LeadingSpace] Loc=<hello.m:4:12>
identifier 'NSLog' [StartOfLine] [LeadingSpace] Loc=<hello.m:5:3>
l_paren '(' Loc=<hello.m:5:8>
at '@' Loc=<hello.m:5:9>
string_literal '"hello, %@"' Loc=<hello.m:5:10>
comma ',' Loc=<hello.m:5:21>
at '@' [LeadingSpace] Loc=<hello.m:5:23>
string_literal '"world"' Loc=<hello.m:5:24>
r_paren ')' Loc=<hello.m:5:31>
semi ';' Loc=<hello.m:5:32>
return 'return' [StartOfLine] [LeadingSpace] Loc=<hello.m:6:3>
numeric_constant '0' [LeadingSpace] Loc=<hello.m:6:10>
semi ';' Loc=<hello.m:6:11>
r_brace '}' [StartOfLine] Loc=<hello.m:7:1>
eof '' Loc=<hello.m:7:2>
仔細(xì)觀察可以發(fā)現(xiàn),每一個(gè)標(biāo)記都包含了對應(yīng)的源碼內(nèi)容和其在源碼中的位置。注意這里的位置是宏展開之前的位置,這樣一來,如果編譯過程中遇到什么問題,clang 能夠在源碼中指出出錯(cuò)的具體位置。
接下來要說的東西比較有意思:之前生成的標(biāo)記流將會被解析成一棵抽象語法樹 (abstract syntax tree -- AST)。由于 Objective-C 是一門復(fù)雜的語言,因此解析的過程不簡單。解析過后,源程序變成了一棵抽象語法樹:一棵代表源程序的樹。假設(shè)我們有一個(gè)程序 hello.m
:
#import <Foundation/Foundation.h>
@interface World
- (void)hello;
@end
@implementation World
- (void)hello {
NSLog(@"hello, world");
}
@end
int main() {
World* world = [World new];
[world hello];
}
當(dāng)我們執(zhí)行 clang 命令 clang -Xclang -ast-dump -fsyntax-only hello.m
之后,命令行中輸出的結(jié)果如下所示::
@interface World- (void) hello;
@end
@implementation World
- (void) hello (CompoundStmt 0x10372ded0 <hello.m:8:15, line:10:1>
(CallExpr 0x10372dea0 <line:9:3, col:24> 'void'
(ImplicitCastExpr 0x10372de88 <col:3> 'void (*)(NSString *, ...)' <FunctionToPointerDecay>
(DeclRefExpr 0x10372ddd8 <col:3> 'void (NSString *, ...)' Function 0x1023510d0 'NSLog' 'void (NSString *, ...)'))
(ObjCStringLiteral 0x10372de38 <col:9, col:10> 'NSString *'
(StringLiteral 0x10372de00 <col:10> 'char [13]' lvalue "hello, world"))))
@end
int main() (CompoundStmt 0x10372e118 <hello.m:13:12, line:16:1>
(DeclStmt 0x10372e090 <line:14:4, col:30>
0x10372dfe0 "World *world =
(ImplicitCastExpr 0x10372e078 <col:19, col:29> 'World *' <BitCast>
(ObjCMessageExpr 0x10372e048 <col:19, col:29> 'id':'id' selector=new class='World'))")
(ObjCMessageExpr 0x10372e0e8 <line:15:4, col:16> 'void' selector=hello
(ImplicitCastExpr 0x10372e0d0 <col:5> 'World *' <LValueToRValue>
(DeclRefExpr 0x10372e0a8 <col:5> 'World *' lvalue Var 0x10372dfe0 'world' 'World *'))))
在抽象語法樹中的每個(gè)節(jié)點(diǎn)都標(biāo)注了其對應(yīng)源碼中的位置,同樣的,如果產(chǎn)生了什么問題,clang 可以定位到問題所在處的源碼位置。
一旦編譯器把源碼生成了抽象語法樹,編譯器可以對這棵樹做分析處理,以找出代碼中的錯(cuò)誤,比如類型檢查:即檢查程序中是否有類型錯(cuò)誤。例如:如果代碼中給某個(gè)對象發(fā)送了一個(gè)消息,編譯器會檢查這個(gè)對象是否實(shí)現(xiàn)了這個(gè)消息(函數(shù)、方法)。此外,clang 對整個(gè)程序還做了其它更高級的一些分析,以確保程序沒有錯(cuò)誤。
每當(dāng)開發(fā)人員編寫代碼的時(shí)候,clang 都會幫忙檢查錯(cuò)誤。其中最常見的就是檢查程序是否發(fā)送正確的消息給正確的對象,是否在正確的值上調(diào)用了正確的函數(shù)。如果你給一個(gè)單純的 NSObject*
對象發(fā)送了一個(gè) hello
消息,那么 clang 就會報(bào)錯(cuò)。同樣,如果你創(chuàng)建了 NSObject
的一個(gè)子類 Test
, 如下所示:
@interface Test : NSObject
@end
然后試圖給這個(gè)子類中某個(gè)屬性設(shè)置一個(gè)與其自身類型不相符的對象,編譯器會給出一個(gè)可能使用不正確的警告。
一般會把類型分為兩類:動(dòng)態(tài)的和靜態(tài)的。動(dòng)態(tài)的在運(yùn)行時(shí)做檢查,靜態(tài)的在編譯時(shí)做檢查。以往,編寫代碼時(shí)可以向任意對象發(fā)送任何消息,在運(yùn)行時(shí),才會檢查對象是否能夠響應(yīng)這些消息。由于只是在運(yùn)行時(shí)做此類檢查,所以叫做動(dòng)態(tài)類型。
至于靜態(tài)類型,是在編譯時(shí)做檢查。當(dāng)在代碼中使用 ARC 時(shí),編譯器在編譯期間,會做許多的類型檢查:因?yàn)榫幾g器需要知道哪個(gè)對象該如何使用。例如,如果 myObject 沒有 hello 方法,那么就不能寫如下這行代碼了:
[myObject hello]
clang 在靜態(tài)分析階段,除了類型檢查外,還會做許多其它一些分析。如果你把 clang 的代碼倉庫 clone 到本地,然后進(jìn)入目錄 lib/StaticAnalyzer/Checkers
,你會看到所有靜態(tài)檢查內(nèi)容。比如 ObjCUnusedIVarsChecker.cpp
是用來檢查是否有定義了,但是從未使用過的變量。而 ObjCSelfInitChecker.cpp
則是檢查在 你的初始化方法中中調(diào)用 self
之前,是否已經(jīng)調(diào)用 [self initWith...]
或 [super init]
了。編譯器還進(jìn)行了一些其它的檢查,例如在 lib/Sema/SemaExprObjC.cpp
的 2,534 行,有這樣一句:
Diag(SelLoc, diag::warn_arc_perform_selector_leaks);
這個(gè)會生成嚴(yán)重錯(cuò)誤的警告 “performSelector may cause a leak because its selector is unknown” 。
clang 完成代碼的標(biāo)記,解析和分析后,接著就會生成 LLVM 代碼。下面繼續(xù)看看hello.c
:
#include <stdio.h>
int main() {
printf("hello world\n");
return 0;
}
要把這段代碼編譯成 LLVM 字節(jié)碼(絕大多數(shù)情況下是二進(jìn)制碼格式),我們可以執(zhí)行下面的命令:
clang -O3 -emit-LLVM hello.c -c -o hello.bc
接著用另一個(gè)命令來查看剛剛生成的二進(jìn)制文件:
llvm-dis < hello.bc | less
輸出如下:
; ModuleID = '<stdin>'
target datalayout = "e-p:64:64:64-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:64:64-f32:32:32-f64:64:64-v64:64:64-v128:128:128-a0:0:64-s0:64:64-f80:128:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.8.0"
@str = private unnamed_addr constant [12 x i8] c"hello world\00"
; Function Attrs: nounwind ssp uwtable
define i32 @main() #0 {
%puts = tail call i32 @puts(i8* getelementptr inbounds ([12 x i8]* @str, i64 0, i64 0))
ret i32 0
}
; Function Attrs: nounwind
declare i32 @puts(i8* nocapture) #1
attributes #0 = { nounwind ssp uwtable }
attributes #1 = { nounwind }
在上面的代碼中,可以看到 main
函數(shù)只有兩行代碼:一行輸出string,另一行返回 0
。
再換一個(gè)程序,拿 five.m
為例,對其做相同的編譯,然后執(zhí)行 LLVM-dis < five.bc | less
:
#include <stdio.h>
#import <Foundation/Foundation.h>
int main() {
NSLog(@"%@", [@5 description]);
return 0;
}
拋開其他的不說,單看 main
函數(shù):
define i32 @main() #0 {
%1 = load %struct._class_t** @"\01L_OBJC_CLASSLIST_REFERENCES_$_", align 8
%2 = load i8** @"\01L_OBJC_SELECTOR_REFERENCES_", align 8, !invariant.load !4
%3 = bitcast %struct._class_t* %1 to i8*
%4 = tail call %0* bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to %0* (i8*, i8*, i32)*)(i8* %3, i8* %2, i32 5)
%5 = load i8** @"\01L_OBJC_SELECTOR_REFERENCES_2", align 8, !invariant.load !4
%6 = bitcast %0* %4 to i8*
%7 = tail call %1* bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to %1* (i8*, i8*)*)(i8* %6, i8* %5)
tail call void (%1*, ...)* @NSLog(%1* bitcast (%struct.NSConstantString* @_unnamed_cfstring_ to %1*), %1* %7)
ret i32 0
}
上面代碼中最重要的是第 4 行,它創(chuàng)建了一個(gè) NSNumber
對象。第 7 行,給這個(gè) number 對象發(fā)送了一個(gè) description
消息。第 8 行,將 description
消息返回的內(nèi)容打印出來。
要想了解 LLVM 的優(yōu)化內(nèi)容,以及 clang 能做哪些優(yōu)化,我們先看一個(gè)略微復(fù)雜的 C 程序:這個(gè)函數(shù)主要是遞歸計(jì)算 階乘
:
#include <stdio.h>
int factorial(int x) {
if (x > 1) return x * factorial(x-1);
else return 1;
}
int main() {
printf("factorial 10: %d\n", factorial(10));
}
先看看不做優(yōu)化的編譯情況,執(zhí)行下面命令:
clang -O0 -emit-llvm factorial.c -c -o factorial.bc && llvm-dis < factorial.bc
重點(diǎn)看一下針對 階乘
部分生成的代碼:
define i32 @factorial(i32 %x) #0 {
%1 = alloca i32, align 4
%2 = alloca i32, align 4
store i32 %x, i32* %2, align 4
%3 = load i32* %2, align 4
%4 = icmp sgt i32 %3, 1
br i1 %4, label %5, label %11
; <label>:5 ; preds = %0
%6 = load i32* %2, align 4
%7 = load i32* %2, align 4
%8 = sub nsw i32 %7, 1
%9 = call i32 @factorial(i32 %8)
%10 = mul nsw i32 %6, %9
store i32 %10, i32* %1
br label %12
; <label>:11 ; preds = %0
store i32 1, i32* %1
br label %12
; <label>:12 ; preds = %11, %5
%13 = load i32* %1
ret i32 %13
}
看一下 %9
標(biāo)注的那一行,這行代碼正是遞歸調(diào)用階乘函數(shù)本身,實(shí)際上這樣調(diào)用是非常低效的,因?yàn)槊看芜f歸調(diào)用都要重新壓棧。接下來可以看一下優(yōu)化后的效果,可以通過這樣的方式開啟優(yōu)化 -- 將 -03
標(biāo)志傳給 clang:
clang -O3 -emit-llvm factorial.c -c -o factorial.bc && llvm-dis < factorial.bc
現(xiàn)在 階乘
計(jì)算相關(guān)代碼編譯后生成的代碼如下:
define i32 @factorial(i32 %x) #0 {
%1 = icmp sgt i32 %x, 1
br i1 %1, label %tailrecurse, label %tailrecurse._crit_edge
tailrecurse: ; preds = %tailrecurse, %0
%x.tr2 = phi i32 [ %2, %tailrecurse ], [ %x, %0 ]
%accumulator.tr1 = phi i32 [ %3, %tailrecurse ], [ 1, %0 ]
%2 = add nsw i32 %x.tr2, -1
%3 = mul nsw i32 %x.tr2, %accumulator.tr1
%4 = icmp sgt i32 %2, 1
br i1 %4, label %tailrecurse, label %tailrecurse._crit_edge
tailrecurse._crit_edge: ; preds = %tailrecurse, %0
%accumulator.tr.lcssa = phi i32 [ 1, %0 ], [ %3, %tailrecurse ]
ret i32 %accumulator.tr.lcssa
}
即便我們的函數(shù)并沒有按照尾遞歸的方式編寫,clang 仍然能對其做優(yōu)化處理,讓該函數(shù)編譯的結(jié)果中只包含一個(gè)循環(huán)。當(dāng)然 clang 能對代碼進(jìn)行的優(yōu)化還有很多方面??梢钥匆韵逻@個(gè)比較不錯(cuò)的 gcc 的優(yōu)化例子ridiculousfish.com。
延伸閱讀
剛剛我們探討了編譯的全過程,從標(biāo)記到解析,從抽象語法樹到分析檢查,再到匯編。讀者不禁要問,為什么要關(guān)注這些?
之所以 clang 很酷:是因?yàn)樗且粋€(gè)開源的項(xiàng)目、并且它是一個(gè)非常好的工程:幾乎可以說全身是寶。使用者可以創(chuàng)建自己的 clang 版本,針對自己的需求對其進(jìn)行改造。比如說,可以改變 clang 生成代碼的方式,增加更強(qiáng)的類型檢查,或者按照自己的定義進(jìn)行代碼的檢查分析等等。要想達(dá)成以上的目標(biāo),有很多種方法,其中最簡單的就是使用一個(gè)名為 libclang 的C類庫。libclang 提供的 API 非常簡單,可以對 C 和 clang 做橋接,并可以用它對所有的源碼做分析處理。不過,根據(jù)我的經(jīng)驗(yàn),如果使用者的需求更高,那么 libclang 就不怎么行了。針對這種情況,推薦使用 Clangkit,它是基于 clang 提供的功能,用 Objective-C 進(jìn)行封裝的一個(gè)庫。
最后,clang 還提供了一個(gè)直接使用 LibTooling 的 C++ 類庫。這里要做的事兒比較多,而且涉及到 C++,但是它能夠發(fā)揮 clang 的強(qiáng)大功能。用它你可以對源碼做任意類型的分析,甚至重寫程序。如果你想要給 clang 添加一些自定義的分析、創(chuàng)建自己的重構(gòu)器 (refactorer)、或者需要基于現(xiàn)有代碼做出大量修改,甚至想要基于工程生成相關(guān)圖形或者文檔,那么 LibTooling 是很好的選擇。
開發(fā)者可以按照 Tutorial for building tools using LibTooling 中的說明去構(gòu)造 LLVM ,clang 以及 clan g的附加工具。需要注意的是,編譯代碼是需要花費(fèi)一些時(shí)間的,即時(shí)機(jī)器已經(jīng)很快了,但是在編譯期間,我還是可以吃頓飯的。
接下來,進(jìn)入到 LLVM 目錄,然后執(zhí)行命令cd ~/llvm/tools/clang/tools/
。在這個(gè)目錄中,可以創(chuàng)建自己獨(dú)立的 clang 工具。例如,我們創(chuàng)建一個(gè)小工具,用來檢查某個(gè)庫是否正確使用。首先將 樣例工程 克隆到本地,然后輸入 make
。這樣就會生成一個(gè)名為 example
的二進(jìn)制文件。
我們的使用場景是:假如有一個(gè) Observer
類, 代碼如下所示:
@interface Observer
+ (instancetype)observerWithTarget:(id)target action:(SEL)selector;
@end
接下來,我們想要檢查一下每當(dāng)這個(gè)類被調(diào)用的時(shí)候,在 target
對象中是否都有對應(yīng)的 action
方法存在??梢詫憘€(gè) C++ 函數(shù)來做這件事(注意,這是我第一次寫 C++ 程序,可能不那么嚴(yán)謹(jǐn)):
virtual bool VisitObjCMessageExpr(ObjCMessageExpr *E) {
if (E->getReceiverKind() == ObjCMessageExpr::Class) {
QualType ReceiverType = E->getClassReceiver();
Selector Sel = E->getSelector();
string TypeName = ReceiverType.getAsString();
string SelName = Sel.getAsString();
if (TypeName == "Observer" && SelName == "observerWithTarget:action:") {
Expr *Receiver = E->getArg(0)->IgnoreParenCasts();
ObjCSelectorExpr* SelExpr = cast<ObjCSelectorExpr>(E->getArg(1)->IgnoreParenCasts());
Selector Sel = SelExpr->getSelector();
if (const ObjCObjectPointerType *OT = Receiver->getType()->getAs<ObjCObjectPointerType>()) {
ObjCInterfaceDecl *decl = OT->getInterfaceDecl();
if (! decl->lookupInstanceMethod(Sel)) {
errs() << "Warning: class " << TypeName << " does not implement selector " << Sel.getAsString() << "\n";
SourceLocation Loc = E->getExprLoc();
PresumedLoc PLoc = astContext->getSourceManager().getPresumedLoc(Loc);
errs() << "in " << PLoc.getFilename() << " <" << PLoc.getLine() << ":" << PLoc.getColumn() << ">\n";
}
}
}
}
return true;
}
上面的這個(gè)方法首先查找消息表達(dá)式, 以 Observer
作為接收者, observerWithTarget:action:
作為 selector,然后檢查 target 中是否存在相應(yīng)的方法。雖然這個(gè)例子有點(diǎn)兒刻意,但如果你想要利用 AST 對自己的代碼庫做某些檢查,按照上面的例子來就可以了。
clang還有許多其他的用途。比如,可以寫編譯器插件(例如,類似上面的檢查器例子)并且動(dòng)態(tài)的加載到編譯器中。雖然我沒有親自實(shí)驗(yàn)過,但是我覺得在 Xcode 中應(yīng)該是可行的。再比如,也可以通過編寫 clang 插件來自定義代碼樣式(具體可以參見 編譯過程)。
另外,如果想對現(xiàn)有的代碼做大規(guī)模的重構(gòu), 而 Xcode 或 AppCode 本身集成的重構(gòu)工具無法達(dá)你的要求,你完全可以用 clang 自己寫個(gè)重構(gòu)工具。聽起來有點(diǎn)兒可怕,讀讀下面的文檔和教程,你會發(fā)現(xiàn)其實(shí)沒那么難。
最后,如果是真的有這種需求,你完全可以引導(dǎo) Xcdoe 使用你自己編譯的 clang 。再一次,如果你去嘗試,其實(shí)這些事兒真的沒想象中那么復(fù)雜,反而會發(fā)現(xiàn)許多個(gè)中樂趣。