初识LLVM&Clang-开发Xcode插件
- 2019 年 10 月 7 日
- 笔记
初识LLVM&Clang-开发Xcode插件
LLVM
Xcode现在使用的编译器就是LLVM
。LLVM
比以前使用的GCC
编译器速度快好几倍。并且LLVM
可以编译 Kotlin,Ruby,Python,Haskell,Java,D,PHP,Pure,Lua 和许多其他语言。
LLVM IR
通过LLVM
编译后的产物是LLVM IR
。LLVM IR
是一个区别于源码和机器码的一种中间代码。这里就是LLVM
的强大之处,不管编译什么哪种语言,输出的都是LLVM IR
。
这里就要说一句:LLVM编译器是区分前后端的,而传统的编译器(GCC)是不区分前后端的。这样导致的后果就是传统编译器如果要支持其他的一种语言或硬件平台的话要做大量工作。


LLVM如果要支持一种新的语言,那么只需要实现一个新的编译器前端即可,后端可以不变,因为前端的产物都是LLVM IR
编译器后端都能识别。如果要改变硬件平台的话,就只要实现一个新的编译器后端即可,通过把前端输出的LLVM IR
再次编译成对应硬件平台的代码。从这就可以看出前后端分离,以及LLVM IR
的作用了。
LLVM IR 的三种格式:
- 内存中的编译中间语言
- 硬盘上存储的可读中间格式(以 .ll 结尾)
- 硬盘上存储的二进制中间语言(以 .bc 结尾)
这三种中间格式完全是等价的。
Bitcode
这么说LLVM IR
可能还不熟悉,但是我们说道bitcode
时就熟悉多了。其实bitcode
就是LLVM IR
第三种格式(硬盘上存储的二进制中间语言)。我们在打包的时候可以选择是否bitcode
编译打包。如果选择了bitcode
打包方式,上传IPA包时同时也会上传bitcode
文件。并且之后Apple就不会使用你的IPA包了,会通过对bitcode
文件再次打包。这么做是因为Apple对上传的bitcode
可做一些优化工作,并且还可以对安装的目标设备进行二进制优化,减少安装包的大小,比如CPU架构为armv7
的就不需要arm64
的文件。去除不必要的架构可以加快打包速度。


Clang
前面说到了LLVM编译器分为前后端,Clang
就是编译器的前端。Clang
的主要功能是输出代码对应的抽象语法树( AST
),针对用户发生的编译错误准确地给出建议,并将代码编译成LLVM IR
。
Clang 的主要工作:
- 预处理: 比如把宏嵌入到对应的位置,头文件的导入,去除注释( clang -E main.m )
- 词法分析: 这里会把代码切成一个个 Token,比如大小括号,等于号还有字符串等
- 语法分析: 验证语法是否正确
- 生成
AST
: 将所有节点组成抽象语法树AST
- 静态分析:分析代码是否存在问题,给出错误信息和修复方案
- 生成
LLVM IR
: CodeGen 会负责将语法树自顶向下遍历逐步翻译成LLVM IR
以上是其中涉及的一些概念点,想深入了解的话还是要单独去找资料阅读。这里只是皮毛中的皮毛?。下面就看下如何实现一个Xcode的插件:
LLVM环境搭建
下载LLVM代码到本地
$ git clone https://git.llvm.org/git/llvm.git/
或者直接到GitHub上下载也可以。
下载clang
$ cd llvm/tools $ git clone https://git.llvm.org/git/clang.git/

配置和构建LLVM和Clang
CMake
首先我要先安装编译工具CMake
,这里有一片介绍文档可够了解。
$ brew install cmake
使用ninja编译
1、安装
$ brew install ninja
2、在llvm
同级目录下新建一个llvm_build
目录,最终会在llvm_build
目录下生成build.ninja
。
3、在llvm
同级目录下新建一个llvm_release
目录,最终编译文件会在llvm_release
文件夹路径下。
$ cd llvm_build $ cmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX=安装路径 //例如:cmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX=/Users/zhouqiang/clangPlugin/llvm_release

4、依次执行编译、安装指令。
$ ninja $ ninja install
创建插件
1、在/llvm/tools/clang/tools
目录下新建插件。

2、修改/llvm/tools/clang/tools
目录下的CMakeLists.txt
文件,新增add_clang_subdirectory(QTPlugin)
。

3、在QTPlugin
目录下新建一个名为QTPlugin.cpp
的文件
#include <iostream> #include "clang/AST/AST.h" #include "clang/AST/DeclObjC.h" #include "clang/AST/ASTConsumer.h" #include "clang/ASTMatchers/ASTMatchers.h" #include "clang/Frontend/CompilerInstance.h" #include "clang/ASTMatchers/ASTMatchFinder.h" #include "clang/Frontend/FrontendPluginRegistry.h" using namespace clang; using namespace std; using namespace llvm; using namespace clang::ast_matchers; namespace QTPlugin { class QTMatchHandler: public MatchFinder::MatchCallback { private: CompilerInstance &CI; bool isUserSourceCode(const string filename) { if (filename.empty()) return false; // 非Xcode中的源码都认为是用户源码 if (filename.find("/Applications/Xcode.app/") == 0) return false; return true; } bool isShouldUseCopy(const string typeStr) { if (typeStr.find("NSString") != string::npos || typeStr.find("NSArray") != string::npos || typeStr.find("NSDictionary") != string::npos/*...*/) { return true; } return false; } public: QTMatchHandler(CompilerInstance &CI) :CI(CI) {} void run(const MatchFinder::MatchResult &Result) { const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl"); if (propertyDecl && isUserSourceCode(CI.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str()) ) { ObjCPropertyDecl::PropertyAttributeKind attrKind = propertyDecl->getPropertyAttributes(); string typeStr = propertyDecl->getType().getAsString(); if (propertyDecl->getTypeSourceInfo() && isShouldUseCopy(typeStr) && !(attrKind & ObjCPropertyDecl::OBJC_PR_copy)) { cout<<"--------- "<<typeStr<<": 不是使用的 copy 修饰--------"<<endl; DiagnosticsEngine &diag = CI.getDiagnostics(); diag.Report(propertyDecl->getBeginLoc(), diag.getCustomDiagID(DiagnosticsEngine::Warning, "--------- %0 不是使用的 copy 修饰--------")) << typeStr; } } } }; class QTASTConsumer: public ASTConsumer { private: MatchFinder matcher; QTMatchHandler handler; public: QTASTConsumer(CompilerInstance &CI) :handler(CI) { matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &handler); } void HandleTranslationUnit(ASTContext &context) { matcher.matchAST(context); } }; class QTASTAction: public PluginASTAction { public: unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef iFile) { return unique_ptr<QTASTConsumer> (new QTASTConsumer(CI)); } bool ParseArgs(const CompilerInstance &ci, const std::vector<std::string> &args) { return true; } }; } static FrontendPluginRegistry::Add<QTPlugin::QTASTAction> X("QTPlugin", "The QTPlugin is my first clang-plugin.");
4、在QTPlugin
目录下新建一个名为CMakeLists.txt
的文件,内容为
add_llvm_library(xxPlugin MODULE xxPlugin.cpp PLUGIN_TOOL clang) if(LLVM_ENABLE_PLUGINS AND (WIN32 OR CYGWIN)) target_link_libraries(xxPlugin PRIVATE clangAST clangBasic clangFrontend LLVMSupport ) endif()
5、目录文件创建完成之后,利用CMake
重新生成一下Xcode项目。
$ cd llvm_xcode $ cmake -G Xcode ../llvm
6、插件源代码在 Xcode 项目中的Loadable modules目录下可以找到,这样就可以直接在 Xcode 里编写插件代码。
7、最后command+B
编译生成QTPlugin.dylib
文件,找到插件对应的QTPlugin.dylib
。

Xcode集成QTPlugin
1、创建一个新的Xcode项目
2、打开需要加载插件的Xcode项目,在Build Settings栏目中的OTHER_CFLAGS添加上如下内容:
-Xclang -load -Xclang (.dylib)动态库路径 -Xclang -add-plugin -Xclang 插件名字(namespace 的名字,名字不对则无法使用插件) 例如: -Xclang -load -Xclang /Users/zhouqiang/clangPlugin/llvm_xcode/Debug/lib/QTPlugin.dylib -Xclang -add-plugin -Xclang QTPlugin

3、编译报错:由于Clang
插件需要使用对应的版本去加载,如果版本不一致则会导致编译错误,会出现如下图所示:

在Build Settings
栏目中新增两项用户定义的设置

分别是CC
和CXX
。

CC
对应的是自己编译的clang
的绝对路径,CXX
对应的是自己编译的clang++
的绝对路径。

clang&clang++.png
4、编译报错如下

则可以在Build Settings
栏目中搜索index
,将Enable Index-Wihle-Building Functionality
的Default
改为NO
。
5、最后在新创建的Xcode项目中编译就会有如下警告了。说明你的插件成功导入并生效了。
