1. 源碼變換
第一章我們提到過,CLAS的本質(zhì)是對(duì)源碼做一次非常簡(jiǎn)單的變換(有些文章里稱作變形),即Source-Source-Transformation腻扇,將打點(diǎn)代碼精確地插入到目標(biāo)函數(shù)的首部情竹,保存到臨時(shí)文件,代替原始文件傳遞到Clang進(jìn)行編譯莉兰。這個(gè)變換過程對(duì)于Clang的編譯流程沒有侵入,保證了與不同版本Clang一定的兼容性礁竞,即使Clang進(jìn)行小版本升級(jí)CLAS仍然可以正常工作無需重新編譯(例如Xcode從8.2.1升級(jí)為8.3.3)糖荒。圍繞著源碼變換可以做出許多非常有創(chuàng)意的工具,大家有興趣可以深入研究這個(gè)話題模捂,我們?cè)谶@里就不展開了捶朵。
Clang提供給我們了一個(gè)非常好用的類clang::Rewriter
用于源碼變換蜘矢。如果你熟悉Clang可能會(huì)知道有一個(gè)大名鼎鼎的編譯選項(xiàng)-rewrite-objc
,這個(gè)選項(xiàng)可以幫助你將OC代碼重寫成C++代碼综看,很多對(duì)于OC內(nèi)部運(yùn)行機(jī)制的窺視和分析都是基于這個(gè)選項(xiàng)得來的品腹,而它就是基于我們第二章所講的ASTConsumer以及本章所講的Rewriter構(gòu)建出來的。
細(xì)看Rewriter的接口會(huì)發(fā)現(xiàn)红碑,它滿足了CLAS對(duì)源碼內(nèi)容增刪改查的全部需求舞吭。例如你可以通過Rewriter向源碼內(nèi)指定位置插入刪除任意長(zhǎng)度的代碼,然后將修改后的內(nèi)容保存到一個(gè)臨時(shí)文件中析珊。Rewriter的接口在Clang的模塊里可以算得上是超級(jí)簡(jiǎn)單易用的了羡鸥,方法的含義根據(jù)方法名就一目了然,而且不需要復(fù)雜的上下文參數(shù)傳遞忠寻。編譯器這種動(dòng)輒幾十人持續(xù)很多年維護(hù)同一個(gè)工程的代碼惧浴,想要很容易地看懂里面任何一個(gè)功能都不是那么簡(jiǎn)單的事情,Rewriter算得上是Clang里面的異類奕剃。
2. 插入代碼
既然大致了解了Rewriter衷旅,接下來我們就要開始真正的插入代碼了。假設(shè)我們需要在每個(gè)方法的開始加入這么一句話祭饭,讓每次方法執(zhí)行時(shí)打印出被調(diào)用的方法名:
{ NSLog(@"進(jìn)入方法:%__FUNCNAME__%"); }
第一個(gè)問題馬上就出現(xiàn)了芜茵,插入的代碼是預(yù)先定義好的,如何能夠根據(jù)不同的方法名插入不同的代碼呢倡蝙?這個(gè)問題很好解決九串,我們需要定義一些CLAS變量,以%包圍寺鸥,例如上面的%__FUNCNAME__%猪钮。在遍歷到每一個(gè)方法準(zhǔn)備插入代碼的之前,將%__FUNCNAME__%替換為當(dāng)前的方法名即可胆建。至于定義哪些變量取決于工具的需要烤低。正式的CLAS系統(tǒng)我們只需要有限的幾個(gè)變量即可(例如__FUNCNAME__, __CLASSNAME__笆载,__CATEGORYNAME__等)扑馁,因?yàn)樾枰迦氲拇a按照第二章的要求都應(yīng)該是盡可能自包含的靜態(tài)代碼,不需要在插入代碼的時(shí)候進(jìn)行過多的人為干預(yù)凉驻。
在這里我們還要單獨(dú)說明一下腻要,插入的代碼不要包含換行符和制表符等,因?yàn)檫@些符號(hào)涝登,尤其是換行符會(huì)破壞源碼的位置信息(SourceLocation)雄家,導(dǎo)致debug的時(shí)候指向錯(cuò)誤的行數(shù)。無論再長(zhǎng)的代碼胀滚,都不要換行趟济,當(dāng)然避免插入過長(zhǎng)的代碼才是最好的乱投。
我們把需要插入的代碼保存到一個(gè)單獨(dú)的文本文件里,然后讓CLAS在啟動(dòng)的時(shí)候讀取這個(gè)文件的內(nèi)容到內(nèi)存中顷编,并在遍歷到每一個(gè)OC方法的時(shí)候插入這段代碼戚炫。至于如何將代碼內(nèi)容從文件中讀入內(nèi)存的細(xì)節(jié)不在本文討論范圍內(nèi),熟悉C++的你可以直接閱讀CLAS源代碼媳纬。我們打開ClangAutoStats.cpp嘹悼,首先需要引入Rewriter的頭文件:
#include "clang/Rewrite/Core/Rewriter.h"
然后我們需要定義一個(gè)Rewriter的靜態(tài)變量:
static clang::Rewriter TheRewriter;
我們假設(shè)需要插入的代碼片段已經(jīng)從文件中讀入內(nèi)存,并存入靜態(tài)變量:
static std::string CodeSnippet;
接下來我們?cè)贑langAutoStatsVisitor的handleObjcMethDecl方法里加入如下代碼:
CompoundStmt *cmpdStmt = MD->getCompoundBody();
SourceLocation loc = cmpdStmt->getLocStart(). getLocWithOffset(1);
if (loc.isMacroID()) {
loc = TheRewriter.getSourceMgr().getImmediateExpansion Range(loc).first;
}
ObjCMethodDecl有一個(gè)方法getCompoundBody层宫,會(huì)返回當(dāng)前方法的復(fù)合語句節(jié)點(diǎn)(Compound Statement)。在AST里其监,每一條語句(Statement)都是一個(gè)Stmt節(jié)點(diǎn)萌腿,而復(fù)合語句從Stmt繼承而來,是包含有0至n個(gè)Stmt的容器型Stmt抖苦,復(fù)合語句也可以嵌套包含復(fù)合語句毁菱。If、For锌历、Switch贮庞、While、do究西、以及OC方法都可以包含一個(gè)復(fù)合語句窗慎。我們插入代碼的位置在方法的復(fù)合語句大括號(hào)后面,例如:
- (void)func {/*在這里插入代碼卤材,不會(huì)破壞debug信息*/
}
CompoundStmt的getLocStat方法可以返回復(fù)合語句的起始位置遮斥,這相當(dāng)于是左大括號(hào)的位置,我們?cè)谶@個(gè)位置的基礎(chǔ)上再向后偏移1個(gè)字節(jié)指向大括號(hào)后面的位置扇丛。在上面的例子术吗,這個(gè)位置會(huì)是回車‘\n’的位置(行級(jí)注釋都不會(huì)出現(xiàn)在AST里面)。找到這個(gè)位置后我們還需要做一個(gè)額外的檢查帆精,看看這個(gè)復(fù)合語句是不是從宏定義展開而來的较屿。如果是根據(jù)宏定義展開的復(fù)合語句,直接調(diào)用getLocStart方法會(huì)獲得定義這個(gè)復(fù)合語句的宏定義的聲明位置卓练,那么我們計(jì)算的插入代碼的位置就錯(cuò)了隘蝎。正確的做法是調(diào)用SourceMgr的getImmediateExpansionRange方法獲取這個(gè)復(fù)合語句的實(shí)際在源碼內(nèi)展開的位置。計(jì)算完畢后昆庇,我們要調(diào)用Rewriter的InsertTextBefore方法進(jìn)行代碼插入末贾。在插入CodeSnippet之前,我們還需要把%__FUNCNAME__%替換為當(dāng)前方法名(C++操作起字符串來真的是比OC費(fèi)勁太多了...):
static std::string varName("%__FUNCNAME__%");
std::string funcName = MD->getDeclName().getAsString();
std::string codes(CodeSnippet);
size_t pos = 0;
while ((pos = codes.find(varName, pos)) != std::string::npos) {
codes.replace(pos, varName.length(), funcName);
pos += funcName.length();
}
TheRewriter.InsertTextBefore(loc, codes);
我們目前修改了Rewriter的內(nèi)容整吆,但并沒有對(duì)源文件有任何影響拱撵,按照CLAS的設(shè)計(jì)要求辉川,我們還需要將修改過后的文件內(nèi)容保存至臨時(shí)文件。這個(gè)我們選擇在ClangAutoStatsAction里重寫EndSourceFileAction方法拴测,在這里面我們將Rewriter的內(nèi)容保存至與原文件同名的.clas后綴的臨時(shí)文件:
void EndSourceFileAction() override {
size_t pos = filePath.find_last_of(".");
if (pos != std::string::npos) {
ClasFilePath = filePath + ".clas";
}
std::ofstream clasFile(ClasFilePath);
assert(clasFile.is_open());
FileID fid = getCompilerInstance().getSourceManager(). getMainFileID();
RewriteBuffer &buffer = LogRewriter.getEditBuffer(fid);
RewriteBuffer::iterator I = buffer.begin();
RewriteBuffer::iterator E = buffer.end();
for (; I != E; I.MoveToNextPiece()) {
(clasFile << I.piece().str());
}
clasFile.flush();
clasFile.close();
}
3. Clang參數(shù)的裁剪和重排
上面的一節(jié)乓旗,我們基本完成了CLAS的框架結(jié)構(gòu),能夠在OC方法最前面自動(dòng)插入自定義代碼集索,當(dāng)然這種插入目前還是無差別的全量插入屿愚,肯定還需要根據(jù)需求進(jìn)行針對(duì)性的打磨,這種精細(xì)化的定制需求就不在本文討論范圍內(nèi)了务荆,你可以根據(jù)這個(gè)框架繼續(xù)改進(jìn)代碼妆距。
接下來我們需要考慮的是如何應(yīng)對(duì)Xcode傳入的Clang指令及參數(shù),以符合CLAS的需要函匕。在前一章我們討論過LibTooling的Fixed Compilation Database娱据,它與Clang的參數(shù)形式并不直接兼容。CLAS被定義為一個(gè)類似Clang Wrapper的工具盅惜,為了避免過多的對(duì)編譯工具鏈進(jìn)行入侵中剩,我們需要將Xcode傳入的Clang指令進(jìn)行精心地裁剪和重新排序,以便讓CLAS可以正常工作抒寂。
舉個(gè)很簡(jiǎn)單的例子结啼,比如我們有一個(gè)HelloWorld.m的文件需要處理:
#import <Foundation/Foundation.h>
@interface HelloWorld : NSObject
@end
@implementation HelloWorld
- (void)sayHi:(NSString *)msg {
NSLog(@"Hello %@", msg);
}
@end
如果在Xcode里編譯這個(gè)文件,查看Build Log會(huì)看到Xcode發(fā)出了如下指令及參數(shù)給Clang(略去了-W以及-I, -F屈芜,否則太長(zhǎng)了):
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang -x objective-c -arch x86_64 -std=gnu99 -fobjc-arc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator10.3.sdk -mios-simulator-version-min=8.0 -c /Users/test/HelloWorld/HelloWorld.m -o /Users/test/HelloWorld/HelloWorld.o
如果調(diào)用CLAS郊愧,則參數(shù)列表需要轉(zhuǎn)換為如下格式:
/usr/local/clas/bin/clas /Users/test/HelloWorld/HelloWorld.m -- -x objective-c -arch x86_64 -std=gnu99 -fobjc-arc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator10.3.sdk -mios-simulator-version-min=8.0 -F/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator10.3.sdk/System/Library/Frameworks -I/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1 -I/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/include -o /Users/test/HelloWorld/HelloWorld.o
我們可以看到,HelloWorld.m被移到了第二位沸伏,后面緊跟了"--"參數(shù)糕珊,表明后面跟隨的都是Clang所需的參數(shù)。這些參數(shù)多了一個(gè)-F和兩個(gè)-I毅糟,分別指向了ios的系統(tǒng)Frameworks目錄红选,以及include目錄。之所以我們需要添加這三個(gè)參數(shù)姆另,是因?yàn)樘O果的Clang會(huì)默認(rèn)加入對(duì)這些目錄喇肋,而我們從源碼編譯的LibTooling的工具卻不會(huì),如果不添加這些參數(shù)會(huì)導(dǎo)致LibTooling分析文件的時(shí)候因?yàn)檎也坏礁鞣N系統(tǒng)頭文件而失敗迹辐。這就是參數(shù)裁剪重排的意義蝶防。CLAS執(zhí)行完成后,還有一個(gè)非常重要的任務(wù)明吩,就是將原文件.m重命名后间学,將CLAS輸出的臨時(shí)文件重命名為原文件,拼接剩余參數(shù)并調(diào)用蘋果原生的Clang(/usr/bin/clang),clang執(zhí)行完成后低葫,無論成功與否详羡,將臨時(shí)文件刪除并將原文件.m復(fù)原,編譯流程至此結(jié)束嘿悬。
如果你熟悉C/C++实柠,這些代碼可以在CLAS里完成而保證最高的執(zhí)行效率,如果不熟悉上面提到的操作完全可以通過腳本來完成善涨,腳本攔截Xcode發(fā)出的編譯指令窒盐,處理參數(shù)后傳遞給CLAS,CLAS處理完成后钢拧,在腳本里繼續(xù)執(zhí)行蘋果的Clang蟹漓。這里我們就不對(duì)這些做詳細(xì)描述了,如果有興趣可以直接研究CLAS源碼源内。
4. 最后
到了這里牧牢,我們已經(jīng)構(gòu)建了一個(gè)簡(jiǎn)單的基于Clang LibTooling的編譯前端工具,可以解析AST姿锭,并在指定位置插入自定義代碼。本文并沒有覆蓋正式項(xiàng)目所具有的實(shí)用性功能伯铣,例如針對(duì)性的代碼插入呻此、靈活的功能配置(例如通過配置文件)等。我們會(huì)在接下來的文章里介紹針對(duì)性的代碼插入以及如果將CLAS集成到Xcode編譯鏈中腔寡,敬請(qǐng)期待...