打造基于Clang LibTooling的iOS自动打点系统CLAS(三)
打造基于clang libtooling的ios自动打点clas(三)。
1. 变换
第一章我们提到过,clas的本质是对源码做一次非常简单的变换(有些文章里称作变形),即source-source-transformation,将打点代码精确地插入到目标函数的首部,保存到临时文件,代替原始文件传递到clang进行编译。这个变换过程对于clang的编译流程没有侵入,保证了与不同版本clang一定的兼容性,即使clang进行小版本升级clas仍然可以正常工作无需重新编译(例如xcode从8.2.1升级为8.3.3)。围绕着源码变换可以做出许多非常有创意的工具,大家有兴趣可以深入研究这个话题,我们在这里就不展开了。
clang提供给我们了一个非常好用的类clang::rewriter用于源码变换。如果你熟悉clang可能会知道有一个大名鼎鼎的编译选项
-rewrite-objc,这个选项可以帮助你将oc代码重写成c++代码,很多对于oc内部运行机制的窥视和分析都是基于这个选项得来的,而它就是基于我们第二章所讲的astconsumer以及本章所讲的rewriter构建出来的。
细看rewriter的接口会发现,它满足了clas对源码内容增删改查的全部需求。例如你可以通过rewriter向源码内指定位置插入删除任意长度的代码,然后将修改后的内容保存到一个临时文件中。rewriter的接口在clang的模块里可以算得上是超级简单易用的了,方法的含义根据方法名就一目了然,而且不需要复杂的上下文参数传递。编译器这种动辄几十人持续很多年维护同一个工程的代码,想要很容易地看懂里面任何一个功能都不是那么简单的事情,rewriter算得上是clang里面的异类。
2. 插入代码
2. 插入代码
既然大致了解了rewriter,接下来我们就要开始真正的插入代码了。假设我们需要在每个方法的开始加入这么一句话,让每次方法执行时打印出被调用的方法名:
#include "clang/rewrite/core/rewriter.h"
然后我们需要定义一个rewriter的静态变量:
static clang::rewriter therewriter;
我们假设需要插入的代码片段已经从文件中读入内存,并存入静态变量:
static std::string codesnippet;
接下来我们在clangautostatsvisitor的handleobjcmethdecl方法里加入如下代码:
compoundstmt *cmpdstmt = md->getcompoundbody(); sourcelocation loc = cmpdstmt->getlocstart(). getlocwithoffset(1); if (loc.ismacroid()) { loc = therewriter.getsourcemgr().getimmediateexpansion range(loc).first; }
objcmethoddecl有一个方法getcompoundbody,会返回当前方法的复合语句节点(compound statement)。在ast里,每一条语句(statement)都是一个stmt节点,而复合语句从stmt继承而来,是包含有0至n个stmt的容器型stmt,复合语句也可以嵌套包含复合语句。if、for、switch、while、do、以及oc方法都可以包含一个复合语句。我们插入代码的位置在方法的复合语句大括号后面,例如:
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的内容,但并没有对源文件有任何影响,按照clas的设计要求,我们还需要将修改过后的文件内容保存至临时文件。这个我们选择在clangautostatsaction里重写endsourcefileaction方法,在这里面我们将rewriter的内容保存至与原文件同名的.clas后缀的临时文件:
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参数的裁剪和重排
3. clang参数的裁剪和重排
上面的一节,我们基本完成了clas的框架结构,能够在oc方法最前面自动插入自定义代码,当然这种插入目前还是无差别的全量插入,肯定还需要根据需求进行针对性的打磨,这种精细化的定制需求就不在本文讨论范围内了,你可以根据这个框架继续改进代码。
接下来我们需要考虑的是如何应对xcode传入的clang指令及参数,以符合clas的需要。在前一章我们讨论过libtooling的fixed compilation database,它与clang的参数形式并不直接兼容。clas被定义为一个类似clang wrapper的工具,为了避免过多的对编译工具链进行入侵,我们需要将xcode传入的clang指令进行精心地裁剪和重新排序,以便让clas可以正常工作。
举个很简单的例子,比如我们有一个helloworld.m的文件需要处理:
#import @interface helloworld : nsobject @end @implementation helloworld - (void)sayhi:(nsstring *)msg { nslog(@"hello %@", msg); } @end
如果在xcode里编译这个文件,查看build log会看到xcode发出了如下指令及参数给clang(略去了-w以及-i, -f,否则太长了):
/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
如果调用clas,则参数列表需要转换为如下格式:
/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被移到了第二位,后面紧跟了”–”参数,表明后面跟随的都是clang所需的参数。这些参数多了一个-f和两个-i,分别指向了ios的系统frameworks目录,以及include目录。之所以我们需要添加这三个参数,是因为苹果的clang会默认加入对这些目录,而我们从源码编译的libtooling的工具却不会,如果不添加这些参数会导致libtooling分析文件的时候因为找不到各种系统头文件而失败。这就是参数裁剪重排的意义。clas执行完成后,还有一个非常重要的任务,就是将原文件.m重命名后,将clas输出的临时文件重命名为原文件,拼接剩余参数并调用苹果原生的clang(/usr/bin/clang),clang执行完成后,无论成功与否,将临时文件删除并将原文件.m复原,编译流程至此结束。
如果你熟悉c/c++,这些代码可以在clas里完成而保证最高的执行效率,如果不熟悉上面提到的操作完全可以通过脚本来完成,脚本拦截xcode发出的编译指令,处理参数后传递给clas,clas处理完成后,在脚本里继续执行苹果的clang。这里我们就不对这些做详细描述了,如果有兴趣可以直接研究clas源码。
4. 最后
4. 最后
到了这里,我们已经构建了一个简单的基于clang libtooling的编译前端工具,可以解析ast,并在指定位置插入自定义代码。本文并没有覆盖正式项目所具有的实用性功能,例如针对性的代码插入、灵活的功能配置(例如通过配置文件)等。我们会在接下来的文章里介绍针对性的代码插入以及如果将clas集成到xcode编译链中,敬请期待…
上一篇: 为什么要做内容营销?内容营销有什么好处
下一篇: C语言实现压缩二例