链接:https://railsware.com/blog/2014/02/28/creation-and-using-clang-plugin-with-xcode/
如何写一个clang plugin,很不错的文章,记录一下。
This tutorial describes how to create Clang plugin and covers the next things:
- environment setup
- basic plugin setup
- setup Xcode project for plugin development
- warnings reporting
- errors reporting
- Xcode integration
- interactive hints for errors/warnings riddance
tl;dr
Clang Rocks!!!
You can find the plugin here.
Intro
While working on BloodMagic, I realised that it’d be nice to have a tool for checking semantic errors related to BM usage. For example:
in the interface property marked as lazy
, but not defined as @dynamic
in the implementation, or property marked as lazy
,
but class container doesn’t support injections.
I concluded that I need to work with and I need a full-featured parser.
I’ve tried different approaches: flex+bison, libclang,
but ultimately I decided to write a Clang plugin.
Just for testing purposes I’ve started a simple plugin with the following goals:
- use Xcode for development
- integrate ready plugin into Xcode and use it workaday
- plugin should report warnings, errors and propose interactive hints for fixes (via Xcode UI)
Features of the test plugin:
- report warning in case of class’ name starts with lowercase letter
- report error in case of class’ name contains underscore
_
- propose hints for fixes
Environment setup
For plugin development we need llvm/clang, built from source
|
cd /opt
sudo mkdir llvm
sudo chown `whoami` llvm
cd llvm
export LLVM_HOME=`pwd`
|
Current clang
version on my system – 3.3.1, so let’s build respective version:
|
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`
|
Basic plugin setup
Create directory for plugin
|
cd $LLVM_HOME
mkdir toy_clang_plugin; cd toy_clang_plugin
|
Our plugin based on example from Clang repo and here is it’s structure:
|
ToyClangPlugin.exports
CMakeLists.txt
ToyClangPlugin.cpp
|
We’ll use one source file for simplification
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");
|
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
"")
|
Now we’re able to generate Xcode-project, based on CMakeLists.txt
|
mkdir
build;
cd
build
cmake
-G
Xcode
..
open
ToyClangPlugin.xcodeproj
|
Run ALL_BUILD
target, and you’ll see dynamic library at lib/Debug/ToyCLangPlugin.dylib
.
RecursiveASTVisitor
Clang AST module provides RecursiveASTVisitor, which allows us to traverse via AST.
We just need to create a subclass and implement interesting methods.
For test we’ll print all the found class names.
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;
};
|
Let’s create test source file and check how plugin works.
|
#import <Foundation/Foundation.h>
@interface
ToyObject
: NSObject
@end
@implementation
ToyObject
@end
|
Rebuild plugin and run
|
/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
|
We’ll see a huge list of classes.
Report warnings
Let’s report warning in case if class’ name starts with lowercase letter
Add ASTContext
to ToyClassVisitor
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());
}
//
...
|
Add check
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);
}
}
|
Add some class with “bad” name
|
@interface
bad_ToyObject
: NSObject
@end
@implementation
bad_ToyObject
@end
|
rebuild and run
|
/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.
|
Report error
Let’s generate error in case of class’ name contains _
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;
}
|
Output after running
|
/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.
|
Uncomment first check checkForLowercasedName
and you’ll see both error and warning in the output
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 integration
Unfortunately, system (under ‘system’ I mean Xcode’s clang) clang doesn’t support plugins, so we need to hack Xcode a bit, to allow using of custom compiler.
Unzip this archive and run following commands:
|
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
|
This will enable custom compiler for Xcode.
Reopen Xcode and you’ll see new compiler:

Create new project and select newly added custom clang in Build settings
To enable plugin add following parameters to the OTHER_CFLAGS
section
|
-Xclang
-load
-Xclang
/opt/llvm/toy_clang_plugin/build/lib/Debug/ToyClangPlugin.dylib
-Xclang
-add-plugin
-Xclang
ToyClangPlugin
|

Note, that we use -add-plugin
here, because we want to add our ASTAction
, not to replace the existing
Also, you should disable modules for this target/build

Add test.m
to this project, or create new one, with class names that match plugin criteria
After build you’ll see error and warnings in a more familiar form

Interactive hints
It’s time to add FixItHints
for both warning and error
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);
}
}
|
Rebuild plugin and try to build test project


Conclusion
As you can see, creating a clang plugin is relatively simple, but it needs some dirty hacks with Xcode, and you should use custom built clang, so I’d not recommend you to use this clang for building apps for production usage. Apple provides patched
version, and we can’t know the difference between them. Also, it needs a lot of efforts to make it work, which doesn’t make it widely usable.
Another issue you might face is unstable API, cause it uses internal API which changes continuously.
You still can use it on your system for different diagnostic purposes, but please do not force other people to depend on such heavyweight things.
If you have any comments, questions or suggestions feel free to ask me on twitter, open issue on GitHub,
or just leave a comment here.
Happy hacking!