iOS底层原理之自定义Clang插件

嗨,这里是逻辑iOS技术号:一个让知识变得感性,让学习变得轻松!活跃的技术小站,希望给你的生活与技术带来意思不一样!关注公众号,回复“           面试题       ”,即可领取更多大厂面试题型哦~ 小逻辑相信我们的生活不止眼前的苟且,还有我们向往的诗和大厂高薪工作~


前言

前文主要介绍了下LLVM和Clang相关的概念、设计思想和编译流程,本篇文章将使用LLVM和Clang实现一个简单的插件。废话不多说,让我们开始今天的内容吧。


图片


 LLVM下载

编写Clang插件之前,需要先下载和编译LLVM。


由于国内的网络限制,我们需要借助镜像下载LLVM的源码。

mirror.tuna.tsinghua.edu.cn/help/llvm-p…


这里提供两种下载方式,一种是下载整个LLVM(包括各个子仓库,比如clang等等),一种是只下载LLVM,然后根据自己需要再去下载子仓库。


1.1: 下载完整LLVM,包含子仓库(2.78G)

    git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm-project.git

    (滑动显示更多)


    1.2: 只下载LLVM,不包含子仓库(1.52G)

      git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm-project/llvm.git

      (滑动显示更多)


      1.2.1: 根据需要下载相应的子仓库


      自定义插件需要的子仓库


      • 在LLVM的tools目录下下载Clang。

        cd llvm/toolsgit clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm-project/clang.git

        (滑动显示更多)


        如果想研究lldb的话,需要在LLVM的tools目录下下载lldb。自定义插件不需要。

          cd llvm/toolsgit clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm-project/lldb.git

          (滑动显示更多)


          在LLVM的projects目录下下载compiler-rt,libcxx,libcxxabi。

            cd ../projectsgit clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm-project/compiler-rt.gitgit clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm-project/libcxx.gitgit clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm-project/libcxxabi.git

            (滑动显示更多)


            在Clang的tools下安装clang-tools-extra工具。

              cd ../tools/clang/toolsgit clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm-project/clang-tools-extra.git

              (滑动显示更多)


              图片


              LLVM编译

              新版macOS默认的shell是zsh,所以,首先进入终端执行:

                echo 'export OSX_COMMANDLINE_SDKROOT="/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk"' >> ~/.zshrc

                (滑动显示更多)


                然后再执行:

                  source ~/.zshrc


                  由于最新的LLVM只支持cmake来编译了,所以我们还需要安装brew和cmake。相关安装方法请移步brew和cmake安装。

                  一些常见的构建系统生成器(generator)有:


                  • Ninja:大多数LLVM开发人员都使用Ninja。


                  • Unix Makefiles:用于生成与make兼容的并行makefile。


                  • Visual Studio:用于生成Visual Studio项目和解决方案。


                  • Xcode:用于生成Xcode项目。

                   

                  作为iOS开发人员,当然首选Xcode来进行编译了。


                  2.1: 使用Xcode构建LLVM项目


                  首先使用Xcode为generator,通过cmake将LLVM编译成Xcode项目。

                   

                  2.1.1: 完整LLVM编译方法

                    cd llvm-project                // 进入完整llvm文件夹mkdir build_xcode              // 新建文件夹build_xcodecd build_xcode                 // 进入build_xcodecmake -G  [options] ../llvm // 编译成Xcode项目,具体命令看下面

                    (滑动显示更多)


                    这里generator我们选择Xcode,-DLLVM_ENABLE_PROJECTS就是需要编译的子项目,这里我们需要加上clang,compiler-rt,libcxx,libcxxabi,clang-tools-extra。

                      cmake -G Xcode -DLLVM_ENABLE_PROJECTS='libcxx;libcxxabi;clang;clang-tools-extra;compiler-rt' -DLLDB_USE_SYSTEM_DEBUGSERVER=ON -DLLDB_TEST_COMPILER=clang++ -DCMAKE_OSX_SYSROOT=$OSX_COMMANDLINE_SDKROOT ../llvm

                      (滑动显示更多)


                      2.1.2: 不完整LLVM编译方法

                        mkdir build_xcode       // 在llvm所在目录新建文件夹build_xcodecd build_xcode          // 进入build_xcodecmake -G Xcode ../llvm  // 编译成Xcode项目

                        (滑动显示更多)


                        不完整LLVM由于我们已经根据自己的情况下载了子仓库,所以不用添加[options]直接编译就可以了。


                        开始编译(完整包为例):


                        图片


                        大概几分钟后后,检测映射完成。


                        图片


                        此时build_xcode目录下大概有67M内容(指定不同[options],大小会有所不同):


                        图片


                        2.2: 使用ninja构建LLVM项目(不推荐)

                         

                        • 使用ninja进行编译,需要先安装ninja,相关安装方法请移步brew和cmake安装。


                        • 在llvm源码根目录下新建build_ninja目录,最终会在build_ninja目录下生成build.ninja。


                        • 在llvm源码根目录下新建llvm-release目录,最终编译文件会在llvm-release文件夹路径下。


                          
                          cd llvm_build
                          
                          
                          
                          // 注意DCMAKE_INSTALL_PREFIX后面不能有空格 cmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX= 安装路径(本机为/ Users/xxx/xxx/LLVM/llvm_release)

                          (滑动显示更多)


                          • 依次执行编译、安装指令。

                            
                            ninja
                            
                            
                            
                            ninja install


                            2.3: 使用Xcode编译Clang

                            • 进入build_xcode目录打开LLVM.xcodeproj:


                            图片


                            进入Xcode界面:


                            图片


                            注意:不要选择Automatically Create Schemes,选择Manually Manage Schemes。

                            否则会引入一些不必要的scheme,拖累Xcode速度。

                            原则:使用哪个scheme,就引入哪个。


                            • 点击左下角加号,在Target中选择我们需要的添加:


                            图片


                            • 开始运行clang和clangTooling,第一次运行时需要进行编译,往后再运行,即可直接运行:


                            图片


                            注意:每次运行时要通过Run Without Building运行。这意味着当你编译一次之后,代码没有改变的情况下,不需要重新编译,直接运行现有可执行文件即可。


                            • 选择Build & Run:


                            图片


                            • 真正进入编译模式:


                            图片


                            起飞,感受机器的轰鸣吧!!!趁这个时间可以洗个澡或吃个饭。 图片


                            图片


                            图片


                            Clang插件

                            开始创建插件之前先对要实现的功能做一个简单的介绍:

                             

                            • 自定义插件想要实现的功能是当检测到NSString、NSArray、NSDictionary类型的属性使用的修饰属性不为copy时,发出警告。

                             

                            3.1: 创建插件

                            在llvm-project/llvm/tools/clang/tools目录下新建插件目录XJPlugin:


                            图片


                            修改llvm-project/llvm/tools/clang/tools目录(即同目录)下的CMakeLists.txt文件,在最下面新增add_clang_subdirectory(XJPlugin)。


                            图片


                            在XJPlugin目录下新建一个名为XJPlugin.cpp的文件和CMakeLists.txt的文件。在CMakeLists.txt中添加如下代码:

                              
                              // 通过终端在XJPlugin目录下创建这两个文件
                              
                              touch CJLPlugin.cpp 
                              
                              touch CMakeLists.txt
                              
                              
                              
                              // CMakeLists.txt文件中添加如下代码 add_llvm_library( XJPlugin MODULE BUILDTREE_ONLY  XJPlugin.cpp )

                              (滑动显示更多)


                              图片


                              接下来使用cmake重新构建一下Xcode项目,终端进入build_xcode目录,运行如下命令:

                                cmake -G Xcode -DLLVM_ENABLE_PROJECTS='libcxx;libcxxabi;clang;clang-tools-extra;compiler-rt' -DLLDB_USE_SYSTEM_DEBUGSERVER=ON -DLLDB_TEST_COMPILER=clang++ -DCMAKE_OSX_SYSROOT=$OSX_COMMANDLINE_SDKROOT ../llvm

                                (滑动显示更多)


                                重新进入build_xcode目录打开LLVM.xcodeproj,然后添加XJPlugin的scheme,并进行编译:


                                图片


                                现在在LLVM的Xcode项目的Loadable modules目录下就可以看到我们的XJPlugin目录了。接下来就在里面编写插件代码。


                                图片



                                工程目录非常多,可以全选之后按住command键,鼠标左键点击目录左边的箭头全部折叠,这样就方便找到Loadable modules目录了。


                                3.2: 编写插件代码

                                在XJPlugin.cpp文件中加入如下代码:

                                  
                                  #include 
                                  
                                  #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 XJPlugin {
                                      // 第三步:扫描完毕回调     // 4、自定义回调类,继承自MatchCallback     class XJMatchCallback : public MatchFinder::MatchCallback {
                                      private:         // CI传递路径:XJASTAction类中的CreateASTConsumer方法参数 -> XJASTConsumer的构造函数 -> XJMatchCallback的私有属性,通过构造函数从XJASTConsumer构造函数中获取         CompilerInstance &CI;
                                          // 判断是否是自己的文件         bool isUserSourceCode(const string fileName) {             // 文件名不为空             if (fileName.empty()) return false;             // 非Xcode中的代码都认为是用户的             if (0 == fileName.find("/Applications/Xcode.app/")) return false;             return true;         }
                                          // 判断是否应该用copy修饰         bool isShouldUseCopy(const string typeStr) {             // 判断类型是否是 NSString / NSArray / NSDictionary             if (typeStr.find("NSString") != string::npos ||                 typeStr.find("NSArray") != string::npos ||                 typeStr.find("NSDictionary") != string::npos) {                 return true;             }             return false;         }
                                      public:        // 构造方法         XJMatchCallback(CompilerInstance &CI):CI(CI) {}
                                          // 重载run方法         void run(const MatchFinder::MatchResult &Result) {             // 通过Result获取节点对象,根据节点id("objcPropertyDecl")获取(此id需要与XJASTConsumer构造方法中bind的id一致)             const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs("objcPropertyDecl");             // 获取文件名称(包含路径)             string fileName = CI.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str();             // 如果节点有值 && 是用户文件             if (propertyDecl && isUserSourceCode(fileName)) {                 // 获取节点的类型,并转成字符串                 string typeStr = propertyDecl->getType().getAsString();                 // 节点的描述信息                 ObjCPropertyAttribute::Kind attrKind = propertyDecl->getPropertyAttributes();                 // 应该使用copy,但是没有使用copy                 if (isShouldUseCopy(typeStr) && !(attrKind & ObjCPropertyAttribute::kind_copy)) {                     // 通过CI获取诊断引擎                     DiagnosticsEngine &diag = CI.getDiagnostics();                     // Report 报告                     /**                      错误位置:getLocation 节点位置                      错误:getCustomDiagID(等级,提示)                      */                     diag.Report(propertyDecl->getLocation(), diag.getCustomDiagID(DiagnosticsEngine::Warning, "%0 - 这个属性推荐使用copy修饰!!"))<< typeStr;                 }             }         }     };
                                      // 第二步:扫描配置完毕     // 3、自定义XJASTConsumer,继承自抽象类 ASTConsumer,用于监听AST节点的信息 -- 过滤器     class XJASTConsumer : public ASTConsumer {     private:         // AST 节点查找器(过滤器)         MatchFinder matcher;         // 回调对象         XJMatchCallback callback;
                                      public:         // 构造方法中创建MatchFinder对象         XJASTConsumer(CompilerInstance &CI):callback(CI) { // 构造即将CI传递给callback             // 添加一个MatchFinder,每个objcPropertyDecl节点绑定一个objcPropertyDecl标识(去匹配objcPropertyDecl节点)             // 回调callback,其实是在CJLMatchCallback里面重写run方法(真正回调的是回调run方法) matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &callback);         }
                                          // 重载两个方法 HandleTopLevelDecl 和 HandleTranslationUnit
                                          // 解析完毕一个顶级的声明就回调一次(顶级节点,即全局变量,属性,函数等)         bool HandleTopLevelDecl(DeclGroupRef D) { //            cout<<"正在解析..."<             return true;         }
                                          // 当整个文件都解析完毕后回调         void HandleTranslationUnit(ASTContext &Ctx) { //            cout<<"文件解析完毕!!!"<             // 将文件解析完毕后的上下文context(即AST语法树) 给 matcher             matcher.matchAST(Ctx);         }     };
                                      //2、继承PluginASTAction,实现我们自定义的XJASTAction,即自定义AST语法树行为     class XJASTAction : public PluginASTAction {     public:
                                          // 重载ParseArgs 和 CreateASTConsumer方法
                                          /*          解析给定的插件命令行参数          - param CI 编译器实例,用于报告诊断。          - return 如果解析成功,则为true;否则,插件将被销毁,并且不执行任何操作。该插件负责使用CompilerInstance的Diagnostic对象报告错误。          */         bool ParseArgs(const CompilerInstance &CI, const std::vector &arg) {             return true;         }
                                          // 返回自定义的XJASTConsumer对象,抽象类ASTConsumer的子类         unique_ptr CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {             /**              传递CI              CI用于:              - 判断文件是否是用户的              - 抛出警告              */             return unique_ptr(new XJASTConsumer(CI));         }     }; }
                                  // 第一步:注册插件,并自定义XJASTAction类 // 1、注册插件 static FrontendPluginRegistry::Add X("XJPlugin", "this is XJPlugin");

                                  (滑动显示更多)


                                  原理主要分为三步:


                                  • 【第一步】注册插件,并自定义XJASTAction类


                                  • 自定义XJASTAction类(继承自抽象类PluginASTAction),重载两个函数ParseArgs和CreateASTConsumer,在CreateASTConsumer中创建XJASTConsumer类对象,并将编译器实例CI传递过去。CI主要用于以下两个方面


                                  • 判断文件是否是用户的


                                  • 抛出警告


                                  • 通过FrontendPluginRegistry注册插件,需要关联插件名与自定义的XJASTAction类。


                                  • 【第二步】扫描配置完毕

                                   

                                  • 自定义XJASTConsumer类(继承自ASTConsumer),声明节点查找器MatchFinder matcher和回调对象XJMatchCallback callback。


                                  • 实现构造函数,创建MatchFinder对象,并将CI传递给回调对象callback。


                                  • 重载两个方法

                                   

                                  • HandleTopLevelDecl:解析完毕一个顶级的声明就回调一次


                                  • HandleTranslationUnit:当整个文件都解析完毕后回调,将文件解析完毕后的上下文context(即AST语法树)给matcher。


                                  • 【第三步】扫描完毕的回调

                                   

                                  • 自定义回调类XJMatchCallback(继承自MatchCallback),声明私有变量CI,用于接收ASTConsumer类传递过来的CI。


                                  • 重写run方法

                                   

                                  • 1、通过Result根据节点id获取节点对象(此id需要与XJASTConsumer构造方法中bind的id一致)。


                                  • 2、判断节点有值并且是用户文件


                                  • 3、获取属性节点的描述信息


                                  • 4、获取属性节点的类型,并转成字符串


                                  • 5、判断属性是否需要用copy但是没有用copy


                                  • 6、通过CI获取诊断引擎


                                  • 7、通过诊断引擎报告错误


                                  通过终端测试插件:

                                    
                                    // 命令格式
                                    
                                    自己编译的clang文件路径 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk(SDK路径)/ -Xclang -load -Xclang 插件(.dyld)路径 -Xclang -add-plugin -Xclang 插件名 -c 源码路径
                                    
                                    
                                    
                                    // 例子 /Users/用户名/llvm-project/build_xcode/Debug/bin/clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk/ -Xclang -load -Xclang /Users/用户名/llvm-project/build_xcode/Debug/lib/XJPlugin.dylib -Xclang -add-plugin -Xclang XJPlugin -c /Users/用户名/Desktop/DemoCode/PluginTestDemo/PluginTestDemo/ViewController.m

                                    (滑动显示更多)


                                    图片


                                    3.3: Xcode集成插件

                                    此插件只作为研究clang之用,实际开发的项目中最好不要继承,因为会影响Xcode编译速度。此插件集成是针对项目,不是针对整个Xcode,测试项目可以放心集成。


                                    3.3.1: 加载插件

                                    打开测试项目,在target -> Build Settings -> Other C Flags添加如下内容:


                                    图片


                                    3.3.2: 设置编译器

                                    由于clang插件需要使用对应的版本去加载,如果版本不一致会导致编译失败,如下所示:


                                    图片


                                    • 在Build Settings栏目中新增两项用户定义的设置,分别是CC和CXX

                                     

                                    • CC 对应的是自己编译的clang的绝对路径


                                    • CXX 对应的是自己编译的clang++的绝对路径


                                    图片


                                    • 接下来在Build Settings中搜索index,将Enable Index-Wihle-Building Functionality的Default改为NO


                                    图片


                                    • 最后,重新编译测试项目,会出现我们想要的效果:


                                    图片


                                    图片

                                    总结

                                    关于LLVM和Clang的研究到此就告一段落了。下篇文章将进入启动优化的探索,敬请期待。


                                    来源:掘金

                                    作者:温暖



                                    请使用浏览器的分享功能分享到微信等