Создание и использование плагина для Clang в Xcode

от автора

Данный туториал описывает создание плагина для Clang и покрывает следующие шаги:

  • настройка окружения
  • создание базового плагина
  • создание Xcode-проекта для разработки плагина
  • генерирование предупреждений
  • генерирование ошибок
  • интеграция плагина в Xcode
  • интерактивные подсказки по устранению предупреждений и ошибок
TL;DR

Готовый плагин можно найти здесь

Вступление

В процессе разработки BloodMagic я решил что было бы здорово иметь инструмент для поиска семантических ошибок при использовании BM. К примеру, свойство отмечено как lazy в интерфейсе, но в имплементации не отмечено как @­dynamic, или отмечено как lazy, но класс контейнер не поддерживает инъекции. Я пришел к выводу что прийдется работать с AST, а потому нужен полноценный парсер.

Я пробовал разные варианты: flex + bison, libclang, но в конце концов решил написать плагин для Clang.

Для тестового плагина я задался следующими целями:

  • использовать Xcode для разработки
  • интегрировать готовый плагин в Xcode для повседневного использования
  • плагин должен уметь генерировать предупреждения, ошибки и показывать интерактивные подсказки (посредством Xcode)

Фичи для тестового плагина:

  • генерировать предупреждение, если имя класса начинается с буквы в нижнем регистре
  • генерировать ошибку, если имя класса содержит подчеркивание
  • предлагать подсказки для исправления

Настройка окружения

Для разработки плагина нам нужен 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 

Мы будем использовать один файл для упрощения:

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* 

Теперь мы можем сгенерировать Xcode-проект на основе `CMakeLists.txt`

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

К сожалению, системный (под системным я понимаю clang из поставки Xcode) 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 в ‘Build settings’.
Чтобы включить плагин нужно добавить следующие параметры в ‘Other C Flags’

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

Обратите внимание, что здесь мы используем `-add-plugin`, потому как хотим добавить наш `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, потому я бы не рекомендовал использовать кастомный компилятор для сборки приложений в production. Apple предоставляет патченую версию clang’а, и мы не можем знать в чем отличие. Кроме того Clang-плагин для Xcode требует немало усилий для того чтобы сделать его работоспособным, что не делает его особо юзабельным.
Есть еще одна проблема, с которой можно столкнуться при разработке, — нестабильный и постоянно изменяющийся API.

Вы можете использовать подобные плагины на вашей системе, но, пожалуйста, не заставляйте других людей зависеть от таких тяжелых штук.

Ели у вас есть какие-то комментарии, вопросы или предложения пишите в twitter, GitHub или просто оставьте комментарий здесь.

Happy hacking!

ссылка на оригинал статьи http://habrahabr.ru/post/214275/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *