iOS 底层原理:Clang 插件开发

2,297 阅读7分钟

1、编译LLVM工程

1.1 LLVM下载

由于国内的网络限制,我们需要借助镜像下载LLVM的源码
mirror.tuna.tsinghua.edu.cn/help/llvm/

下载llvm项目:

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

llvmtools目录下下载Clang

cd llvm/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang.git

llvmprojects目录下下载compiler-rtlibcxxlibcxxabi

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

Clangtools下安装extra工具:

cd ../tools/clang/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang-tools-e xtra.git

1.2 安装 cmake

由于最新的LLVM只支持cmake来编译了,我们还需要安装cmake

  • 查看brew是否安装cmake如果有就跳过下面步骤
    brew list
    
  • 通过brew安装cmake 
    brew install cmake
    

1.3 编译 LLVM

1.3.1 通过 xcode 编译 LLVM

cmake编译成Xcode项目

mkdir build_xcode
cd build_xcode
cmake -G Xcode ../llvm

使用Xcode编译Clang

自动创建Schemes时间会较长,所以我们选择手动管理:

image.png

点击左下⻆加号,在Target中添加clangclangTooling

image.png

分别选择clangclangTooling进行编译(编译时间较长):

image.png

1.3.2 通过 ninja 编译 LLVM

  • 使用ninja进行编译则还需要安装ninja。使用$ brew install ninja命令即可安装ninja。
  • llvm源码根目录下新建一个build_ninja目录,最终会在build_ninja目录下生成build.ninja
  • llvm源码根目录下新建一个llvm_release目录,最终编译文件会在llvm_release文件夹路径下。
    $ cd llvm_build
    $ cmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX= 安装路径(本机为/Users/xxx/xxx/LLVM/llvm_release,注意DCMAKE_INSTALL_PREFIX后面不能有空格
    
  • 依次执行编译、安装指令
      $ ninja
      $ ninja install
    

2、创建Clang插件

/llvm/tools/clang/tools目录下新建插件HKPlugin

image.png

修改/llvm/tools/clang/tools目录下CMakeLists.txt文件,新增add_clang_subdirectory(HKPlugin)

image.png

HKPlugin目录下新建一个名为HKPlugi.cpp的文件和CMakeLists.txt的文件:

image.png

CMakeLists.txt中写入下面的内容:

add_llvm_library( HKPlugin MODULE BUILDTREE_ONLY
  HKPlugin.cpp
)

接下来利用cmake重新生成一下Xcode项目,在build_xcode中执行cmake -G Xcode ../llvm命令。

最后可以在LLVMXcode项目中看到Loadable modules目录下有自己的Plugin目录了,我们可以在里面编写插件代码:

image.png

3、编写插件代码

3.1 顶级节点的解析

HKPlugin.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;

// 定义命名空间
namespace HKPlugin {

    // 自定义的HKConsumer
    class HKConsumer:public ASTConsumer{
        public:
            // 解析完毕一个顶级的声明就回调一次
            bool HandleTopLevelDecl(DeclGroupRef D) {
                cout<<"正在解析..."<<endl;
                return true;
            }
            // 当整个文件都解析完成后回调
            void HandleTranslationUnit(ASTContext &Ctx) {
                cout<<"文件解析完成..."<<endl;
            }
    };
    
    // 定义一个类 继承于PluginASTAction
    class HKASTAction:public PluginASTAction{
        public:
            std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {
                return unique_ptr<HKConsumer> (new HKConsumer);
            }
            bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &arg) {
                return true;
            }
    };
}

// 注册插件
static FrontendPluginRegistry::Add<HKPlugin::HKASTAction> X("HKPlugin", "This is the description of the plugin");

编译HKPlugin项目,在项目的Products目录下找到clangShow In Finder查看可执行文件:

image.png

同样的方式找到HKPlugin.dylib的可执行文件:

image.png

随便找个地方创建hello.m文件,写入下面代码:

int sum(int a);
int a;

int sum(int a){
    int b = 10;
    return 10 + b;
}

int sum2(int a,int b){
    int c = 10;
    return a + b + c;
}

执行下面命令,测试插件:

// 自己编译的clang路径 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk/ -Xclang -load -Xclang 插件(.dylib)路径 -Xclang -add-plugin -Xclang 插件名称 -c 源码路径

