编译原理:如何制作clang插件来为iOS开发提效

2,340 阅读6分钟

clang插件介绍

是什么?如何使用?

clang是LLVM的一种编译器前端,将OC的sourcecode编译成语言和目标架构无关的IR实现,而使用插件可以在编译时执行额外的一些自定义操作。 clang插件即为一个dylib,可以用于在编译期间对C/C++/OC代码进行规范检查或优化等。

预热两个命令

cmake

cross platform make 跨平台的安装(编译)工具,使用CMakeList.txt来描述整个编译安装流程,然后根据目标平台进一步生成所需的本地化makefile和文件等。

而GNU的Make,则采用makefile文件。

ninja

Google开源的一个构建工具,类似GNU的Make。 最初用于解决使用Make来构建Chrome的效率问题,可用于加速LLVM源码编译。

编译llvm和clang

当前总目录为llvm_all:

llvm
llvm_build
llvm_release
llvm_xcode

下载llvm源码:

git clone https://git.llvm.org/git/llvm.git

在llvm/tools目录下中,放置clang源码

git clone https://git.llvm.org/git/clang.git

使用cmake

cd llvm_build
cmake ../llvm
cmake --build

耗时很久,大概两三个小时以上。 完成之后,在llvm_build/bin目录下多了很多二进制文件。如clang, llvm-xxx等的各种工具。

使用ninja

cd llvm_build
cmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX=../llvm_release

ninja编译

cd llvm_build
ninja

耗时十几分钟,结果:

输出 [3516/3516] Linking CXX executable bin/opt
27.16 GB

ninja install

cd llvm_build
ninja install

将很多二进制安装到 llvm_release/bin 目录下。

使用Xcode

cd llvm_xcode
cmake -G Xcode ../llvm -DCMAKE_INSTALL_PREFIX=../llvm_release

选择scheme,ALL_BUILD,执行build。

制作clang插件

随着 Xcode 变得封闭,插件挂载到 Xcode 上运行在未来的版本中可能会被禁止

注意:这里推荐 不要使用 主开发的Xcode.app。 使用 xcode-select --print-path 可查看当前主开发的Xcode.app目录。

准备其他版本的Xcode:

/xxx/Xcode_9.0.app
/xxx/Xcode_10.0.app

新建插件配置及源码文件

llvm/tools/clang/tools/目录下,新增cmake需要的配置:

在CMakeLists.txt文件添加插件信息

add_clang_subdirectory(MyPlugin)

新建MyPlugin目录,在其中新建一个CMakeLists.txt文件和MyPlugin.cpp文件:

add_llvm_library( MyPlugin MODULE BUILDTREE_ONLY
  MyPlugin.cpp
)

cpp代码中注意保持与plugin名称一致。

注意:这里CMakeLists.txt文件中填写的不是网上到处说的命令 add_llvm_loadable_module。 而是应该 仿照Loadable modules中的LLVMHello中的格式填写

使用Xcode 10.0来编译目标dylib

注意:使用Xcode 9.0版本不能build成功。而使用Xcode 10.0版本可以。猜测是当前下载的LLVM源码是最新版本的原因。

cd llvm_xcode
cmake -G Xcode ../llvm

也可以使用很多自定义参数:

cmake -G Xcode ../llvm -DCMAKE_INSTALL_PREFIX=../llvm_release

cmake -G Xcode -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES:STRING=x86_64 -DLLVM_TARGETS_TO_BUILD=host -DLLVM_INCLUDE_TESTS=OFF -DCLANG_INCLUDE_TESTS=OFF -DLLVM_INCLUDE_UTILS=OFF -DLLVM_INCLUDE_DOCS=OFF -DLLVM_INCLUDE_EXAMPLES=OFF -DLLVM_BUILD_EXTERNAL_COMPILER_RT=ON -DLIBCXX_INCLUDE_TESTS=OFF -DCOMPILER_RT_INCLUDE_TESTS=OFF -DCOMPILER_RT_ENABLE_IOS=OFF ../llvm

