Clang plugin with Xcode

Creating and using Clang plugin with Xcode

  • 环境设置
  • plugin 设置
  • Xcode 工程设置
  • warnning
  • error
  • Xcode 集成
  • 与 error/warning 的交互

环境设置

  1. 需要 llvm/clang,源码
1
2
3
4
5
cd /opt
sudo mkdir llvm
sudo chown `whoami` llvm
cd llvm
export LLVM_HOME=`pwd`
  1. 根据本机的 clang 版本来编译,本机3.3.1 为例
1
2
3
4
5
6
7
8
9
git clone -b release_33 https://github.com/llvm-mirror/llvm.git llvm
git clone -b release_33 https://github.com/llvm-mirror/clang.git llvm/tools/clang
git clone -b release_33 https://github.com/llvm-mirror/clang-tools-extra.git llvm/tools/clang/tools/extra
git clone -b release_33 https://github.com/llvm-mirror/compiler-rt.git llvm/projects/compiler-rt

mkdir llvm_build
cd llvm_build
cmake ../llvm -DCMAKE_BUILD_TYPE:STRING=Release
make -j`sysctl -n hw.logicalcpu`

plugin 设置

  1. 创建 plugin 的文件夹
1
2
cd $LLVM_HOME
mkdir toy_clang_plugin; cd toy_clang_plugin

基于 plugin 的例子结构如下
ToyClangPlugin.exports
CMakeLists.txt
ToyClangPlugin.cpp

  1. 文件内容
  • ToyClangPlugin.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include "clang/Frontend/FrontendPluginRegistry.h"
#include "clang/AST/AST.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/Frontend/CompilerInstance.h"

using namespace clang;

namespace
{
class ToyConsumer : public ASTConsumer
{
};

class ToyASTAction : public PluginASTAction
{
public:
virtual clang::ASTConsumer *CreateASTConsumer(CompilerInstance &Compiler,
llvm::StringRef InFile)
{
return new ToyConsumer;
}

bool ParseArgs(const CompilerInstance &CI, const
std::vector<std::string>& args) {
return true;
}
};
}

static clang::FrontendPluginRegistry::Add<ToyASTAction>
X("ToyClangPlugin", "Toy Clang Plugin");
  • CMakeLists.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
cmake_minimum_required (VERSION 2.6)
project (ToyClangPlugin)

set( CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin )
set( CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib )
set( CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib )

set( LLVM_HOME /opt/llvm )
set( LLVM_SRC_DIR ${LLVM_HOME}/llvm )
set( CLANG_SRC_DIR ${LLVM_HOME}/llvm/tools/clang )
set( LLVM_BUILD_DIR ${LLVM_HOME}/llvm_build )
set( CLANG_BUILD_DIR ${LLVM_HOME}/llvm_build/tools/clang)

add_definitions (-D__STDC_LIMIT_MACROS -D__STDC_CONSTANT_MACROS)
add_definitions (-D_GNU_SOURCE -DHAVE_CLANG_CONFIG_H)

set (CMAKE_CXX_COMPILER "${LLVM_BUILD_DIR}/bin/clang++")
set (CMAKE_CC_COMPILER "${LLVM_BUILD_DIR}/bin/clang")

set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}
-fPIC
-fno-common
-Woverloaded-virtual
-Wcast-qual
-fno-strict-aliasing
-pedantic
-Wno-long-long
-Wall
-Wno-unused-parameter
-Wwrite-strings
-fno-exceptions
-fno-rtti")

set (CMAKE_MODULE_LINKER_FLAGS "-Wl,-flat_namespace -Wl,-undefined -Wl,suppress")

set (LLVM_LIBS
LLVMJIT
LLVMX86CodeGen
LLVMX86AsmParser
LLVMX86Disassembler
LLVMExecutionEngine
LLVMAsmPrinter
LLVMSelectionDAG
LLVMX86AsmPrinter
LLVMX86Info
LLVMMCParser
LLVMCodeGen
LLVMX86Utils
LLVMScalarOpts
LLVMInstCombine
LLVMTransformUtils
LLVMipa
LLVMAnalysis
LLVMTarget
LLVMCore
LLVMMC
LLVMSupport
LLVMBitReader
LLVMOption
)

macro(add_clang_plugin name)
set (srcs ${ARGN})

include_directories( "${LLVM_SRC_DIR}/include"
"${CLANG_SRC_DIR}/include"
"${LLVM_BUILD_DIR}/include"
"${CLANG_BUILD_DIR}/include" )
link_directories( "${LLVM_BUILD_DIR}/lib" )

add_library( ${name} SHARED ${srcs} )

if (SYMBOL_FILE)
set_target_properties( ${name} PROPERTIES LINK_FlAGS
"-exported_symbols_list ${SYMBOL_FILE}")
endif()