示例[:](url)
/Users/lcy/study/build_xcode/Debug/bin/clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk -Xclang -load -Xclang /Users/lcy/study/build_xcode/Debug/lib/HKPlugin.dylib -Xclang -add-plugin -Xclang HKPlugin -c ./hello.m

-------------------------

//输出以下内容:
正在解析...
正在解析...
正在解析...
正在解析...
文件解析完成...

3.2 分析OC代码

创建App项目,在ViewController.m中写入下面代码:

#import "ViewController.h"

@interface ViewController ()

@property(nonatomic, strong) NSString* name;
@property(nonatomic, strong) NSArray* arrs;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
}

@end

执行clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk -fmodules -fsyntax-only -Xclang -ast-dump ViewController.m命令,得到AST代码:

image.png

  • ObjCPropertyDecl节点中,namearrs的类型、修饰符、位置等信息都详细的展示出来了。

3.3 MatchFinder 过滤AST节点

HKPlugin.cpp中添加MatchFinder相关代码:

// 导入插件使用的头文件
#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 HKPlugin {

    class HKMatchCallBack: public MatchFinder::MatchCallback {
    public:
        void run(const MatchFinder::MatchResult &Result) {
            // 通过结果获取到节点信息
            const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");
            if (propertyDecl) { // 如果拿到了属性节点值
                string typeStr = propertyDecl -> getType().getAsString();
                cout<<"--------拿到了:"<<typeStr<<endl;
            }
        }
    };

    // 自定义的HKConsumer
    class HKConsumer:public ASTConsumer{
        private:
            // AST 节点过滤器
            MatchFinder matcher;
            HKMatchCallBack callback;
        
        public:
            HKConsumer() {
                // 添加一个MatchFinder去匹配-ObjCPropertyDecl节点
                // 找到需要的节点,进行回调,回调的是一个类
                matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &callback);
            }
            // 解析完毕一个顶级的声明就回调一次
            bool HandleTopLevelDecl(DeclGroupRef D) {
                cout<<"正在解析..."<<endl;
                return true;
            }
            // 当整个文件都解析完成后回调
            void HandleTranslationUnit(ASTContext &Ctx) {
                cout<<"文件解析完成..."<<endl;
                matcher.matchAST(Ctx);
            }
    };
    
    // 定义一个类 继承于PluginASTAction
    class HKASTAction:public PluginASTAction{
        public:
            std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {
                return unique_ptr<HKConsumer> (new HKConsumer);
            }
            bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &arg) {
                return true;
            }
    };
}

// 注册插件
static FrontendPluginRegistry::Add<HKPlugin::HKASTAction> X("HKPlugin", "This is the description of the plugin");

针对ViewController.m测试插件:

image.png

  • namearrs的类型被打印了出来。
  • 因为头文件的展开,系统文件的属性也被打印了出来,数量还非常多,我们要想办法过滤这些系统文件。

3.4 过滤系统文件

通过文件路径来判断,是不是系统文件,系统文件都在Xcode的包里面,也就是以/Applications/Xcode.app开头的路径。

HKPlugin.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 HKPlugin {

    class HKMatchCallBack: 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;
        }
    public:
        HKMatchCallBack(CompilerInstance &CI):CI(CI){}
        void run(const MatchFinder::MatchResult &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();
                cout<<"--------拿到了:"<<typeStr<<"它属于文件:"<<fileName<<endl;
            }
        }
    };

    // 自定义的HKConsumer
    class HKConsumer:public ASTConsumer{
        private:
            // AST 节点过滤器
            MatchFinder matcher;
            HKMatchCallBack callback;
        
        public:
            HKConsumer(CompilerInstance &CI):callback(CI) {
                // 添加一个MatchFinder去匹配-ObjCPropertyDecl节点
                // 找到需要的节点,进行回调,回调的是一个类
                matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &callback);
            }
            // 解析完毕一个顶级的声明就回调一次
            bool HandleTopLevelDecl(DeclGroupRef D) {
                cout<<"正在解析..."<<endl;
                return true;
            }
            // 当整个文件都解析完成后回调
            void HandleTranslationUnit(ASTContext &Ctx) {
                cout<<"文件解析完成..."<<endl;
                matcher.matchAST(Ctx);
            }
    };
    
    // 定义一个类 继承于PluginASTAction
    class HKASTAction:public PluginASTAction{
        public:
            std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {
                return unique_ptr<HKConsumer> (new HKConsumer(CI));
            }
            bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &arg) {
                return true;
            }
    };
}

// 注册插件
static FrontendPluginRegistry::Add<HKPlugin::HKASTAction> X("HKPlugin", "This is the description of the plugin");

针对ViewController.m测试插件:

image.png

  • 这时候就剔除了系统的文件。

3.5 Copy修饰符校验 发出警告信息

我们对属性修饰符进行校验,应该使用copy,但是没有使用copy的,发出警告信息。

HKPlugin.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 HKPlugin {

    class HKMatchCallBack: 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:
        HKMatchCallBack(CompilerInstance &CI):CI(CI){}
        void run(const MatchFinder::MatchResult &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();
                // 拿到节点的描述信息
                ObjCPropertyDecl::PropertyAttributeKind attrKind = propertyDecl->getPropertyAttributes();
                
                if (isShouldUseCopy(typeStr) && !(attrKind & ObjCPropertyDecl::OBJC_PR_copy)) { // 应该使用copy,但是没有使用copy
                    // 诊断引擎
                    DiagnosticsEngine &diag = CI.getDiagnostics();
                    // report 报告
                    diag.Report(propertyDecl->getLocation(),diag.getCustomDiagID(DiagnosticsEngine::Warning, "这个地方应该用copy"));
                }
            }
        }
    };

    // 自定义的HKConsumer
    class HKConsumer:public ASTConsumer{
        private:
            // AST 节点过滤器
            MatchFinder matcher;
            HKMatchCallBack callback;
        
        public:
            HKConsumer(CompilerInstance &CI):callback(CI) {
                // 添加一个MatchFinder去匹配-ObjCPropertyDecl节点
                // 找到需要的节点,进行回调,回调的是一个类
                matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &callback);
            }
            // 解析完毕一个顶级的声明就回调一次
            bool HandleTopLevelDecl(DeclGroupRef D) {
                cout<<"正在解析..."<<endl;
                return true;
            }
            // 当整个文件都解析完成后回调
            void HandleTranslationUnit(ASTContext &Ctx) {
                cout<<"文件解析完成..."<<endl;
                matcher.matchAST(Ctx);
            }
    };
    
    // 定义一个类 继承于PluginASTAction
    class HKASTAction:public PluginASTAction{
        public:
            std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {
                return unique_ptr<HKConsumer> (new HKConsumer(CI));
            }
            bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &arg) {
                return true;
            }
    };
}

// 注册插件
static FrontendPluginRegistry::Add<HKPlugin::HKASTAction> X("HKPlugin", "This is the description of the plugin");

针对ViewController.m测试插件:

image.png

  • namearrs属性报出了警告,并指出了它们所在的位置信息。

3.6 Xcode集成编译器插件

3.6.1 加载插件

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

-Xclang -load -Xclang (.dylib)动态库路径 -Xclang -add-plugin -Xclang HKPlugin

image.png

3.6.2 设置编译器

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

error: unable to load plugin '/Users/lcy/study/build_xcode/Debug/lib/HKPlugin.dylib': 'dlopen(/Users/lcy/study/build_xcode/Debug/lib/HKPlugin.dylib, 9): Symbol not found: __ZN5clang12ast_matchers16objcPropertyDeclE
  Referenced from: /Users/lcy/study/build_xcode/Debug/lib/HKPlugin.dylib
  Expected in: flat namespace
 in /Users/lcy/study/build_xcode/Debug/lib/HKPlugin.dylib'
/Users/lichunyang/Library/Developer/Xcode/DerivedData/MyDemo-asmouncvgpgfyzccmbpymrxoyjha/Build/Intermediates.noindex/MyDemo.build/Debug-iphonesimulator/MyDemo.build/Objects-normal/x86_64/main.dia:1:1: warning: Could not read serialized diagnostics file: error("Failed to open diagnostics file") (in target 'MyDemo' from project 'MyDemo')
Command CompileC failed with a nonzero exit code

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

image.png image.png

  • 分别是CCCXXCC对应的是自己编译的clang的绝对路径,CXX对应的是自己编译的clang++的绝对路径。

编译项目,错误信息就不一样了,不同的clang版本的错误没有了:

image.png

接下来在Build Settings栏目中搜索index,将Enable Index-Wihle-Building FunctionalityDefault改为NO

image.png

最后编译项目,终于看到了我们想要的效果,对属性进行了⚠️提示:

image.png