Creating and using Clang plugin with Xcode
环境设置
plugin 设置
Xcode 工程设置
warnning
error
Xcode 集成
与 error/warning 的交互
环境设置
需要 llvm/clang,源码
1 2 3 4 5 cd /opt sudo mkdir llvm sudo chown `whoami` llvm cd llvmexport LLVM_HOME=`pwd`
根据本机的 clang 版本来编译,本机3.3.1 为例
1 2 3 4 5 6 7 8 9 git clone -b release_33 https: git clone -b release_33 https: git clone -b release_33 https: git clone -b release_33 https: mkdir llvm_build cd llvm_build cmake ../llvm -DCMAKE_BUILD_TYPE:STRING=Release make -j`sysctl -n hw.logicalcpu`
plugin 设置
创建 plugin 的文件夹
1 2 cd $LLVM_HOME mkdir toy_clang_plugin; cd toy_clang_plugin
基于 plugin 的例子结构如下 ToyClangPlugin.exports CMakeLists.txt 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" ) ;
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 "" )
现在我们能够基于 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 Clang 的 AST 模块提供了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
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) { 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 并不支持插件,所以我们要这么做。
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,而不是替换掉现存的
增加交互
针对 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); } }