然后使用Xcode 10.0打开 LLVM.xcodeproj,选择ALL_BUILD执行build。

Xcode 10.0工程中Loadable modules中可看到MyPlugin的源码文件,后续也可以直接在Xcode中进行代码修改。

注意:每次要新增一个clang插件,都需要使用cmake命令 cmake -G Xcode ../llvm 来更新Xcode项目。

插件代码编写完成,使用Xcode 10.0进行build即可生成 llvm_xcode/Debug/lib/MyPlugin.dylib。

Xcode集成该clang插件

使用Xcode 9.0来加载该clang插件

注意:使用Xcode 10.0版本不行,加载插件后,build测试项目失败。而使用Xcode 9.0版本可以,可能是Xcode的封闭导致。

使用Xcode 10.0的报错信息:

could not create session: requestError(description: "unknown error while handling message: unableToInitializeCore(errors: [\"/xxx/Xcode_10.0.app/Contents/PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer/Library/Xcode/Plug-ins/HackedClang.xcplugin/Contents/Resources/Default Compiler.xcspec: warning: spec \\\':com.apple.compilers.gcc\\\' already registered from /xxx/Xcode_10.0.app/Contents/PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer/Library/Xcode/Plug-ins/Clang LLVM 1.0.xcplugin/Contents/Resources/Default Compiler.xcspec\", \"/xxx/Xcode_10.0.app/Contents/PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer/Library/Xcode/Plug-ins/HackedClang.xcplugin/Contents/Resources/HackedClang.xcspec: error: unable to declare macro for option \\\'LLVM_LTO\\\': conflictingMacroDeclarationType(previousType: XCBCore.MacroType.string, name: \\\"LLVM_LTO\\\") (while parsing \\\'com.apple.compilers.llvm.clang.hacked\\\')\", \"/xxx/Xcode_10.0.app/Contents/PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer/Library/Xcode/Plug-ins/HackedClang.xcplugin/Contents/Resources/HackedClang.xcspec: error: unable to declare macro for option \\\'CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK\\\': conflictingMacroDeclarationType(previousType: XCBCore.MacroType.string, name: \\\"CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK\\\") (while parsing \\\'com.apple.compilers.llvm.clang.hacked\\\')\", \"/xxx/Xcode_10.0.app/Contents/PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer/Library/Xcode/Plug-ins/HackedClang.xcplugin/Contents/Resources/HackedClang.xcspec: error: unable to declare macro for option \\\'GCC_WARN_UNINITIALIZED_AUTOS\\\': conflictingMacroDeclarationType(previousType: XCBCore.MacroType.string, name: \\\"GCC_WARN_UNINITIALIZED_AUTOS\\\") (while parsing \\\'com.apple.compilers.llvm.clang.hacked\\\')\", \"/xxx/Xcode_10.0.app/Contents/PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer/Library/Xcode/Plug-ins/HackedClang.xcplugin/Contents/Resources/HackedClang.xcspec: error: unable to declare macro for option \\\'CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION\\\': conflictingMacroDeclarationType(previousType: XCBCore.MacroType.string, name: \\\"CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION\\\") (while parsing \\\'com.apple.compilers.llvm.clang.hacked\\\')\", \"/xxx/Xcode_10.0.app/Contents/PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer/Library/Xcode/Plug-ins/HackedClang.xcplugin/Contents/Resources/HackedClang.xcspec: error: unable to declare macro for option \\\'CLANG_WARN_CONSTANT_CONVERSION\\\': conflictingMacroDeclarationType(previousType: XCBCore.MacroType.string, name: \\\"CLANG_WARN_CONSTANT_CONVERSION\\\") (while parsing \\\'com.apple.compilers.llvm.clang.hacked\\\')\", \"/xxx/Xcode_10.0.app/Contents/PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer/Library/Xcode/Plug-ins/HackedClang.xcplugin/Contents/Resources/HackedClang.xcspec: error: unable to declare macro for option \\\'CLANG_WARN_INT_CONVERSION\\\': conflictingMacroDeclarationType(previousType: XCBCore.MacroType.string, name: \\\"CLANG_WARN_INT_CONVERSION\\\") (while parsing \\\'com.apple.compilers.llvm.clang.hacked\\\')\", \"/xxx/Xcode_10.0.app/Contents/PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer/Library/Xcode/Plug-ins/HackedClang.xcplugin/Contents/Resources/HackedClang.xcspec: error: unable to declare macro for option \\\'CLANG_WARN_BOOL_CONVERSION\\\': conflictingMacroDeclarationType(previousType: XCBCore.MacroType.string, name: \\\"CLANG_WARN_BOOL_CONVERSION\\\") (while parsing \\\'com.apple.compilers.llvm.clang.hacked\\\')\", \"/xxx/Xcode_10.0.app/Contents/PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer/Library/Xcode/Plug-ins/HackedClang.xcplugin/Contents/Resources/HackedClang.xcspec: error: unable to declare macro for option \\\'CLANG_WARN_ENUM_CONVERSION\\\': conflictingMacroDeclarationType(previousType: XCBCore.MacroType.string, name: \\\"CLANG_WARN_ENUM_CONVERSION\\\") (while parsing \\\'com.apple.compilers.llvm.clang.hacked\\\')\", \"/xxx/Xcode_10.0.app/Contents/PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer/Library/Xcode/Plug-ins/HackedClang.xcplugin/Contents/Resources/HackedClang.xcspec: error: unable to declare macro for option \\\'CLANG_WARN_IMPLICIT_SIGN_CONVERSION\\\': conflictingMacroDeclarationType(previousType: XCBCore.MacroType.string, name: \\\"CLANG_WARN_IMPLICIT_SIGN_CONVERSION\\\") (while parsing \\\'com.apple.compilers.llvm.clang.hacked\\\')\", \"/xxx/Xcode_10.0.app/Contents/PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer/Library/Xcode/Plug-ins/HackedClang.xcplugin/Contents/Resources/HackedClang.xcspec: error: unable to declare macro for option \\\'GCC_WARN_64_TO_32_BIT_CONVERSION\\\': conflictingMacroDeclarationType(previousType: XCBCore.MacroType.string, name: \\\"GCC_WARN_64_TO_32_BIT_CONVERSION\\\") (while parsing \\\'com.apple.compilers.llvm.clang.hacked\\\')\", \"/xxx/Xcode_10.0.app/Contents/PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer/Library/Xcode/Plug-ins/HackedClang.xcplugin/Contents/Resources/HackedClang.xcspec: error: unable to load \\\':com.apple.compilers.llvm.clang.hacked.analyzer\\\' (unable to load base spec)\", \"/xxx/Xcode_10.0.app/Contents/PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer/Library/Xcode/Plug-ins/HackedClang.xcplugin/Contents/Resources/HackedClang.xcspec: error: unable to load \\\':com.apple.compilers.llvm.clang.hacked.compiler\\\' (unable to load base spec)\"])")

