Developer's Blog

Clang の構文解析インターフェースを Python から叩いてみようという話

iOS App 開発担当の松本です。
Sleipnir for Mac をお試しいただいた皆様、ありがとうございます。まだの方は是非!

さて、先日の記事にもあるように、Apple は LLVM を基盤とした開発環境を着々と整備してきています。今日は LLVM 関連ツールの中でも特に注目されている Clang で少し遊んでみたいと思います。

Clang は LLVM をバックエンドとした C/C++/Objective-C のコンパイラです。GCC に比べてコンパイルが速かったり、生成されたバイナリがより最適化されてたりと、色々と優れているのですが、中でも面白いのがコンパイル途中で生成される構文木に、アクセスする為のインターフェースが用意されている点です。Xcode ではお馴染みの「コード補完」や「リファクタリング」などの機能が、このインターフェースを利用して実装されていて、方法次第でかなり強力なツールを作れる事がわかります。今回はこのインターフェースを叩いて構文解析を少しだけ行います。

早速試してみます。まず llvm および clang をインストールしましょう。llvm と clang は別々にチェックアウトしますが、ビルドは一括でできます。また上記のインターフェースには Python 用のバインディングが用意されているので、合わせてパスの通ったディレクトリにコピーしておきましょう。

$ svn co http://llvm.org/svn/llvm-project/llvm/trunk llvm
$ cd llvm/tools
$ svn co http://llvm.org/svn/llvm-project/cfe/trunk clang
$ cd ..
$ ./configure
$ make && make install
$ cp -R tools/clang/bindings/python/clang /path/to/python_modules

ここでは以下の Objective-C のコードの構文解析を行ってみる事にします。簡単のため本来必要な #import 文は省いています。

/* test.m */
// 登録されているすべてのクラスを表示します
void showClassList()
{
    int numClasses = objc_getClassList(NULL, 0);
    if (numClasses == 0) return;
    
    Class *classes = malloc(sizeof(Class) * numClasses);
    numClasses = objc_getClassList(classes, numClasses);

    NSLog(@"%d", numClasses);
    for (int i = 0; i < numClasses; i++) {
        Class *c = (Class *)classes[i];
        NSLog(@"%@", c);
    }
    
    free(classes);
} 

int main() {
    showClassList();
    return 0;
}

まずは構文木をすべて表示してみましょう。以下のコードを実行します。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# python dump_tree.py test.m

import sys
import clang.cindex

def visit_node(node, indent=0):
  print '%s%s : %s' % ('  ' * indent, node.kind.name, node.spelling)
  for c in node.get_children():
    visit_node(c, indent=indent+1)

index = clang.cindex.Index.create()
tree = index.parse(sys.argv[1])
visit_node(tree.cursor)

長くなるので省略しますが、上記コードを実行すると以下のように構文木が出力されます。簡単ですね。

TRANSLATION_UNIT : None
  TYPEDEF_DECL : __int128_t
  TYPEDEF_DECL : __uint128_t
  TYPEDEF_DECL : SEL
    TYPE_REF : None
  TYPEDEF_DECL : id
    TYPE_REF : None
  TYPEDEF_DECL : Class
    TYPE_REF : None
  STRUCT_DECL : __va_list_tag
    FIELD_DECL : gp_offset
    FIELD_DECL : fp_offset
    FIELD_DECL : overflow_arg_area
    FIELD_DECL : reg_save_area
...

次に変数が宣言されている部分を抜き出してみましょう。先ほどの Python スクリプトを少し書き換えます。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
import clang.cindex

def visit_node(node, indent=0):
  if node.kind.name == 'VAR_DECL':
    print '%s (line:%s column:%s)' % (node.spelling, node.location.line, node.location.column)
  for c in node.get_children():
    visit_node(c)
    
index = clang.cindex.Index.create()
tree = index.parse(sys.argv[1])
visit_node(tree.cursor)

これを実行すると以下のような結果が得られます。変数宣言部分のみ抜き出せていますね。

numClasses (line:4 column:9)
classes (line:7 column:12)
i (line:11 column:14)
c (line:12 column:16)

このように Clang は単純にコンパイラとしてだけではなく、強力な構文解析機としても利用できます。C++ や Objective-C の構文解析器を flex や bison で組む事を考えると、とてつもなく楽ができますね。現状では doxygen で出力した api リファレンス程度しかドキュメントがありませんが、どんなことが出来るのかざっと見ておくのは悪くないでしょう。

C++0x に未対応等、現時点では GCC を完全に置き換えるまでは行かないものの、速いペースで開発が進む Clang の動向をこれからも見守って行きたいと思います。

Copyright © 2019 Fenrir Inc. All rights reserved.