打造基于Clang LibTooling的iOS自動(dòng)打點(diǎn)系統(tǒng)CLAS(三)

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)期待...

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末焚鲜,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子放前,更是在濱河造成了極大的恐慌忿磅,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,729評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件凭语,死亡現(xiàn)場(chǎng)離奇詭異葱她,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)似扔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,226評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門吨些,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人炒辉,你說我怎么就攤上這事豪墅。” “怎么了黔寇?”我有些...
    開封第一講書人閱讀 169,461評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵偶器,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng)屏轰,這世上最難降的妖魔是什么颊郎? 我笑而不...
    開封第一講書人閱讀 60,135評(píng)論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮亭枷,結(jié)果婚禮上袭艺,老公的妹妹穿的比我還像新娘。我一直安慰自己叨粘,他們只是感情好猾编,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,130評(píng)論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著升敲,像睡著了一般答倡。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上驴党,一...
    開封第一講書人閱讀 52,736評(píng)論 1 312
  • 那天瘪撇,我揣著相機(jī)與錄音,去河邊找鬼港庄。 笑死倔既,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的鹏氧。 我是一名探鬼主播渤涌,決...
    沈念sama閱讀 41,179評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼把还!你這毒婦竟也來了实蓬?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,124評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤吊履,失蹤者是張志新(化名)和其女友劉穎安皱,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體艇炎,經(jīng)...
    沈念sama閱讀 46,657評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡酌伊,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,723評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了缀踪。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片腺晾。...
    茶點(diǎn)故事閱讀 40,872評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖辜贵,靈堂內(nèi)的尸體忽然破棺而出悯蝉,到底是詐尸還是另有隱情,我是刑警寧澤托慨,帶...
    沈念sama閱讀 36,533評(píng)論 5 351
  • 正文 年R本政府宣布鼻由,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏蕉世。R本人自食惡果不足惜蔼紧,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,213評(píng)論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望狠轻。 院中可真熱鬧奸例,春花似錦、人聲如沸向楼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,700評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽湖蜕。三九已至逻卖,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間昭抒,已是汗流浹背评也。 一陣腳步聲響...
    開封第一講書人閱讀 33,819評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留灭返,地道東北人盗迟。 一個(gè)月前我還...
    沈念sama閱讀 49,304評(píng)論 3 379
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像熙含,于是被迫代替她去往敵國(guó)和親诈乒。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,876評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容