Xcode 9.0打开TestMyPlugin项目,Build Settings -> OTHER_CFLAGS 使用如下命令:

-Xclang -load -Xclang /xxx/llvm_xcode/Debug/lib/MyPlugin.dylib -Xclang -add-plugin -Xclang MyPlugin

解释:

-Xclang -load -Xclang 动态库路径 -Xclang -add-plugin -Xclang 插件名称

修改Xcode 9.0默认的编译器

Hack Xcode 9.0

解压缩XcodeHacking.zip,修改HackedClang.xcplugin/Contents/Resources/HackedClang.xcspec的内容

BuiltinJambaseRuleName = ProcessC;
ExecPath = "/xxx/llvm_build/bin/clang";
UseCPlusPlusCompilerDriverWhenBundlizing = Yes;
CommandOutputParser = "XCSimpleBufferedCommandOutputParser";
SupportsHeadermaps = Yes;

即修改其中的ExecPath为build的clang路径即可。

然后在XcodeHacking目录下执行如下命令:

sudo cp -rf HackedClang.xcplugin /xxx/Xcode_9.0.app/Contents/PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer/Library/Xcode/Plug-ins
sudo cp -f HackedBuildSystem.xcspec /xxx/Xcode_9.0.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Xcode/Specifications
修改测试工程中的编译器配置

