美文网首页
iOS 编绎生成 clang 编绎器 + clang 插件开发

iOS 编绎生成 clang 编绎器 + clang 插件开发

作者: 笔头还没烂 | 来源:发表于2022-04-08 19:32 被阅读0次

    最近在研究 LLVM,网上看了很多这方面的教程,照着做总出现这样那样的问题,估计是时间隔太久,部分更新导致之前的东西出问题了,于是自己重新整理了一下,基本把坑都踩完了。希望能帮到有需要的童鞋,让有兴趣的童鞋少踩点坑。

    先看最终效果,如图所示:


    image.png

    为了达到这样的效果,无论步骤多么繁琐,都是激励自己实现效果的最好动力!

    ps:
    (1)生成的 clang 版本是 15.0 。
    (2)部分步骤重复的,会用步骤前面的序号代替详细说明。如 3.1 即是下载 LLVM。
    (3)插件的代码都是亲测过,可正常运行。可直接复制使用。

    一、附上官网的链接:
    https://llvm.org/docs/GettingStarted.html#getting-started-with-llvm

    二、步骤总览:
    2.1、下载 LLVM 工程;
    2.2、安装 cmake工具;
    2.3、把 llvm-project 目录下的 clang 文件夹拷贝到 llvm 目录下;在llvm目录下找到CMakeLists.txt,然后搜索 add_subdirectory(projects),并在其后面添加 add_subdirectory(clang);
    2.4、用cmake命令生成我们的 llvm 项目(包含clang);
    2.5、编绎我们的 clang 项目,生成clang编绎器;
    2.6、编写自己的插件
    2.7、测试插件
    2.8、根据需求,修改插件代码
    2.9、将clang插件集成到Xcode中

    三、下面分步骤详细解说:
    3.1、下载 LLVM
    mac 直接通过下面官方链接在终端用 git clone 命令将项目克隆下来即可。项目还挺大的,整个项目克隆下来大概 3.5G 左右。(克隆之后的项目已包含clang)
    官方链接:git clone https://github.com/llvm/llvm-project.git
    目录结构如下图所示:

    image.png

    3.2、安装 cmake工具。(如已安装,可直接跳过这一步)
    (1)先检查mac是否已安装cmake工具:
    打开终端输入cmake,如下图所示:


    image.png

    如果提示command not found,则说明未安装cmake

    (2)进入cmake官方下载页面:https://cmake.org/download/,完成下载安装,双击打开后界面如下图所示:

    image.png
    为了能在终端使用cmake命令,点击上方菜单栏Tools,选择"How to install For Command Line Use"
    image.png
    这里cmake提供三种方式,如下图所示:
    image.png
    这里可以选择其中一种方式。以第一种方式为例,拷贝第一种方式提供的路径,在前面加export,在mac电脑的 Home 目录的.bash_profile文件底部追加(类似于配置环境变量):
    export PATH="/private/var/folders/4w/vyrtq4g54p16r733bx9cr79r0000gn/T/AppTranslocation/F6102686-D9D7-4E93-9034-2E77D6E07DF9/d/CMake.app/Contents/bin":"$PATH"
    

    如果没有该文件,可以直接创建.bash_profile文件并追加该环境变量。如下图所示


    image.png

    接着,打开我们的终端Terminal(默认已经是在家目录的路径下,如果没有,切换到家目录下即可)执行下面的命令,让我们刚才配置的环境变量生效:

    source .bash_profile
    

    最后,尝试一下cmake命令是否有效:

    cmake --version
    

    可以看到,我们的cmake已经能正常使用了,如下图所示:


    image.png

    3.3、为了能在llvm工程中包含 clang scheme,我们需要做两步操作:
    (1)把 llvm-project 目录下的clang文件夹拷贝到llvm目录下。
    (2)在llvm目录下找到CMakeLists.txt,然后搜索 add_subdirectory(projects),并在其后面添加 add_subdirectory(clang)。如下图所示:


    image.png

    (ps:如果没有执行这一步,我们生成的 llvm 项目是没有包含 clang scheme 的。这一点要注意。)

    3.4、在终端依次执行以下命令,生成我们的 llvm 项目:
    (1)cd llvm-project
    (2)mkdir build
    (3)cd build
    (4)cmake -G Xcode ../llvm
    第 4 个命令执行完之后,cmake工具会帮我们在llvm-project目录下的 build 目录下生成包含 clang 和 clangTooling scheme 的llvm Xcode工程。

    3.5、在 build 目录下双击打开 llvm 工程,会有如下图所示的提示:


    image.png

    直接选默认蓝色的第一个:自动创建 schemes即可。

    3.6、点击Xcode选择要编绎的项目的位置,会弹出所有的子项目。我们滚动到最后,选择管理我们的schemes。找到 clang scheme 将并它放在比较靠前的位置,这里是为了方便后续可以快速找到它并对它进行编绎。如下图所示:


    image.png
    image.png
    image.png

    3.7、编绎我们的 clang 项目。这里要花的时间比较漫长,时间的长短取决于机器的性能。编绎完成后,会生成 clang 可执行文件,我们可以在 llvm-project 目录下的 build 目录下的 Debug 目录下的 bin 目录下找到它。如下图所示:


    image.png

    到这里,我们已经知道如何编绎生成 clang 文件了。接下来,我们可以开始编写我们的插件,让编译好的 clang 和我们插件结合一起,发挥出一些独特的功能。

    四、编写插件代码的准备工作。
    传统的编绎流程分为:前端 + 优化器 + 后端。
    前端负责源码的解析、词义分析、语法分析(构建抽象语法树),LLVM的前端还会生成中间代码。
    优化器负责进行各种优化、改善代码运行时间等。
    后端负责将代码映射到各种目标指令集。生成机器语言,并对机器语言进行优化。

    4.1、首先,我们在 llvm-project/llvm/clang/tools/ 新建目录WXPlugin,然后在WXPlugin目录下创建两个文件:CMakeLists.txt 和 WXPlugin.cpp。
    4.2、在 CMakeLists.txt 文件中添加下面的代码:

    add_llvm_library( WXPlugin MODULE BUILDTREE_ONLY WXPlugin.cpp )
    

    4.3、在与 WXPlugin 同一个目录中找到 CMakeLists.txt 文件,并在该文件中添加下下代码:

    add_clang_subdirectory(WXPlugin)
    

    如下图所示:


    image.png

    4.4、参考步骤 3.4,重新在build目录下执行cmake命令。
    4.5、参考步骤 3.5,双击打开Xcode 工程,提示是否自动创建 scheme,选自动创建。
    4.6、于是,我们可以在Xcode工程中的 Loadable modules 中找到我们添加的插件。


    image.png

    4.7、参考步骤 3.6,将 WXPlugin scheme 移动到靠前的位置,方便后续快速找到它并对它进行编绎。
    4.8、展开该目录,如下图所示,我们就可以在 .cpp 文件中编写我们的插件代码了。


    image.png

    五、编写插件代码。
    5.1、将下面的代码直接拷贝到 WXPlugin.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 WXPlugin {
    
        class WXMatchCallback: 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;
        }
    
        //判断是否应该用copy修饰
        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:
            WXMatchCallback(CompilerInstance &CI):CI(CI){}
            //真正的回调
            void run(const MatchFinder::MatchResult &Result) {
            //通过result拿到节点
            const ObjCPropertyDecl * propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");
                if (propertyDecl) {
                    string typeStr = propertyDecl->getType().getAsString();
                    cout<<"-------拿到了:"<<typeStr<<"-------"<<endl;
                }
        };
    };
    
    
    //自定义WXConsumer
    class WXConsumer: public ASTConsumer{
    private:
        //AST节点的查找过程
        MatchFinder matcher;
        WXMatchCallback callback;
    public:
        
        WXConsumer(CompilerInstance &CI):callback(CI){
            //添加一个MatchFinder去匹配objcPropertyDecl节点
            //回调在WXMatchCallback里面run方法!
            matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &callback);
        }
        
        
        //解析完一个顶级的声明就回调一次
        bool HandleTopLevelDecl(DeclGroupRef D) {
    //        cout<<"正在解析……"<<endl;
            return true;
        }
        
        //整个文件都会解析完成的回调
        void HandleTranslationUnit(ASTContext &Ctx) {
    //        cout<<"文件解析完毕!"<<endl;
            matcher.matchAST(Ctx);
        }
    };
    
    
    //继承PluginASTAction 实现我们自定义的Action
    class WXASTACtion:public PluginASTAction{
    public:
        bool ParseArgs(const CompilerInstance &CI,const std::vector<std::string> &arg){
            
            return true;
        }
        
        unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,StringRef InFile){
            return unique_ptr<WXConsumer>(new WXConsumer(CI));
        }
    };
    
    }
    
    
    //注册插件
    static FrontendPluginRegistry::Add<WXPlugin::WXASTACtion>WX("WXPlugin","this is WXPlugin");
    

    5.2、编绎我们的 WXPlugin scheme。编绎后生成的 clang 可执行文件和 WXPlugin 插件可以通过 Xcode 工程中的 Product 目录下找到对应的文件 Show In Finder自动跳转到文件所在的目录,如下图所示:


    image.png
    image.png
    image.png

    也可以在build 目录下中的Debug子目录 bin 和 lib两个目录中找到。


    image.png
    image.png
    当然,每次我们更新了 插件的代码,就需要重新编绎生成我们的新的插件。

    六、测试插件
    (1)我们先用终端来测试
    命令如下:

    自己编绎的 clang 路径 -isysroot  Xcode_sdk的路径 -Xclang -load -Xclang  自己编绎的插件生成的插件路径 -Xclang -add-plugin -Xclang 插件的名字 -c 源码路径
    

    例子如下:

    /Users/pilipala/Downloads/0404/llvm-project/build/Debug/bin/clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator15.0.sdk -Xclang -load -Xclang /Users/pilipala/Downloads/0404/llvm-project/build/Debug/lib/WXPlugin.dylib  -Xclang -add-plugin -Xclang WXPlugin -c /Users/pilipala/Downloads/0406/Test/Test/ViewController.m
    

    当键盘敲下回车的那一瞬间,我们能看到激动人心的效果,如下所示,这说明我们的插件测试是ok的:

    build % /Users/pilipala/Downloads/0404/llvm-project/build/Debug/bin/clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator15.0.sdk -Xclang -load -Xclang /Users/pilipala/Downloads/0404/llvm-project/build/Debug/lib/WXPlugin.dylib  -Xclang -add-plugin -Xclang WXPlugin -c /Users/pilipala/Downloads/0406/Test/Test/ViewController.m
    -------拿到了:NSUInteger-------
    -------拿到了:Class-------
    -------拿到了:NSString *-------
    -------拿到了:NSString *-------
    -------拿到了:BOOL-------
    -------拿到了:Class _Nonnull-------
    -------拿到了:id _Nonnull-------
    -------拿到了:NSArray<ObjectType> * _Nonnull-------
    -------拿到了:NS_RETURNS_INNER_POINTER const char *-------
    -------拿到了:id _Nullable-------
    -------拿到了:void * _Nullable-------
    -------拿到了:char-------
    -------拿到了:unsigned char-------
    -------拿到了:short-------
    -------拿到了:unsigned short-------
    -------拿到了:int-------
    -------拿到了:unsigned int-------
    -------拿到了:long-------
    -------拿到了:unsigned long-------
    -------拿到了:long long-------
    -------拿到了:unsigned long long-------
    -------拿到了:float-------
    -------拿到了:double-------
    -------拿到了:BOOL-------
    ……
    ……
    

    七、根据需求修改我们的插件代码,过滤一些系统节点。这里我们以属性 NSString 不能用 strong 修饰,如果用了strong 修饰,我们给以警告提示为例。插件的完整的代码如下:

    
    #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 WXPlugin {
    
        class WXMatchCallback: 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;
        }
    
        //判断是否应该用copy修饰
        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:
            WXMatchCallback(CompilerInstance &CI):CI(CI){}
            //真正的回调
            void run(const MatchFinder::MatchResult &Result) {
            //通过result拿到节点
                const ObjCPropertyDecl * propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");
                string fileName = CI.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str();
                if (propertyDecl && isUserSourceCode(fileName)) {
                    
                    string typeStr = propertyDecl->getType().getAsString();
                    
                    ObjCPropertyAttribute::Kind attrKind = propertyDecl->getPropertyAttributes();
                    
                    if (isShouldUseCopy(typeStr) && !(attrKind & ObjCPropertyDecl::Copy)) {
                        DiagnosticsEngine &diag = CI.getDiagnostics();
                        diag.Report(propertyDecl->getBeginLoc(), diag.getCustomDiagID(DiagnosticsEngine::Warning, "%0这个地方推荐使用copy!!"))<<typeStr;
                    }
                    
                    cout<<"----获取到了:"<<typeStr<<"------"<<"属于----"<<fileName<<"------"<<endl;
                }
                
            };
    };
    
    
    //自定义WXConsumer
    class WXConsumer: public ASTConsumer{
    private:
        //AST节点的查找过程
        MatchFinder matcher;
        WXMatchCallback callback;
    public:
        
        WXConsumer(CompilerInstance &CI):callback(CI){
            //添加一个MatchFinder去匹配objcPropertyDecl节点
            //回调在WXMatchCallback里面run方法!
            matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &callback);
        }
        
        
        //解析完一个顶级的声明就回调一次
        bool HandleTopLevelDecl(DeclGroupRef D) {
    //        cout<<"正在解析……"<<endl;
            return true;
        }
        
        //整个文件都会解析完成的回调
        void HandleTranslationUnit(ASTContext &Ctx) {
    //        cout<<"文件解析完毕!"<<endl;
            matcher.matchAST(Ctx);
        }
    };
    
    
    //继承PluginASTAction 实现我们自定义的Action
    class WXASTACtion:public PluginASTAction{
    public:
        bool ParseArgs(const CompilerInstance &CI,const std::vector<std::string> &arg){
            
            return true;
        }
        
        unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,StringRef InFile){
            return unique_ptr<WXConsumer>(new WXConsumer(CI));
        }
    };
    
    }
    
    
    //注册插件
    static FrontendPluginRegistry::Add<WXPlugin::WXASTACtion>WX("WXPlugin","this is WXPlugin");
    

    重新编译生成插件之后,我们还是先用终端来测试,测试成功如下图所示:


    image.png

    八、将Clang编绎器集成到Xcode中
    8.1、在Xcode项目中,做以下配置:
    (1)在BuildSettings 中搜索Other C Flags,将下面的内容配置到 other C Flags:

    -Xclang -load -Xclang 插件的路径 -Xclang -add-plugin -Xclang 插件名
    

    举个例子,如下所示:

    -Xclang -load -Xclang /Users/pilipala/Downloads/0404/llvm-project/build/Debug/lib/WXPlugin.dylib  -Xclang -add-plugin -Xclang WXPlugin
    

    如下图所示:


    image.png

    (2)在BuildSettings 中添加两项用户自定义,如下图所示:


    image.png
    其中 CC 对应自己编译后的 clang 的绝对路径;CXX对应自己编绎后的 clang++ 的绝对路径。
    (3)在BuildSettings 中搜索 index,将Enable Index-While-Building Functionality 选项默认的 Default 改成 NO ,如下图所示:
    image.png

    完成这三步的配置,即可完成 clang 在 Xcode 中的集成。重新编绎项目,即可看到文中开头提到的效果。恭喜,你已经了解了 clang 插件开发的整个流程!

    九、你可能会遇到的问题:
    9.1、 编绎clang项目的提示 如下图所示:


    image.png

    这时需要重新走一遍第四步,用 cmake 重新编绎出我们 llvm 项目即可。因为属于增量编绎,所以不会像我们第一次编绎生成 llvm 项目那么久,会很快执行完。

    9.2、4.5步骤执行完之后,在工程 Loadable modules 中找不到我们添加的插件。
    解决方案参考如下:
    (1)检查以下拼写是否有错,建议直接复制,不要手敲:

    add_clang_subdirectory(WXPlugin)
    

    相关文章

      网友评论

          本文标题:iOS 编绎生成 clang 编绎器 + clang 插件开发

          本文链接:https://www.haomeiwen.com/subject/dpydsrtx.html