使用clang编写XCode代码检测插件
一、项目配置
二、代码编写
-
RecursiveASTVisitor
3.1 编译工程
3.2 编译JRVisitor
3.3 运行JRVisitor
3.4 测试OC源码
3.5 报错
三、插件化配置
四、补充内容:MatchFinder和CXCursor
一、项目配置
1.下载llvm源码
终端输入以下命令:
sudo mkdir llvm
sudo chown `whoami` llvm
cd llvm
export LLVM_HOME=`pwd`
git clone -b release_60 https://github.com/llvm-mirror/llvm.git llvm
git clone -b release_60 https://github.com/llvm-mirror/clang.git llvm/tools/clang
git clone -b release_60 https://github.com/llvm-mirror/clang-tools-extra.git llvm/tools/clang/tools/extra
git clone -b release_60 https://github.com/llvm-mirror/compiler-rt.git llvm/projects/compiler-rt
目前最新版本是release_90,这里采用和网上文章同样的release_60,避免出现其他配置错误(不影响最终效果)。

2.安装CMake
cmake是跨平台的编译工具,使用简单的语句来描述所有平台的安装(编译)过程。
brew update
brew install cmake
2.1 配置CMAKE
在编写插件之前,需要结合CMake来描述编译过程,添加我们自己的源码。
首先进入工作目录:
cd llvm/llvm/tools/clang/examples
在该目录下,已经包含了官方提供的几个示例,如PrintFunctionNames等。
在该目录下,需要修改CMakeLists.txt文件:
if(NOT CLANG_BUILD_EXAMPLES)
set_property(DIRECTORY PROPERTY EXCLUDE_FROM_ALL ON)
set(EXCLUDE_FROM_ALL ON)
endif()
if(CLANG_ENABLE_STATIC_ANALYZER)
add_subdirectory(analyzer-plugin)
endif()
add_subdirectory(clang-interpreter)
add_subdirectory(PrintFunctionNames)
add_subdirectory(AnnotateFunctions)
# 新添加的自定义插件代码目录名称
add_subdirectory(JRVisitor)
我们在最后添加了add_subdirectory(JRVisitor)
,这表示在编译的时候,会包含当前目录下的JRVisitor
目录文件,该目录就是我们编写插件代码的地方。
同样的,我们需要在当前目录下创建JRVisitor
目录:
mkdir JRVisitor && cd JRVisitor
2.2 Loadable modules和Clang executables的区别
有了上面的JRVisitor
目录,接下来我们将在该目录下编写插件代码。
首先,我们创建源文件:
touch JRVisitor.cpp
JRVisitor.cpp
文件就是编写插件代码的文件。此时它里面是空的,没有任何代码。
其次,我们需要配置CMakeLists.txt文件,告诉编译JRVisitor.cpp
文件时,需要有哪些依赖,输出哪种格式的文件:
touch CMakeLists.txt
新创建的CMakeLists.txt
中的内容为:
set(LLVM_LINK_COMPONENTS support)
add_clang_executable(JRVisitor
JRVisitor.cpp
)
target_link_libraries(JRVisitor
PRIVATE
clangTooling
libclang
)
-
add_clang_executable
我们把注意力放在add_clang_executable
这里,这里表示将JRVisitor.cpp
的源文件编译成目标文件JRVisitor
,并且这个目标文件是可执行的。如此,我们即可在编写插件代码的时候动态的调试我们的程序。 -
add_llvm_loadable_module
如果我们将add_clang_executable
修改为add_llvm_loadable_module
,则最后生成的目标文件为.dylib
。这是我们最后需要的插件的格式。
我们这里先采用 add_clang_executable
的方式,方便我们编写代码并调试。
二、代码编写
3. RecursiveASTVisitor
源代码会被转化为抽象语法树(AST),我们需要对抽象语法树进行访问,找到我们需要检验的节点,这是我们编写插件主要做的事情。
RecursiveASTVisitor
为访问抽象语法树提供了一系列的回调方法,我们可以在需要的方法中进行处理。
为了使用RecursiveASTVisitor
,我们需要准备以下模版代码:
#include "clang/Tooling/CommonOptionsParser.h"
#include "clang/Tooling/Tooling.h"
#include "clang/Frontend/FrontendActions.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Frontend/CompilerInstance.h"
using namespace clang::tooling;
using namespace llvm;
using namespace clang;
// Apply a custom category to all command-line options so that they are the
// only ones displayed.
static llvm::cl::OptionCategory MyToolCategory("my-tool options");
// CommonOptionsParser declares HelpMessage with a description of the common
// command-line options related to the compilation database and input files.
// It's nice to have this help message in all tools.
static cl::extrahelp CommonHelp(CommonOptionsParser::HelpMessage);
// A help message for this specific tool can be added afterwards.
static cl::extrahelp MoreHelp("\nMore help text...\n");
/*
我们在CFunctionCalledVisitor中做逻辑处理
*/
class CFunctionCalledVisitor : public RecursiveASTVisitor<CFunctionCalledVisitor> {
public:
explicit CFunctionCalledVisitor(ASTContext *Context, std::string fName)
: Context(Context), fName(fName) {}
bool VisitCallExpr(CallExpr *CallExpression) {
QualType q = CallExpression->getType();
const clang::Type *t = q.getTypePtrOrNull();
if (t != NULL) {
FunctionDecl *func = CallExpression->getDirectCallee();
const std::string funcName = func->getNameInfo().getAsString();
if (fName == funcName) {
/*
如果你的llvm版本是release_90版本,则需要将CallExpression->getLocStart()修改为CallExpression->getBeginLoc()
*/
FullSourceLoc FullLocation =
Context->getFullLoc(CallExpression->getLocStart());
if (FullLocation.isValid())
llvm::outs() << "Found call at "
<< FullLocation.getSpellingLineNumber() << ":"
<< FullLocation.getSpellingColumnNumber() << "\n";
}
}
return true;
}
private:
ASTContext *Context;
std::string fName;
};
class CFunctionCalledConsumer : public clang::ASTConsumer {
public:
explicit CFunctionCalledConsumer(CompilerInstance &Instance, ASTContext *Context, std::string fName)
: Instance(Instance), Visitor(Context, fName) {}
bool HandleTopLevelDecl(DeclGroupRef DG) override {
return true;
}
void HandleTranslationUnit(clang::ASTContext &Context) override {
Visitor.TraverseDecl(Context.getTranslationUnitDecl());
}
private:
CompilerInstance &Instance;
CFunctionCalledVisitor Visitor;
};
/*
*/
class CFunctionCalledAction : public clang::ASTFrontendAction {
public:
CFunctionCalledAction() {}
virtual std::unique_ptr<clang::ASTConsumer> CreateASTConsumer(
clang::CompilerInstance &Compiler, llvm::StringRef InFile) {
const std::string fName = "foo";
return std::unique_ptr<clang::ASTConsumer>(
new CFunctionCalledConsumer(Compiler, &Compiler.getASTContext(), fName));
}
};
int main(int argc, const char **argv) {
CommonOptionsParser OptionsParser(argc, argv, MyToolCategory);
// OptionsParser.getSourcePathList()获取要解析的源文件,目前是test.cpp
const std::vector<std::string> vec = OptionsParser.getSourcePathList();
for (size_t i = 0; i < vec.size(); ++i) {
string str = vec[i];
printf("filepath:%s\n",str.c_str());
}
ClangTool Tool(OptionsParser.getCompilations(),
OptionsParser.getSourcePathList());
return Tool.run(newFrontendActionFactory<CFunctionCalledAction>().get());
}
ASTFrontendAction
是编译前段操作的入口类,这里我们继承自它来定义CFunctionCalledAction
,我们自己的前端操作类主要目的是用来查找函数foo
的调用的地方。
ASTConsumer
是处理AST的地方。
- 其中
HandleTopLevelDecl
当每次遇到一个顶层定义时都会回调,询问我们是否继续。这里我们返回YES,当然,对于不关心的节点,我们可以直接返回NO。
// test.cpp
1 void foo() {
2
3 }
4
5 class Y {
6 public:
7 void foo() {
8
9 }
10 };
11
12 int main() {
13 foo();
14 return 1;
15 }
比如对于test.cpp
来说,HandleTopLevelDecl
会执行三次,分别为foo
,Y
,main
。
-
HandleTranslationUnit
是获取AST上下文的函数,这里,我们使用RecursiveASTVisitor
的实例来读取AST的信息。
CFunctionCalledVisitor
是我们处理每个节点的地方,现在,我们只定义了VisitCallExpr
函数来捕获所有的调用表达式。
在VisitCallExpr
方法中,我们首先判断函数调用的类型是否存在?如果存在,获取函数调用的名称,判断是否等于foo
,如果相等,则输出函数调用所在的行数。
现在,尝试编译下整个llvm项目:
3.1 编译工程
cd /User/LQ/llvm/llvm_build
cmake -G Xcode -DCMAKE_BUILD_TYPE:STRING=Release ../llvm
等待编译结束后,应该可以在llvm_build看到编译出的xcode工程LLVM.xcodeproj
。
3.2 编译JRVisitor
双击打开工程,会弹出如下提示,选择Automatically Create Schemes
。

