流霞地

使用Clang插件

最近在把一个功能模块从一个独立的项目迁入另一个大型项目中,要求这个功能模块只在某一业务的页面上开启。 首先想到的方法是判断一个页面的类是否属于该业务。对于dynamic framework可以通过NSBundle方法判断

+ (NSBundle *)bundleForClass:(Class)aClass;

对于static链接的framework或者library无法使用这种方式。 只能换一种思路,通过正则搜索代码里哪些类继承自UIViewController,但是对于继承UIViewController的子类,这种手工搜索方式就显得捉襟见肘了。 然后想到能不能在编译的时候,去获得UIViewController的子类名称。这时候Clang插件就派上用场了。

编译Clang插件

首先要下载Clang的代码:

git clone -b release_80 https://github.com/llvm-mirror/llvm.git
git clone -b release_80 https://github.com/llvm-mirror/clang.git llvm/tools/clang
git clone -b release_80 https://github.com/llvm-mirror/compiler-rt.git llvm/projects/compiler-rt
git clone -b release_80 https://github.com/llvm-mirror/clang-tools-extra.git llvm/tools/clang/extra

插件

在 llvm/tools/clang/examples/下增加一个文件夹,新建CPlugin.cpp

#include <iostream>
#include <set>
#include "clang/AST/AST.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/FrontendPluginRegistry.h"

using namespace clang;
using namespace std;
using namespace llvm;

set<string> classNames;

namespace CPlugin {
	class MyASTVisitor: public RecursiveASTVisitor <MyASTVisitor> {
	private:
		CompilerInstance &instance;
		ASTContext *context;
	public:
		MyASTVisitor(CompilerInstance &inst): instance(inst) {}
		void setContext(ASTContext &context) {
			this->context = &context;
		}
		
		bool VisitDecl(Decl *decl) {
			string filename = instance.getSourceManager().getFilename(decl->getSourceRange().getBegin()).str();
			if(filename.find("/Applications/Xcode.app/") == 0) return true;
			if (isa<ObjCInterfaceDecl>(decl)) {
				ObjCInterfaceDecl *interDecl = (ObjCInterfaceDecl *)decl;
				if (interDecl->getSuperClass()) {
					string interName = interDecl->getNameAsString();
					string superClassName = interDecl->getSuperClass()->getNameAsString();
					if (superClassName.compare("UIViewController") == 0 || classNames.find(superClassName) != classNames.end()) {
						if (classNames.find(interName) == classNames.end()) {
							classNames.insert(interName);
							cout << "-------- ClassName:" << interName << endl;
						}
					}
				}
			}
			
			return true;
		}
	};
	
	class MyASTConsumer: public ASTConsumer {
	private:
		MyASTVisitor visitor;
		void HandleTranslationUnit(ASTContext &context) {
			visitor.setContext(context);
			visitor.TraverseDecl(context.getTranslationUnitDecl());
		}
	public:
		MyASTConsumer(CompilerInstance &inst): visitor(MyASTVisitor(inst)) {}
	};
	class MyASTAction: public PluginASTAction {
	public:
		unique_ptr <ASTConsumer> CreateASTConsumer(CompilerInstance & Compiler, StringRef InFile) {
			return unique_ptr <MyASTConsumer> (new MyASTConsumer(Compiler));
		}
		bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string>& args) {
			return true;
		}
	};
}
static clang::FrontendPluginRegistry::Add<CPlugin::MyASTAction> X("CPlugin", "CPlugin desc");

新建CMakeLists.txt

add_llvm_library(CPlugin MODULE CPlugin.cpp PLUGIN_TOOL clang)

if(LLVM_ENABLE_PLUGINS AND (WIN32 OR CYGWIN))
  target_link_libraries(CPlugin PRIVATE
    clangAST
    clangBasic
    clangFrontend
    clangLex
    LLVMSupport
    )
endif()

修改examples/CMakeLists.txt,在文件的末尾增加add_subdirectory(CPlugin)

编译

需要先安装cmake

brew install cmake

编译方法

mkdir llvm/build
cd llvm/build
cmake -G Xcode ../ -DCMAKE_BUILD_TYPE:STRING=MinSizeRel

等待结束后在llvm/build目录下找到LLVM.xcodeproj,使用Xcode打开后编译clang,libclang和CPlugin三个target。

运行

打开目标project,在Build Settings里Add User-Defined Setting增加

CC=/path/to/llvm/build/Debug/bin/clang
CXX=/path/to/llvm/build/Debug/bin/clang++

在Build Settings里的Other C Flags增加

-Xclang -load -Xclang /path/to/llvm/build/Debug/lib/CPlugin.dylib -Xclang -add-plugin -Xclang CPlugin

如果遇到错误

clang: error: cannot specify -o when generating multiple output files

关掉Build Settings里的Enable Index-While-Building Functionality

收工

经过一系列的操作,就能得到所有UIViewController的子类类名了。 PS:可以优化类名的写操作,直接写到项目的某个文件里,这样就无需在build info里查找了。