foreach (clang_lib ${CLANG_LIBS})
target_link_libraries( ${name} ${clang_lib} )
endforeach()

foreach (llvm_lib ${LLVM_LIBS})
target_link_libraries( ${name} ${llvm_lib} )
endforeach()

foreach (user_lib ${USER_LIBS})
target_link_libraries( ${name} ${user_lib} )
endforeach()

endmacro(add_clang_plugin)

set(SYMBOL_FILE ToyClangPlugin.exports)

set (CLANG_LIBS
clang
clangFrontend
clangAST
clangAnalysis
clangBasic
clangCodeGen
clangDriver
clangFrontendTool
clangLex
clangParse
clangSema
clangEdit
clangSerialization
clangStaticAnalyzerCheckers
clangStaticAnalyzerCore
clangStaticAnalyzerFrontend
)

set (USER_LIBS
pthread
curses
)

add_clang_plugin(ToyClangPlugin
ToyClangPlugin.cpp
)

set_target_properties(ToyClangPlugin PROPERTIES
LINKER_LANGUAGE CXX
PREFIX "")
  • ToyClangPlugin.exports
1
__ZN4llvm8Registry*

现在我们能够基于 CMakeLists.txt 生成 Xcode-project,

1
2
3
mkdir build; cd build
cmake -G Xcode ..
open ToyClangPlugin.xcodeproj

运行 ALL_BUILD target ,然后就能看到动态库:lib/Debug/ToyClangPlugin.dylib

RecuresiveASTVisitor

ClangAST 模块提供了RecuresiveASTVisitor,他能够让你编译整个 AST,我们只需要创建一个子类并且实现我们感兴趣的方法就行。为了测试一下,现在只是打印所有发现的类名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class ToyClassVisitor : public RecursiveASTVisitor<ToyClassVisitor>
{
public:
bool VisitObjCInterfaceDecl(ObjCInterfaceDecl *declaration)
{
printf("ObjClass: %s\n", declaration->getNameAsString().c_str());
return true;
}
};

class ToyConsumer : public ASTConsumer
{
public:
void HandleTranslationUnit(ASTContext &context) {
visitor.TraverseDecl(context.getTranslationUnitDecl());
}
private:
ToyClassVisitor visitor;
};

下面我们就来创建测试的文件并且看看 plugin 是如何工作的。

1
2
3
4
5
6
7
8
9
#import <Foundation/Foundation.h>

@interface ToyObject : NSObject

@end

@implementation ToyObject

@end

rebuild & 运行 插件

1
2
3
4
5
/opt/llvm/toy_clang_plugin/build $ $LLVM_HOME/llvm_build/bin/clang ../test.m \
-Xclang -load \
-Xclang lib/Debug/ToyClangPlugin.dylib \
-Xclang -plugin \
-Xclang ToyClangPlugin

报警告

下面我们试试这个情况:类名是以小写字母开始。

  • ToyClassVisitor 中增加 ASTContext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ToyClassVisitor : public RecursiveASTVisitor<ToyClassVisitor>
{
private:
ASTContext *context;
public:
void setContext(ASTContext &context)
{
this->context = &context;
}
// ...
};

// ...
void HandleTranslationUnit(ASTContext &context) {
visitor.setContext(context);
visitor.TraverseDecl(context.getTranslationUnitDecl());
}
// ...
  • 增加检测
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bool VisitObjCInterfaceDecl(ObjCInterfaceDecl *declaration)
{
checkForLowercasedName(declaration);
return true;
}
// ...
void checkForLowercasedName(ObjCInterfaceDecl *declaration)
{
StringRef name = declaration->getName();
char c = name[0];
if (isLowercase(c)) {
DiagnosticsEngine &diagEngine = context->getDiagnostics();
unsigned diagID = diagEngine.getCustomDiagID(DiagnosticsEngine::Warning, "Class name should not start with lowercase letter");
SourceLocation location = declaration->getLocation();
diagEngine.Report(location, diagID);
}
}
  • 测一下
1
2
3
4
5
6
7
@interface bad_ToyObject : NSObject

@end

@implementation bad_ToyObject

@end
  • rebuild & run
1
2
3
4
5
6
7
8
9
10
/opt/llvm/toy_clang_plugin/build $ $LLVM_HOME/llvm_build/bin/clang ../test.m \
-Xclang -load \
-Xclang lib/Debug/ToyClangPlugin.dylib \
-Xclang -plugin \
-Xclang ToyClangPlugin

../test.m:11:12: warning: Class name should not start with lowercase letter
@interface bad_ToyObject : NSObject
^
1 warning generated.