Xcode 9.0打开TestMyPlugin项目,Build Settings 搜索 compiler -> Build Options, 修改Compiler for C/C++/Objective-C 为Clang LLVM Trunk。

因为苹果默认的Default compiler(Apple LLVM 9.0)不支持dylib的加载。

build工程会直接报错:

我的第一个clang插件:类名中不能带有下划线

如何编写clang插件?

几个关键的类

DiagnosticsEngine

向编译器报告错误或警告信息。

// CompilerInstance &ci
DiagnosticsEngine &D = ci.getDiagnostics();
SourceLocation loc = decl->getLocation().getLocWithOffset(pos);
D.Report(loc, D.getCustomDiagID(DiagnosticsEngine::Error, "我的第一个clang插件:类名中不能带有下划线"));
D.Report(loc, D.getCustomDiagID(DiagnosticsEngine::Warning, "我的第一个clang插件:这是一条警告"));

FrontendPluginRegistry

注册插件,必须放在cpp文件尾部:

static FrontendPluginRegistry::Add<MyPlugin::MyASTAction>
X("MyPlugin", "The MyPlugin is my first clang-plugin.");

clang在测试项目构建过程中会注册该插件。

-Xclang -load -Xclang 动态库路径 -Xclang -add-plugin -Xclang 插件名称

注意保持一致。

PluginASTAction

clang插件注册后执行的入口。 基于ASTConsumer的AST前端Action抽象基类,其中会使用ASTConsumer。

class MyASTAction: public PluginASTAction {
    public:
        // 一旦clang插件加载后,由clang调用
        unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &ci, StringRef iFile) {
            return unique_ptr<MyASTConsumer> (new MyASTConsumer(ci));
        }
        
        // 自定义参数
        bool ParseArgs(const CompilerInstance &ci, const vector<string> &args) {
            return true;
        }
};

ASTConsumer

用于读取AST的抽象类

class MyASTConsumer: public ASTConsumer {
    private:
        MatchFinder matcher;
        MyHandler handler;

    public:
        MyASTConsumer(CompilerInstance &ci) :handler(ci) {
            matcher.addMatcher(objcInterfaceDecl().bind("ObjCInterfaceDecl"), &handler);
        }

        void HandleTranslationUnit(ASTContext &context) {
            matcher.matchAST(context);
        }
};

ObjCInterfaceDecl为OC中Interface声明,检测到AST中有类的Interface时,即调用指定的handler方法。

MatchFinder

class MyHandler : public MatchFinder::MatchCallback {
    private:
        CompilerInstance &ci;

    public:
        MyHandler(CompilerInstance &ci) :ci(ci) {}

        void run(const MatchFinder::MatchResult &Result) {
            if (const ObjCInterfaceDecl *decl = Result.Nodes.getNodeAs<ObjCInterfaceDecl>("ObjCInterfaceDecl")) {
                size_t pos = decl->getName().find('_');
                if (pos != StringRef::npos) {
                    DiagnosticsEngine &D = ci.getDiagnostics();
                    SourceLocation loc = decl->getLocation().getLocWithOffset(pos);
                    D.Report(loc, D.getCustomDiagID(DiagnosticsEngine::Error, "我的第一个clang插件:类名中不能带有下划线"));
                }
            }
        }
};

获取Interface的节点,获取类名decl->getName(),查找_。 找到即向编译器汇报错误。

RecursiveASTVisitor

遍历搜索整个AST,能够访问每个节点。

clang插件的一些常见用途

如: 类名是否小写字母开头,是否包含特殊字符 属性名称是否规范,关键字是否有误 方法名称是否规范,参数是否规范,方法实现是否超过多少行

参考资料

一些原始链接