Blog by Railsware

Creating and using Clang plugin with Xcode

Creating and using Clang plugin with Xcode

This tutorial describes how to create Clang plugin and covers the next things:

tl;dr

Clang Rocks!!!

You can find the plugin here.

Intro

While working on BloodMagic, I realised that it’d be nice to have a tool for checking semantic errors related to BM usage. For example: in the interface property marked as lazy, but not defined as @dynamic in the implementation, or property marked as lazy, but class container doesn’t support injections.

I concluded that I need to work with and I need a full-featured parser.

I’ve tried different approaches: flex+bison, libclang, but ultimately I decided to write a Clang plugin.

Just for testing purposes I’ve started a simple plugin with the following goals:

Features of the test plugin:

Environment setup

For plugin development we need llvm/clang, built from source

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

Current clang version on my system – 3.3.1, so let’s build respective version:

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`

Basic plugin setup

Create directory for plugin

cd $LLVM_HOME
mkdir toy_clang_plugin; cd toy_clang_plugin

Our plugin based on example from Clang repo and here is it’s structure:

ToyClangPlugin.exports
CMakeLists.txt
ToyClangPlugin.cpp

We’ll use one source file for simplification

#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");
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 "")
__ZN4llvm8Registry*

Now we’re able to generate Xcode-project, based on CMakeLists.txt

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

Run ALL_BUILD target, and you’ll see dynamic library at lib/Debug/ToyCLangPlugin.dylib.

RecursiveASTVisitor

Clang AST module provides RecursiveASTVisitor, which allows us to traverse via AST.
We just need to create a subclass and implement interesting methods.
For test we’ll print all the found class names.

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

Let’s create test source file and check how plugin works.

#import <Foundation/Foundation.h>

@interface ToyObject : NSObject

@end

@implementation ToyObject

@end

Rebuild plugin and run

/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

We’ll see a huge list of classes.

Report warnings

Let’s report warning in case if class’ name starts with lowercase letter

Add ASTContext to ToyClassVisitor

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

Add check

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

Add some class with “bad” name

@interface bad_ToyObject : NSObject

@end

@implementation bad_ToyObject

@end

rebuild and run

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

Report error

Let’s generate error in case of class’ name contains _

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 running

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

Uncomment first check checkForLowercasedName and you’ll see both error and warning in the output

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

Unfortunately, system (under ‘system’ I mean Xcode’s clang) clang doesn’t support plugins, so we need to hack Xcode a bit, to allow using of custom compiler.

Unzip this archive and run following commands:

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

This will enable custom compiler for Xcode.

Reopen Xcode and you’ll see new compiler:

Create new project and select newly added custom clang in Build settings

To enable plugin add following parameters to the OTHER_CFLAGS section

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

Note, that we use -add-plugin here, because we want to add our ASTAction, not to replace the existing

Also, you should disable modules for this target/build

Add test.m to this project, or create new one, with class names that match plugin criteria

After build you’ll see error and warnings in a more familiar form

Interactive hints

It’s time to add FixItHints for both warning and error

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 plugin and try to build test project

Conclusion

As you can see, creating a clang plugin is relatively simple, but it needs some dirty hacks with Xcode, and you should use custom built clang, so I’d not recommend you to use this clang for building apps for production usage. Apple provides patched version, and we can’t know the difference between them. Also, it needs a lot of efforts to make it work, which doesn’t make it widely usable.
Another issue you might face is unstable API, cause it uses internal API which changes continuously.

You still can use it on your system for different diagnostic purposes, but please do not force other people to depend on such heavyweight things.

If you have any comments, questions or suggestions feel free to ask me on twitter, open issue on GitHub, or just leave a comment here.

Happy hacking!

Exit mobile version