iOS-底层探索29:自定义Clang插件

iOS 底层探索 文章汇总

目录


一、LLVM编译

1.1、LLVM 下载

当前系统环境如下:
OS:macOS Big Sur, 芯片:Apple Silicon M1
根据Mac当前系统下载LLVM Releases源码,保存的路径中不要包含空格之类的字符,这里选择当前最新的llvmorg-11.0.0源码。
https://github.com/llvm/llvm-project/releases/tag/llvmorg-11.0.0
下载 Source code(zip)

1.2、LLVM编译

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

安装 cmake
  • 查看brew是否安装cmake如果有就跳过下面的步骤
brew list
  • 通过brew安装cmake
brew install cmake
1.2.1、通过Xcode编译LLVM

cd llvm-project文件夹
mkdir build_xcode
cd build_xcode
cmake -G Xcode ../llvm

cmake完成后会在build_xcode中出现Xcode项目
Xcode中使用Automatically Create Schemes然后选择ALL_BUILD Schemes编译项目(Xcode能编译成功)

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

二、创建自定义Clang插件

2.1、创建插件

1、在/llvm-project/clang/tools目录下新建插件CJPlugin

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

image.png

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

add_llvm_library( CJPlugin MODULE BUILDTREE_ONLY
  CJPlugin.cpp
)

4、接下来利用cmake重新生成一下Xcode项目

cd build_xcode
cmake -G Xcode -DLLVM_ENABLE_PROJECTS="clang;libcxx;libcxxabi" ../llvm //此命令会编译clang目录下的自定义插件

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

2.2、编写插件代码

#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 CJPlugin {
class CJASTConsumer: public ASTConsumer {
public:
    // clang 解析完一个顶级的声明的回调
    bool HandleTopLevelDecl(DeclGroupRef D) {
        cout<<"正在解析..."<<endl;
        return true;
    }
    
    // clang 解析完整个文件的回调
    void HandleTranslationUnit(ASTContext &context) {
        cout<<"文件解析完毕!"<<endl;
    }
};

// 继承PluginASTAction实现我们自定义的Action
class CJASTAction: public PluginASTAction {
public:
    unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef iFile) {
        return unique_ptr<CJASTConsumer> (new CJASTConsumer);
    }
    
    bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &args) {
        return true;
    }
};

// 注册插件
static FrontendPluginRegistry::Add<CJPlugin::CJASTAction> CJ("CJPlugin", "This is the description of the plugin");
}

2.3、测试插件代码

  • 编译Xcode项目
  • 编写测试源码
vi hello.m

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

int sum2 (int a, int b) {
       int c = 10;
       return a + b + c;
}
  • 使用自己编译的clang文件路径和插件路径测试
命令如下:

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

测试结果:

2.4、Xcode集成插件

1、新建测试项目Test_LLVM,并添加如下代码:
@interface ViewController ()

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

@end

我们接下来使用我们自定义的clang插件提示属性定义存在的问题。

使用原生的clang解析ViewController.m并生成AST

cd ViewController.m所在的目录
clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.2.sdk -fmodules -fsyntax-only -Xclang -ast-dump ViewController.m

AST中我们发现属性定义节点名为ObjCPropertyDecl,因此我们自定clang中需要过滤出ObjCPropertyDecl节点。

2、修改代码过滤出ViewController.m中的ObjCPropertyDecl节点
namespace CJPlugin {

class CJMatchCallback: public MatchFinder::MatchCallback {
private:
    CompilerInstance &CI;
    // 排除系统的属性
    bool isUserSourceCode(const string filename) {
        if (filename.empty()) return false;
        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:
    CJMatchCallback(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();
            cout<<"拿到属性:"<<typeStr<<"属于文件:"<<fileName<<endl;
            // 拿到节点的描述信息
            ObjCPropertyAttribute::Kind attrKind = propertyDecl->getPropertyAttributes();
            if (propertyDecl->getTypeSourceInfo() && isShouldUseCopy(typeStr) && !(attrKind & ObjCPropertyAttribute::kind_copy)) {
                cout<<"推荐使用 copy 修饰"<<endl;
            }
        }
    }
};

class CJASTConsumer: public ASTConsumer {
private:
    MatchFinder matcher;// AST节点查找过滤器
    CJMatchCallback callback;
public:
    CJASTConsumer(CompilerInstance &CI) :callback(CI) {
        // 添加一个MatchFinder去匹配objcPropertyDecl节点
        // 回调在CJMatchCallback中的run方法中
        matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &callback);//绑定标记"objcPropertyDecl"和取的时候一致即可
    }
    // clang 解析完一个顶级的声明的回调
    bool HandleTopLevelDecl(DeclGroupRef D) {
        cout<<"正在解析..."<<endl;
        return true;
    }
    
    // clang 解析完整个文件的回调
    void HandleTranslationUnit(ASTContext &context) {
        matcher.matchAST(context);
        cout<<"文件解析完毕!"<<endl;
    }
};

// 继承PluginASTAction实现我们自定义的Action
class CJASTAction: public PluginASTAction {
public:
    unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef iFile) {
        return unique_ptr<CJASTConsumer> (new CJASTConsumer(CI));//CI用于户过滤系统属性
    }
    
    bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &args) {
        return true;
    }
};

// 注册插件
static FrontendPluginRegistry::Add<CJPlugin::CJASTAction> CJ("CJPlugin", "This is the description of the plugin");
}

使用自定义clang解析ViewController.m代码如下:

cd ViewController.m所在的目录
/Users/ztkj/Projects/LLVM_Projects/llvm-project/build_xcode/Debug/bin/clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.3.sdk  -Xclang -load -Xclang /Users/ztkj/Projects/LLVM_Projects/llvm-project/build_xcode/Debug/lib/CJPlugin.dylib -Xclang -add-plugin -Xclang CJPlugin -c ViewController.m 

测试结果:

3、添加Xcode显示自定义提示的代码
cout<<"推荐使用 copy 修饰"<<endl;
// 诊断引擎
DiagnosticsEngine &diag = CI.getDiagnostics();
diag.Report(propertyDecl->getBeginLoc(), diag.getCustomDiagID(DiagnosticsEngine::Warning, "%0推荐使用 copy 修饰"))<<typeStr;//%必须带参数否者会报错

测试结果:

4、加载插件

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

-Xclang -load -Xclang (.dylib)动态库路径 -Xclang -add-plugin -Xclang CJPlugin
5、设置编译器
  • 由于Clang插件需要使用对应的版本去加载,如果版本不一致就会导致编译错误,如下图所示:

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

  • 分别是CCCXX
    CC对应的是自己编译的clang的绝对路径
    CXX对应的是自己编译的clang++的绝对路径
  • 接下来在Build Settings栏目中搜索index,将Enable Index-Wihle-Building Functionality的Default改为NO
6、编译Test_LLVM项目成功后即可看到Xcode显示自定义clang插件中的提示

目前自定义的插件必须编译后才能显示提示,代码修改后也不会自动更新提示,且还没完善Fix功能。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容