这一篇我们继续来学习 Clang,以及如何将它用在工作中。在此之前你可能需要了解一点 iOS 的编译知识,可以看这一篇。
一、 Clang、LibClang、LibTooling
我想你应该是听过 Clang 的,Clang 为分析代码语法、语义信息的工具提供了很好的基础设施。以此衍生出来了 3 个东西: LibClang、Clang Plugin 和 LibTooling。
LibClang
LibClang 提供了一个稳定的高级 C 接口,Xcode 使用的就是 LibClang。LibClang 可以访问 Clang 的上层高级抽象的能力,比如获取所有 Token、遍历语法树、代码补全等。由于 API 很稳定,Clang 版本更新对其影响不大。但是,LibClang 并不能完全访问到 Clang AST 信息。 使用 LibClang 可以直接使用它的 C API。官方也提供了 Python binding 脚本供你调用。还有开源的 node-js/ruby binding。你要是不熟悉其他语言,还有个第三方开源的 Objective-C 写的ClangKit 库可供使用。
Clang Plugins
Clang Plugins 可以让你在 AST 上做些操作,这些操作能够集成到编译中,成为编译的一部分。插件是在运行时由编译器加载的动态库,方便集成到构建系统中。 使用 Clang Plugins 一般都是希望能够完全控制 Clang AST,同时能够集成在编译流程中,可以影响编译的过程,进行中断或者提示。相关实操文章可看这篇:《Clang 代码规范检查插件》;
LibTooling
LibTooling 是一个 C++ 接口,通过 LibTooling 能够编写独立运行的语法检查和代码重构工具。LibTooling 的优势如下:
- 所写的工具不依赖于构建系统,可以作为一个命令单独使用,比如 clang-check、clang-fixit、clang-format;
- 可以完全控制 Clang AST;
- 能够和 Clang Plugins 共用一份代码;
与 Clang Plugins 相比,LibTooling 无法影响编译过程;与 LibClang 相比,LibTooling 的接口没有那么稳定,也无法开箱即用,当 AST 的 API 升级后需要更新接口的调用。但是,LibTooling 基于能够完全控制 Clang AST 和可独立运行的特点,可以做的事情就非常多了。比如代码语言转换、坚持代码规范、分析甚至重构代码等。
在 LibTooling 的基础之上有个开发人员工具合集 Clang tools,Clang tools 作为 Clang 项目的一部分,已经提供了一些工具,主要包括:
- 语法检查工具 clang-check;
- 自动修复编译错误工具 clang-fixit;
- 自动代码格式工具 clang-format;
- 新语言和新功能的迁移工具;
- 重构工具;
二、自动埋点
接下来我们使用 LibTooling 来做一个类似自动埋点功能,需求大概是这样的:在不影响原来代码的情况下,可以实现当调用某个方法的时候,会自动 NSLog,输出方法的名称。 自动埋点其实就是在不手动处理的情况下,在合适的地方加入一段代码。这次我们使用 LibTooling,利用编译阶段对代码的语义分析,在合适的地方加入埋点代码。 因为本身 LibTooling 可以完全控制 Clang AST,所以这个在理论上是没问题的。但是 LibTooling 本身无法跟 Xcode 的编译(LibClang)过程交互,所以目前我们的思路是这样:
- 利用 LibTooling 实现代码插入,同时保存一份原代码;
- 进行 Xcode 编译;
- Xcode 编译完成之后,将原代码替换回来;
LibTooling 开发
接下来我们要建一个 LibTooling 工程,第一步需要下载 LLVM 项目。
下载编译 LLVM
在本地新建一个文件夹 LLVM,打开命令行 cd 到该目录下,输入命令:
git clone https://github.com/llvm/llvm-project.git
cd llvm-project
mkdir build
cd build
cmake -G Xcode -DLLVM_ENABLE_PROJECTS=clang ../llvm
其中 cmake -G Xcode -DLLVM_ENABLE_PROJECTS=clang ../llvm 会生成 LLVM 的 Xcode 编译工程,此时可以看到本地目录如下:
目录中 clang 是类 C 语言编译器的代码目录;llvm 目录的代码包含两部分,一部分是对源码进行平台无关优化的优化器代码,另一部分是生成平台相关汇编代码的生成器代码;lldb 目录里是调试器的代码;lld 里是链接器代码。
编译工程
接下来编译 LLVM 工程,双击打开 LLVM.Xcodeproj ,选择 Autocreat Schemes,添加 schemes All_BUILD:
然后点击 Running,等待编译完成,预计要大半个小时:
新建 LibTooling 项目
等 LLVM 编译完成之后,我们来新建一个 LibTooling 工程,名字就叫做 AddCodePlugin。
打开clang-tools-CMakeLists.txt文件,在末尾加入一句话 add_clang_subdirectory(AddCodePlugin)
同时在这个目录下新建一个 AddCodePlugin 的文件夹,文件夹里面新建两个文件:AddCodePlugin.cpp和CMakeLists.txt。
在AddCodePlugin-CMakeLists.txt文件中写入:
set(LLVM_LINK_COMPONENTS support)
add_clang_executable(AddCodePlugin
AddCodePlugin.cpp
)
target_link_libraries(AddCodePlugin
PRIVATE
clangAST
clangBasic
clangDriver
clangFormat
clangLex
clangParse
clangSema
clangFrontend
clangTooling
clangToolingCore
clangRewrite
clangRewriteFrontend
)
AddCodePlugin.cpp 的具体代码可以看这里,我贴出它的核心代码:
main 函数,是 LibTooling 的启动入口:
int main(int argc, const char **argv) {
CommonOptionsParser op(argc, argv, AddOptionCategory);
ClangTool Tool(op.getCompilations(), op.getSourcePathList());
int result = Tool.run(newFrontendActionFactory<AddCodePlugin::ClangAutoStatsAction>().get());
return result;
}
在 handleObjcMethDecl 中处理替换字符,找到方法的大括号后一位,在这里添加一段代码,这里我加入的是一段 NSLog 打印,可以将该方法名称打印出来。
bool handleObjcMethDecl(ObjCMethodDecl *MD) {
if (!MD->hasBody()) return true;
cout << "handleObjcMethDecl" << endl;
//找到方法的括号后面 同时避免宏定义没展开
CompoundStmt *CS = MD->getCompoundBody();
SourceLocation loc = CS->getBeginLoc().getLocWithOffset(1);
if (loc.isMacroID()) {
loc = rewriter.getSourceMgr().getImmediateExpansionRange(loc).getBegin();
}
static std::string varName("%__FUNCNAME__%");
std::string funcName = MD->getDeclName().getAsString();
std::string codes(CodeSnippet);
size_t pos = 0;
//替换特殊字符%__FUNCNAME__%为方法名
while ((pos = codes.find(varName, pos)) != std::string::npos) {
codes.replace(pos, varName.length(), funcName);
pos += funcName.length();
}
//写入
rewriter.InsertTextBefore(loc, codes);
return true;
}
在文件处理结束的时候,将修改的内容写入到文件中,顺便打印出修改的内容:
void EndSourceFileAction() {
cout << "EndSourceFileAction" << endl;
SourceManager &SM = fileRewriter.getSourceMgr();
std::string filename = SM.getFileEntryForID(SM.getMainFileID())->getName().str();
std::error_code error_code;
llvm::raw_fd_ostream outFile(filename, error_code, llvm::sys::fs::F_None);
// 将Rewriter结果输出到文件中
fileRewriter.getEditBuffer(SM.getMainFileID()).write(outFile);
// 将Rewriter结果输出在控制台上
fileRewriter.getEditBuffer(SM.getMainFileID()).write(llvm::outs());
}
上面都做完之后,需要回到 build 文件夹,重新运行一下命令:
cd build
cmake -G Xcode -DLLVM_ENABLE_PROJECTS=clang ../llvm
然后回到 LLVM 的工程,编译运行一下 scheme:
编译完成会得到一个 AddCodePlugin 可执行文件,找到这个文件的路径,记录下来
运行 LibTooling 工具
接下来我们来运行一下这个工具,先写一个测试文件 test.m:
#import <UIKit/UIKit.h>
@interface HelloViewController : UIViewController
- (void)sayHi;
@end
@implementation HelloViewController
- (void)sayHi {
NSLog(@"Hello world!");
}
- (void)sayHi123 {
NSLog(@"Hello world!");
}
@end
在命令行中,进入 test.m 所在文件夹执行命令:
/Users/wuyanji/Desktop/LLVM/llvm-project/build/Debug/bin/AddCodePlugin test.m -- -x objective-c -arch x86_64 -std=gnu99 -fobjc-arc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk -mios-simulator-version-min=8.0 -F/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.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
后面的一些参数是为了编译时防止找不到头文件等错误,也可以简单直接输入:
/Users/wuyanji/Desktop/LLVM/llvm-project/build/Debug/bin/AddCodePlugin test.m --
可以看到运行结果如下:
wuyanjideMac-mini-2:YourClang wuyanji$ /Users/wuyanji/Desktop/LLVM/llvm-project/build/Debug/bin/AddCodePlugin test.m -- -x objective-c -arch x86_64 -std=gnu99 -fobjc-arc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk -mios-simulator-version-min=8.0 -F/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.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
handleObjcMethDecl
handleObjcMethDecl
EndSourceFileAction
#import <UIKit/UIKit.h>
@interface HelloViewController : UIViewController
- (void)sayHi;
@end
@implementation HelloViewController
- (void)sayHi {NSLog(@"sayHi");
NSLog(@"Hello world!");
}
- (void)sayHi123 {NSLog(@"sayHi123");
NSLog(@"Hello world!");
}
@end
test.m 文件里面的方法已经加入我们需要的代码块了。现在我们的工具已经可以了,接下来就是如何跟 Xcode 编译一起运行了。
嵌入 Xcode 编译
接下来我们把这个工具加入到 Xcode 的编译中,可以看一下 Targets-Build Phases,
这里就是 Xcode 编译执行的任务顺序,其中 Compile Sources 就是编译相关的文件。那其实我们可以这样,在编译文件之前先运行工具添加代码,在编译文件之后再吧文件改回来,这样就做到了既不修改文件,又能将我们相关的代码编译到 APP 中。修改后 Build Phases 如下:
上面的脚本会复制保存一份原代码,等编译完成之后下面的脚本会将原代码复制回原文件中。这次我们尝试对一个文件进行,试试看效果如何。这两个命令分别如下:
cp /Users/wuyanji/Documents/Build/Build/ViewController.m /Users/wuyanji/Documents/Build/Build/ViewController.m.temp
/Users/wuyanji/Desktop/LLVM/llvm-project/build/Debug/bin/AddCodePlugin /Users/wuyanji/Documents/Build/Build/ViewController.m -- -x objective-c -arch $ARCHS -std=gnu99 -fobjc-arc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk -mios-simulator-version-min=9.0 -F/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.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
mv /Users/wuyanji/Documents/Build/Build/ViewController.m.temp /Users/wuyanji/Documents/Build/Build/ViewController.m
最后看一下运行结果,果然如我们所愿,触发方法的时候会同时把方法名打印出来:
三、 总结
这次我们开发了一个基于 LibTooling 的自动添加打印方法名的功能,要做到自动埋点只需在此基础上继续开发即可。关键在于 LibTooling 可以完全的控制编译过程,这给我们留出了很多的操作空间,据此可以做的其他功能还有很多,有想法可以自己尝试。
四、相关文章
打造基于Clang LibTooling的iOS自动打点系统CLAS(一)
打造基于Clang LibTooling的iOS自动打点系统CLAS(二)
打造基于Clang LibTooling的iOS自动打点系统CLAS(三)
TUTORIAL FOR BUILDING TOOLS USING LIBTOOLING AND LIBASTMATCHERS