报错误

  • 试一下如果类名里面包含下划线
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void checkForUnderscoreInName(ObjCInterfaceDecl *declaration)
{
size_t underscorePos = declaration->getName().find('_');
if (underscorePos != StringRef::npos) {
DiagnosticsEngine &diagEngine = context->getDiagnostics();
unsigned diagID = diagEngine.getCustomDiagID(DiagnosticsEngine::Error, "Class name with `_` forbidden");
SourceLocation location = declaration->getLocation().getLocWithOffset(underscorePos);
diagEngine.Report(location, diagID);
}
}

bool VisitObjCInterfaceDecl(ObjCInterfaceDecl *declaration)
{
// disable this check temporary
// checkForLowercasedName(declaration);
checkForUnderscoreInName(declaration);
return true;
}
  • 看看运行后的输出
1
2
3
4
5
6
7
8
9
10
/opt/llvm/toy_clang_plugin/build $ $LLVM_HOME/llvm_build/bin/clang ../test.m \
-Xclang -load \
-Xclang lib/Debug/ToyClangPlugin.dylib \
-Xclang -plugin \
-Xclang ToyClangPlugin

../test.m:11:15: error: Class name with `_` forbidden
@interface bad_ToyObject : NSObject
^
1 error generated.
  • 我们试试两个监测都打开的时候的样子
1
2
3
4
5
6
7
8
9
10
11
12
13
/opt/llvm/toy_clang_plugin/build $ $LLVM_HOME/llvm_build/bin/clang ../test.m \
-Xclang -load \
-Xclang lib/Debug/ToyClangPlugin.dylib \
-Xclang -plugin \
-Xclang ToyClangPlugin

../test.m:11:12: warning: Class name should not start with lowercase letter
@interface bad_ToyObject : NSObject
^
../test.m:11:15: error: Class name with `_` forbidden
@interface bad_ToyObject : NSObject
^
1 warning and 1 error generated.

Xcode 集成

很遗憾,Xcode 的 clang 并不支持插件,所以我们要这么做。

  • 在 Xcode 中启用自定义编译工具
1
2
sudo mv HackedClang.xcplugin `xcode-select -print-path`/../PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer/Library/Xcode/Plug-ins
sudo mv HackedBuildSystem.xcspec `xcode-select -print-path`/Platforms/iPhoneSimulator.platform/Developer/Library/Xcode/Specifications
  • 这样就能在 build option 里面找到编译器了
  • OTHER_CFLAGS 中增加
1
-Xclang -load -Xclang /opt/llvm/toy_clang_plugin/build/lib/Debug/ToyClangPlugin.dylib -Xclang -add-plugin -Xclang ToyClangPlugin

注意:这个地方使用 -add-plugin,因为我们想用自己的 ASTAction,而不是替换掉现存的

  • 在 Module - CLANG_ENABLE_MODULES 设置为 NO

  • 把之前新建的 test.m 增加到工程中。即可见到

增加交互

  • 针对 warning & error 增加 FixItHints
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
void checkForLowercasedName(ObjCInterfaceDecl *declaration)
{
StringRef name = declaration->getName();
char c = name[0];
if (isLowercase(c)) {
std::string tempName = name;
tempName[0] = toUppercase(c);
StringRef replacement(tempName);

SourceLocation nameStart = declaration->getLocation();
SourceLocation nameEnd = nameStart.getLocWithOffset(name.size());

FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement);

DiagnosticsEngine &diagEngine = context->getDiagnostics();
unsigned diagID = diagEngine.getCustomDiagID(DiagnosticsEngine::Warning, "Class name should not start with lowercase letter");
SourceLocation location = declaration->getLocation();
diagEngine.Report(location, diagID).AddFixItHint(fixItHint);
}
}

void checkForUnderscoreInName(ObjCInterfaceDecl *declaration)
{
StringRef name = declaration->getName();
size_t underscorePos = name.find('_');
if (underscorePos != StringRef::npos) {
std::string tempName = name;
std::string::iterator end_pos = std::remove(tempName.begin(), tempName.end(), '_');
tempName.erase(end_pos, tempName.end());
StringRef replacement(tempName);

SourceLocation nameStart = declaration->getLocation();
SourceLocation nameEnd = nameStart.getLocWithOffset(name.size());

FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement);

DiagnosticsEngine &diagEngine = context->getDiagnostics();
unsigned diagID = diagEngine.getCustomDiagID(DiagnosticsEngine::Error, "Class name with `_` forbidden");
SourceLocation location = declaration->getLocation().getLocWithOffset(underscorePos);
diagEngine.Report(location, diagID).AddFixItHint(fixItHint);
}
}
作者

shouyi.www

发布于

2019-12-04

更新于

2025-01-30

许可协议

评论

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×