Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Создание и применение плагина для Clang в Xcode

Anna | 24.06.2014 | нет комментариев

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

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

Готовый плагин дозволено обнаружить тут

Введение

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

Я пробовал различные варианты: flex bisonlibclang, но в конце концов решил написать плагин для 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.

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

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

Happy hacking!

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB