Home Other Creating and using a plugin for Clang in Xcode

Creating and using a plugin for Clang in Xcode

by admin

This tutorial describes how to create a plugin for Clang and covers the following steps :

  • environment setting
  • creating a base plugin
  • Creating an Xcode project to develop a plugin
  • warning generation
  • error generation
  • plugin integration in Xcode
  • Interactive hints for troubleshooting warnings and errors

TL;DR

The finished plugin can be found here

Introduction

In progress BloodMagic I decided it would be nice to have a tool to find semantic errors when using BM. For example, a property marked as lazy in the interface, but is not marked as @dynamic in the implementation, or is marked as lazy , but the container class does not support injection. I’ve come to the conclusion that I have to work with AST and therefore need a full parser.
I’ve tried different options: flex + bison , libclang but I finally decided to write a plugin for Clang.
For the test plugin I set the following goals :

  • Use Xcode for development
  • Integrate a finished plugin into Xcode for everyday use
  • the plugin must be able to generate warnings, errors and show interactive hints (via Xcode)

Features for the test plugin :

  • generate warning if class name starts with a lowercase letter
  • to generate an error if the class name is underscored
  • offer hints for correction

Setting up the environment

To develop the plugin, we need llvm/clang, compiled from source

cd /optsudo mkdir llvmsudo chown `whoami` llvmcd llvmexport LLVM_HOME=`pwd`

The current version of clang on my machine is 3.3.1, so I use the corresponding version of :

git clone -b release_33 https://github.com/llvm-mirror/llvm.git llvmgit clone -b release_33 https://github.com/llvm-mirror/clang.git llvm/tools/clanggit clone -b release_33 https://github.com/llvm-mirror/clang-tools-extra.git llvm/tools/clang/tools/extragit clone -b release_33 https://github.com/llvm-mirror/compiler-rt.git llvm/projects/compiler-rtmkdir llvm_buildcd llvm_buildcmake ../llvm -DCMAKE_BUILD_TYPE:STRING=Releasemake -j`sysctl -n hw.logicalcpu`

Creating a base plugin

Create a directory for the plugin

cd $LLVM_HOMEmkdir toy_clang_plugin; cd toy_clang_plugin

Our plugin is based on the example from the Clang repository and has the following structure :

ToyClangPlugin.exportsCMakeLists.txtToyClangPlugin.cpp

We will use a single file to simplify :
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, conststd::vector<std::string> args) {return true;}};}static clang::FrontendPluginRegistry::Add<ToyASTAction>X("ToyClangPlugin", "Toy Clang Plugin");

Data needed for assembly :
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_LIBSLLVMJITLLVMX86CodeGenLLVMX86AsmParserLLVMX86DisassemblerLLVMExecutionEngineLLVMAsmPrinterLLVMSelectionDAGLLVMX86AsmPrinterLLVMX86InfoLLVMMCParserLLVMCodeGenLLVMX86UtilsLLVMScalarOptsLLVMInstCombineLLVMTransformUtilsLLVMipaLLVMAnalysisLLVMTargetLLVMCoreLLVMMCLLVMSupportLLVMBitReaderLLVMOption)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_LIBSclangclangFrontendclangASTclangAnalysisclangBasicclangCodeGenclangDriverclangFrontendToolclangLexclangParseclangSemaclangEditclangSerializationclangStaticAnalyzerCheckersclangStaticAnalyzerCoreclangStaticAnalyzerFrontend)set (USER_LIBSpthreadcurses)add_clang_plugin(ToyClangPluginToyClangPlugin.cpp)set_target_properties(ToyClangPlugin PROPERTIESLINKER_LANGUAGE CXXPREFIX "")

ToyClangPlugin.exports

__ZN4llvm8Registry*

Now we can generate an Xcode project based on `CMakeLists.txt

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

Run ‘ALL_BUILD’, if successful the finished library will lie here : `lib/Debug/ToyCLangPlugin.dylib’.

RecursiveASTVisitor

AST module provides RecursiveASTVisitor which allows us to walk through the syntax tree. All we need to do is inherit and implement the methods of interest.
As a little test we will display all the classes we have encountered :

class ToyClassVisitor : public RecursiveASTVisitor<ToyClassVisitor>{public:bool VisitObjCInterfaceDecl(ObjCInterfaceDecl *declaration){printf("ObjClass: %sn", 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 a test class and check how the plugin works

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

Running the plugin

/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

The output should be a huge list of classes.

Generating warnings

If the class name starts with a lowercase letter, the user will see a warning.
You need context to generate the warnings

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

Validation of class name :

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

Now we need to add a class with a “bad” name

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

and check the plugin

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

Error generation

If the class name contains an underscore (‘_’), the user will see an error.

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 startup

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

Explain the first check and the output will be both an error and a warning

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

Integration with Xcode

Unfortunately, the system clang (by system clang I mean the clang that comes with Xcode) doesn’t support plugins, so you’ll have to modify Xcode a bit to be able to use the custom compiler
Unpack this one archive and run the following commands :

sudo mv HackedClang.xcplugin `xcode-select -print-path`/../PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer/Library/Xcode/Plug-inssudo mv HackedBuildSystem.xcspec `xcode-select -print-path`/Platforms/iPhoneSimulator.platform/Developer/Library/Xcode/Specifications

These hacks will add a new compiler to Xcode and allow them to build projects for OSX and iPhoneSimulator.
After restarting Xcode you will see the new clang in the list of
Creating and using a plugin for Clang in Xcode
Create a new project and select our custom clang in ‘Build settings’.
You need to add the following parameters to ‘Other C Flags’ to enable the plugin

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

Creating and using a plugin for Clang in Xcode
Note that we use `-add-plugin` here because we want to add our `ASTAction`, not replace the existing one.
We also need to disable the modules for this build :
Creating and using a plugin for Clang in Xcode
Add our `test.m` to this project or create a new class with names that fit the plugin’s criteria.
After building it you should see warnings and errors in a more familiar form :
Creating and using a plugin for Clang in Xcode

Interactive hints

Now we should also add interactive hints for error corrections and warnings

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 the plugin and run the test project build
Creating and using a plugin for Clang in Xcode
Creating and using a plugin for Clang in Xcode

Conclusion

As you see creating a plugin for clang is relatively easy but it requires some dirty hacks with Xcode and you need to build your own clang so I would not recommend using a custom compiler to build applications in production. Apple provides a patched version of clang and we can’t know the difference. Also Clang-plugin for Xcode requires a lot of effort to make it work which does not make it usable.
There is another problem you may encounter during development: an unstable and constantly changing API.
You can use such plugins on your system, but please don’t make other people depend on such heavy stuff.
If you have any comments, questions or suggestions, write to twitter , GitHub Or just leave a comment here.
Happy hacking!

You may also like