欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

打造基于Clang LibTooling的iOS自动打点系统CLAS(三)

程序员文章站 2022-11-05 11:35:20
打造基于clang libtooling的ios自动打点clas(三)。 1. 变换 第一章我们提到过,clas的本质是对源码做一次非常简单的变换(有些文章里称作变形),即source-source...

打造基于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. 插入代码

既然大致了解了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参数的裁剪和重排

上面的一节,我们基本完成了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. 最后

到了这里,我们已经构建了一个简单的基于clang libtooling的编译前端工具,可以解析ast,并在指定位置插入自定义代码。本文并没有覆盖正式项目所具有的实用性功能,例如针对性的代码插入、灵活的功能配置(例如通过配置文件)等。我们会在接下来的文章里介绍针对性的代码插入以及如果将clas集成到xcode编译链中,敬请期待…