XcodeでのClangプラグインの作成と使用

このチュートリアルでは、Clang用のプラグインの作成方法について説明し、次の手順を説明します。







TL; DR


準備ができたプラグインはこちらにあります。



エントリー


BloodMagicの開発中に、BMを使用する際にセマンティックエラーを見つけるためのツールがあると素晴らしいと思いました。 たとえば、インターフェイスではプロパティはlazy



としてマークされますが、実装では@dynamicまたはlazy



としてマークされませんが、コンテナクラスはインジェクションをサポートしません。 ASTを使用する必要があるという結論に達しました。したがって、本格的なパーサーが必要です。



さまざまなオプションを試しました: flex + bisonlibclang 、最終的にはClangのプラグインを書くことにしました。



テストプラグインでは、次の目標を設定しました。





テストプラグインの機能:





環境設定


プラグインを開発するには、ソースからコンパイルされたllvm / clangが必要です



 cd /opt sudo mkdir llvm sudo chown `whoami` llvm cd llvm export LLVM_HOME=`pwd`
      
      





私のマシンのclangの現在のバージョンは3.3.1なので、適切なバージョンを使用します。



 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`
      
      





基本的なプラグインを作成する


プラグインのディレクトリを作成します



 cd $LLVM_HOME mkdir toy_clang_plugin; cd toy_clang_plugin
      
      





プラグインはClangリポジトリのサンプルに基づいており、次の構造を持っています。



 ToyClangPlugin.exports CMakeLists.txt ToyClangPlugin.cpp
      
      





1つのファイルを使用して単純化します。



ToyClangPlugin.cpp
 // ToyClangPlugin.cpp #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
 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
 __ZN4llvm8Registry*
      
      





これで、 `CMakeLists.txt`に基づいてXcodeプロジェクトを生成できます



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





「ALL_BUILD」を実行します。成功した場合、完成したライブラリは「lib / Debug / ToyCLangPlugin.dylib」になります。



RecursiveASTVisitor


ASTモジュールは、構文ツリーを走査できるRecursiveASTVisitorを提供します。 必要なのは、目的のメソッドを継承して実装することだけです。

小さなテストとして、発生したすべてのクラスを表示します。



 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; };
      
      





テストクラスを作成してプラグインをテストする



 #import <Foundation/Foundation.h> @interface ToyObject : NSObject @end @implementation ToyObject @end
      
      





プラグインの起動



 /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
      
      





出力はクラスの膨大なリストになります。



アラート生成


クラス名が小文字で始まる場合、ユーザーに警告が表示されます。

アラートを生成するには、コンテキストが必要です



 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()); } // ...
      
      





クラス名の検証:



 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); } }
      
      





次に、「悪い」名前のクラスを追加する必要があります



 @interface bad_ToyObject : NSObject @end @implementation bad_ToyObject @end
      
      





プラグインを確認します



 /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.
      
      





エラー生成


クラス名にアンダースコア( '_')が含まれている場合、ユーザーにエラーが表示されます。



 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; }
      
      





起動後の出力



 /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.
      
      





最初のチェックのコメントを外すと、出力はエラーと警告の両方になります



 /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を意味します)clangはプラグインをサポートしないため、カスタムコンパイラを使用するにはXcodeを少し使用する必要があります



このアーカイブを解凍し、次のコマンドを実行します。



 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
      
      





これらのハッキングにより、Xcodeに新しいコンパイラが追加され、OSXおよびiPhoneSimulatorのプロジェクトを構築できるようになります。



Xcodeを再起動すると、新しいclangがリストに表示されます







新しいプロジェクトを作成し、「ビルド設定」でカスタムclangを選択します。

プラグインを有効にするには、次のパラメーターを「その他のCフラグ」に追加する必要があります



 -Xclang -load -Xclang /opt/llvm/toy_clang_plugin/build/lib/Debug/ToyClangPlugin.dylib -Xclang -add-plugin -Xclang ToyClangPlugin
      
      









ここでは、 `-add-plugin`を使用します。これは、既存のASTActionを置き換えずにASTActionを追加するためです。

また、このアセンブリのモジュールを無効にする必要があります。



disable_modules



このプロジェクトに「test.m」を追加するか、プラグインの条件に一致する名前で新しいクラスを作成します。

アセンブリ後、より馴染みのある形式で警告とエラーが表示されます。



error_warning



インタラクティブなヒント


エラーや警告を修正するためのインタラクティブなヒントを追加する価値があります。



 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); } }
      
      





プラグインを再構築し、テストプロジェクトのビルドを実行します



warning_fixit_hint



error_fixit_hint



おわりに


ご覧のとおり、clangのプラグインの作成は比較的簡単な作業ですが、Xcodeでの汚いハックが必要であり、clangをビルドする必要があるため、カスタムコンパイラを使用して実稼働環境でアプリケーションをビルドすることはお勧めしません。 Appleはパッチを適用したclangのバージョンを提供していますが、その違いはわかりません。 さらに、Xcode用のClangプラグインは、機能させるために多大な労力を必要としますが、特に使用可能にはなりません。

開発中に発生する可能性のある別の問題があります-不安定で絶えず変化するAPI。



システムで同様のプラグインを使用できますが、他の人にそのような重い部分に依存するように強制しないでください。



コメント、質問、提案がある場合は、 twitterGitHubに書き込むか、ここにコメントを残してください。



ハッピーハッキング!



All Articles