选择
JRVisitor
的target,command+B
进行编译,编译完成后,即可动态调试。
注意:后续修改代码,直接在项目中修改即可。修改完成
command+B
重新编译即可,不需要再使用cmake编译llvm的源码。
3.3 运行JRVisitor
运行有两种方式:
- 使用命令行工具,需要提前编译出可执行文件,使用终端进入目录后执行
./JRVisitor /Users/58liqiang/llvm/test.cpp --
;
$ cd /Users/58liqiang/llvm_build/Debug/bin
$ ./JRVisitor /Users/58liqiang/llvm/test.cpp --
- XCode中设置
JRVisitor
target的Scheme,在Arguments
中添加两个参数/Users/58liqiang/llvm/test.cpp
和--
。
1018593-876ac7b35375b531.png
不出意外的话,我们应该可以看到如下结果:
Found call at 13 : 5
即表明foo
方法的调用在test.cpp
文件的第13行,第5列。
- 除了
VisitCallExpr
方法外,clang提供了其他的OC专属的回调接口,比如bool VisitObjCMethodDecl(ObjCMethodDecl *declaration)
方法是找到OC方法时的回调;bool VisitObjCInterfaceDecl(ObjCInterfaceDecl *declaration)
则是找到OC类的定义时的回调,还有其他各种关于OC的方法回调。- 参数
--
表示不使用任何编译数据库。编译数据库是json
结构的数据,包含了一定的编译规则。
3.4 测试OC源码
上面的插件代码,在源文件为OC代码的时候,是无法成功的(尤其导入了<Foundation/Foundation.h>
),需要对各项配置进行修改。
比如我们现在添加两个文件Person.h
和Person.m
。
.h文件
#import <Foundation/Foundation.h>
@interface person : NSObject
@property (nonatomic,copy)NSString *MyName;
@end
.m文件
#import "Person.h"
@implementation person
void foo() {
}
- (void)MyTestFunc {
foo();
}
@end
将Schemes
中的第一个参数修改为Person.m
文件路径,然后运行项目,报错结果为下:
/Users/58liqiang/llvm/Person.h:1:9: fatal error: 'Foundation/Foundation.h' file not found
#import <Foundation/Foundation.h>
^~~~~~~~~~~~~~~~~~~~~~~~~
为了使Foundation框架能被正常找到,我们需要在Scheme
中添加如下参数:

之所以需要添加-extra-arg
来分割增加的两个参数,是因为我们的这个命令行工具就是通过-extra-arg来追加参数的,你可以通过终端执行$ ./JRVisitor -help
来查看具体信息。
bogon:~ 58liqiang$ cd /Users/58liqiang/llvm/llvm_build/Debug/bin/
bogon:bin 58liqiang$ ./JRVisitor -help
USAGE: JRVisitor [options] <source0> [... <sourceN>]
OPTIONS:
Generic Options:
-help - Display available options (-help-hidden for more)
-help-list - Display list of available options (-help-list-hidden for more)
-version - Display the version of this program
my-tool options:
-extra-arg=<string> - Additional argument to append to the compiler command line
-extra-arg-before=<string> - Additional argument to prepend to the compiler command line
-p=<string> - Build path
-p <build-path> is used to read a compile command database.
For example, it can be a CMake build directory in which a file named
compile_commands.json exists (use -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
CMake option to get this output). When no build path is specified,
a search for compile_commands.json will be attempted through all
parent paths of the first input file . See:
http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html for an
example of setting up Clang Tooling on a source tree.
<source0> ... specify the paths of source files. These paths are
looked up in the compile command database. If the path of a file is
absolute, it needs to point into CMake's source tree. If the path is
relative, the current working directory needs to be in the CMake
source tree and the file must be in a subdirectory of the current
working directory. "./" prefixes in the relative files will be
automatically removed, but the rest of a relative path must be a
suffix of a path in the compile command database.
More help text...
bogon:bin 58liqiang$
注意
:SDK的路径必须是你自己的路径,我的路径是/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk
然后,修改我们JRVisitor.cpp
中CFunctionCalledVisitor
方法,增加以下代码:
bool VisitObjCMethodDecl(ObjCMethodDecl *declaration)
{
const std::string name = declaration->getNameAsString();
llvm::outs() << "Found OC method " << name << "\n";
return true;
}
bool VisitObjCInterfaceDecl(ObjCInterfaceDecl *declaration)
{
const std::string name = declaration->getNameAsString();
llvm::outs() << "Found OC Interface " << name << "\n";
return true;
}
重新运行运行项目,不出意外的话,应该可以在控制台看到下面的结果:
filepath:/Users/58liqiang/llvm/Person.m
warning: using sysroot for 'iPhoneSimulator' but targeting 'MacOSX' [-Wincompatible-sysroot]
Found OC Interface NSArray
Found OC Interface NSAttributedString
Found OC Interface NSString
Found OC Interface NSNull
Found OC Interface NSCharacterSet
...
Found OC method initWithUUIDString:
Found OC method initWithUUIDBytes:
Found OC method getUUIDBytes:
Found OC Interface person
Found OC method MyTestFunc
Found call at 10:5
Program ended with exit code: 0
根据结果,我们找到了person
类的定义,找到了MyTestFunc
的OC方法,找到了foo
的函数调用处。
3.5 报错
在以上的示例中,我们的person
类是首字母小写的,需要对此作出提示。
一个提示的方法是使用printf
在控制台输出错误。
更好的方法是使用编译器的诊断引擎DiagnosticsEngine
,如此,在以后我们将程序编译成插件的时候,就可以使用XCode的诊断引擎了,这会直接在源码中进行错误提示。
需要考虑两点:
- 源文件是否是用户代码(非系统代码)
- 获取诊断引擎
DiagnosticsEngine
判断是否是用户代码,对于我们的例子来说,import <Foundation/Foundation.h>
应该被忽略。在CFunctionCalledVisitor
中添加如下代码:
bool isUserSourceCode (Decl *decl)
{
std::string filename = Instance.getSourceManager().getFilename(decl->getSourceRange().getBegin()).str();
if (filename.empty()) return false;
//非XCode中的源码都认为是用户源码
if(filename.find("/Applications/Xcode.app/") != filename.npos) return false;
return true;
}
诊断引擎,需要从编译器实例CompilerInstance
中获取。我们修改bool VisitObjCInterfaceDecl(ObjCInterfaceDecl *declaration)
方法:
bool VisitObjCInterfaceDecl(ObjCInterfaceDecl *declaration)
{
if (!isUserSourceCode(declaration)) return true;
const std::string name = declaration->getNameAsString();
const char *cname = name.c_str();
if (cname[0] < 'A' || cname[0] > 'Z') {
DiagnosticsEngine &D = Instance.getDiagnostics();
int diagID = D.getCustomDiagID(DiagnosticsEngine::Error, "%0");
SourceLocation location = declaration->getLocation();
D.Report(location, diagID) << "类名应该大写";
}
return true;
}
现在重新运行程序,应该可以在控制台中看到以下结果:
...
In file included from /Users/58liqiang/llvm/Person.m:1:
/Users/58liqiang/llvm/Person.h:3:12: error: 类名应该大写
@interface person : NSObject
^
Found OC method MyTestFunc
Found call at 10:5
1 error generated.
Error while processing /Users/58liqiang/llvm/Person.m.
Program ended with exit code: 1
4.插件化
以上代码编译为可执行文件,方便动态调试。如果我们需要编译成.dylib
,则需要修改部分配置。
4.1. 修改CMakeLists文件
cd /User/58liqiang/llvm/llvm/tools/clang/examples/JRVisitor
CMakeLists内容替换为:
if( NOT MSVC ) # MSVC mangles symbols differently
if( NOT LLVM_REQUIRES_RTTI )
if( NOT LLVM_REQUIRES_EH )
set(LLVM_EXPORTED_SYMBOL_FILE ${CMAKE_CURRENT_SOURCE_DIR}/JRVisitor.exports)
endif()
endif()
endif()
add_llvm_loadable_module(JRVisitor
JRVisitor.cpp
)
if(LLVM_ENABLE_PLUGINS AND (WIN32 OR CYGWIN))
target_link_libraries(JRVisitor
PRIVATE
clangAST
clangBasic
clangFrontend
LLVMSupport
)
endif()
4.2. 修改JRVisitor.cpp文件
主要修改三个部分:
- 头文件修改为以下(为防止干扰,删除不必要的头文件):
#include "clang/Frontend/FrontendPluginRegistry.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Frontend/CompilerInstance.h"
using namespace clang;
// 删除下面三句代码
//static llvm::cl::OptionCategory MyToolCategory("my-tool options");
//static cl::extrahelp CommonHelp(CommonOptionsParser::HelpMessage);
//static cl::extrahelp MoreHelp("\nMore help text...\n");
- 删除main函数,并且添加
static clang::FrontendPluginRegistry::Add<CFunctionCalledAction> X("JRVisitor", "use visitor");
- 修改
CFunctionCalledAction
类如下:
class CFunctionCalledAction : public clang::PluginASTAction {
public:
CFunctionCalledAction() {}
virtual std::unique_ptr<clang::ASTConsumer> CreateASTConsumer(
clang::CompilerInstance &Compiler, llvm::StringRef InFile) override {
const std::string fName = "foo";
return llvm::make_unique<CFunctionCalledConsumer>(Compiler,
&Compiler.getASTContext(),
fName);
}
bool ParseArgs(const CompilerInstance &CI,
const std::vector<std::string> &args) override
{
return true;
}
};
/*
删除main函数
int main(int argc, const char **argv) {
...
}
*/
static clang::FrontendPluginRegistry::Add<CFunctionCalledAction>
X("JRVisitor", "use visitor");
4.3. 重新编译生成XCode项目
编译之前,需要删除现有的编译项目,删除llvm_build
。然后执行下面操作:
$ cd /Users/58liqiang/llvm
$ mkdir llvm_build && cd llvm_build
$ cmake -G Xcode -DCMAKE_BUILD_TYPE:STRING=Release ../llvm
成功后,同上面一样,打开LLVM.xcodeproj
,编译JRVisitor
和clang
。直到完成,整个插件化的工作就做完了。
5. 配置XCode工程
新建xcode项目。
5.1 配置build setting
中的Other C Flags
为-Xclang -load -Xclang /Users/58liqiang/Desktop/JRvisitor.dylib -Xclang -add-plugin -Xclang JRVisitor
。
在4.3步骤中编译生成的
JRVisitor.dylib
默认在llvm_build/Debug/lib
目录下,这里我把它拷贝到了桌面,因此你需要根据自己的实际情况填写这里的路径。
5.2 build setting
中增加两个自定义键值对:

分别为CC
和CXX
,其中CC
的值是4.3步骤中编译出的clang
可执行文件,默认在llvm_build/Debug/bin
中,CXX
的值是clang++
可执行文件,默认在llvm_build/Debug/bin
中。

使用
CXX
或者CPLUSPLUS
都可以。CC
标记C编译器,CXX
标记C++编译器。类似的还有LD
、LDPLUSPLUS
、LIBTOOL
,分别为使用指定路径的C链接器、C++链接器、libtool。
5.3 build setting
中设置Enable Index-While-Building Functionality
为NO
6. 运行插件
现在,在新建的项目中增加自定义的person
类,然后编译,会发现提示了我们自定义的错误:

修改插件代码不需要重新编译生成xcode项目,仅仅更新插件即可。
四、补充内容:MatchFinder和CXCursor
- MatchFinder
除了使用RecursiveASTVisitor
访问抽象语法树外,还可以使用MatchFinder
来进行访问。它采用DSL语法。 - CXCursor
根据节点进行遍历访问。
未完待续...