libclanglua (https://github.com/ifritJP/libclanglua) を利用した c/c++ ソースコードタグシステム
libclang を利用することで、異なる構造体、クラスで同名のメンバーがあるような場合でも、 型を認識して正確に参照箇所をリストできます。
libclang を利用したソースコードタグシステムは RTags がメジャーですが、 複雑な Makefile を使用したプロジェクトだと適応し難いので、 プロジェクトごとにカスタマイズしやすいインタフェースを持った ソースコードタグシステムを作成しました。
lctags を利用した際のサンプルです。
シンボル名、メンバー名の補完を行ないます。
補完したい位置にカーソルを移動し、 C-c C-/ で候補を表示します。
補完候補の絞り込みは helm (あるいは anything) を利用します。
メンバーが構造体の場合、 C-M-f でさらに展開します。 C-M-b で展開を戻します。
メンバー補完する際、 C-u プレフィックスを付けて C-u C-c C-/ を実行すると、 構造体型のメンバーを候補にリストします。
lctags-helm-expand-ignore-pattern に構造体メンバー名を指定することで、 そのメンバーの構造体は候補にリストしません。
lctags-default-expand-candidate-p を nil 意外を指定することで、 C-c C-/ と C-u C-c C-/ の動作を入れ替えます。
ファイルを変更している場合、補完が正常に動作しない場合があります。 一旦 lctags-update-this-file (C-c l u) で解析情報を更新してから実行してください。
enum 値に対して、次の補完が可能です。
- enum 型名から enum 値を補完 C-c C-x
- enum 値から、同型の enum 値に変換 C-c C-x
- enum 型の変数への ==, = で enum 値を補完 C-c C-/
このサンプル動画では、次の処理を行なっています。
enum_t型の変数を宣言し、その初期値として func12345() の戻り値を設定- func を入力して func12345() を展開。
- 展開された func12345 のプロトタイプの引数
enum_tから enum 値enum_val2を補完 - val の比較値として
enum_val1を補完 enum_val1をenum_val2に補完
次の定型処理を行ないます。
- M-x lctags-generate-to-convert-enumName-at
- カーソル位置の enum 型の値から、対応する文字列を返す処理
- M-x lctags-generate-to-dump-member-at
- カーソル位置の構造体変数から、構造体メンバーをダンプする処理
サブルーチン化支援を行ないます。
- M-x lctags-split-at
- カーソル位置のブロックをサブルーチン化します。
詳しい使用方法は次を参照してください。
https://qiita.com/dwarfJP/items/263db943fe8ebc407abb
include、関数の関係を示すグラフを作成することができます。 この機能は graphviz の dot を利用します。 グラフは svg フォーマットで作成します。
新しいコールグラフ
https://qiita.com/dwarfJP/items/ef868813a7aaa2572468
$ lctags graph <incSrc|inc|caller|callee|symbol> [-d depth] [-b|-o file] [-f format] [name]
$ lctags graph-at <caller|callee|symbol> [-d depth] [-b|-o file] [-f type] [--lctags-target target] file line column 次のグラフを作成します。
- include 元 (incSrc)
- include 先 (inc)
- 関数呼び出し元 (caller)
- 関数呼び出し先 (callee)
- シンボル参照元 (symbol)
name には、関数名あるいはファイル名あるいはシンボル名、 あるいはそれらを示す ID を指定します。 name を省略した場合、ID をリストします。 関数名、シンボル名は完全限定名で指定する必要があります。 ファイル名は、カレントディレクトリからの相対パスか、フルパスで指定します。
-d は、表示するグラフの階層を指定します。 デフォルトでは、4 階層までのグラフを作成します。
-o は、作成するグラフのファイル名を指定します。
-b は、作成したグラフを表示します。
-f は、作成するグラフの画像フォーマットを指定します。
lctags コマンドを使用します。
usage:
- build DB
lctags init projDir [-it] [-is] [-im]
lctags build compiler [--lctags-out-info] [--lctags-conf conf] [--lctags-target target] [--lctags-recSql file] [--lctags-prof] [--lctags-srv] [--lctags-indiv] comp-op [...] src
lctags update [-j jobs] pattrn
lctags register [--lctags-conf conf] [--lctags-target target] <-i|file>
lctags depIncs comp-op src
lctags server [--lctags-target target] <start|stop>
lctags statusServer <start|stop|wait>
lctags status
- query DB
lctags dump <ver|all|target|targetList|file|ref|def|call|inc|digest|prepro> [path]
lctags ref-at[a] [--lctags-target target] [-i] file line column
lctags def-at[a] [--lctags-target target] [-i] file line column
lctags call-at[a] [--lctags-target target] [-i] file line column
lctags ns-at [--lctags-target target] [-i] file line column
lctags comp-at [--lctags-target target] [-i] file line column
lctags inq-at [--lctags-target target] [-i] file line column
lctags list <incSrc|inc> [-d depth] name
lctags -x[t|s|r][a] [--use-global] symbol
lctags -xP[a] [--use-global] file
lctags -c [--use-global] symbol
lctags dcall
- graph
lctags graph <incSrc|inc|caller|callee|symbol> [-d depth] [-b|-o file] [-f type] [name]
lctags graph-at <caller|callee|symbol> [-d depth] [-b|-o file] [-f type] [--lctags-target target] file line column
- modify db
lctags rm <file|tgt> name
lctags shrink [--lctags-db path]
lctags chkFiles [--lctags-db path]
lctags chg-proj projDir [--lctags-db path] [src@dst src@dst src@dst src@dst]
lctags set-projDir projDir [--lctags-db path]
- misc
lctags split-at [--lctags-target target] [-i] file line column [-ignore-sym-list sym1,sym2,...]
lctags clang-ver
option:
init: initialize DB file. "projDir" is a root directory of your project.
-it: enable individual type mode.
-is: enable individual struct mode.
-is: enable individual macro mode.
build: build DB for "src".
"compiler" is "gcc" or "cc" or ....
"comp-op" is compiler option. This include source file path.
register: register source file from json.
shrink: shrink DB.
chg-proj: change project directory.
dump: dump DB.
--lctags-conf: confing file.
--lctags-target: set build target.
-x: query DB.
-xt: symbol declaration
-xs: symbol declaration
-xr: symbol reference
-xP: file list
-c: list symbol.
def-at: symbol declaration at position
ref-at: symbol reference at position
call:at: function call at position
-i: input from stdin for source file contents.
--use-global: use GNU global when db is not found.
graph: draw graph.
graph-at: draw graph at position.
inc: include relation.
caller: caller graph.
callee: callee graph.
-d: depth.
-b: browse graph.
-o: output image file.
-f: image type. (svg, png)
common option:
--lctags-quiet: discard clang diagnostic.
--lctags-db: set DB file path.
--lctags-log: set log level. default is 1. when lv > 1, it is datail mode.$ lctags init .
プロジェクトのルートディレクトリで上記のコマンドを実行します。 このコマンドを実行すると、以下のファイルが生成されます。
- lctags.conf
- lctags.sqlite3
これは、一度だけ実行します。
init を実行すると、次のエラーが表示される場合があります。
$ lctags init .
/usr/include/stdio.h:33:11: fatal error: 'stddef.h' file not found
please set clang inc-path at getClangIncPath() in /proj/lua-5.4.2/src/lctags.conf, and retry init.これは、 lctags で利用している libclang が clang の include ディレクトリを探せないことを示すエラーです。
この場合、 init コマンドで生成された lctags.conf を編集する必要があります。
lctags.conf 内の getClangIncPath() を編集します。
function config:getClangIncPath()
-- return "/usr/lib/llvm/lib/clang/include"
return nil -- ここを編集
end上記の ここを編集 の nil を、 clang の include ディレクトリに置き換えます。
clang の include ディレクトリは、次のコマンドで検索します。
$ find /usr -iname 'stddef.h'
/usr/include/linux/stddef.h
/usr/lib/gcc/x86_64-linux-gnu/6/include/stddef.h
/usr/lib/llvm-3.8/lib/clang/3.8.1/include/stddef.hここで、 /usr/lib/llvm-3.8/lib/clang/3.8.1/include が
clang の include ディレクトリです。
環境によってパスが異なるので、適宜読み替えてください。
このパスを lctags.conf にセットします。
function config:getClangIncPath()
-- return "/usr/lib/llvm/lib/clang/include"
return "/usr/lib/llvm-3.8/lib/clang/3.8.1/include" -- ここを編集
endこの状態でもう一度 lctags init . を実行し、エラーしないかどうか確認してください。
$ lctags build compiler [–lctags-target target] comp-op […] src
ソースの解析を行ないます。コンパイルは行ないません。
compiler には、使用しているコンパイルコマンドを指定します。
comp-op には、コンパイラに指定しているコンパイルオプションを指定します。
src には、コンパイル対象のソースファイルパスを指定します。オプションの最後に指定する必要はありません。 src は、一つだけ指定してください。
このコマンドは、コンパイルを実行しているディレクトリと同じディレクトリで実行してください。
基本的には通常のコンパイルと同じオプションを渡すだけです。
例えば、 次のようにコンパイルしているソースを解析する場合、
$ gcc hoge.cpp -c -o hoge.o -Iinclude
次のように lctags を実行します。
$ lctags build gcc hoge.cpp -c -o hoge.o -Iinclude
なお、lctags はデフォルトで gcc のコンパイルオプションに対応していますが、 後述する方法で簡単にカスタマイズできます。
lctags は解析時に使用したコンパイルオプションを記憶し、 後述するインデックス問い合わせ時に利用します。 1 つのソースに対して、複数のコンパイルオプションを切り替えて 異なるオブジェクトを生成するような場合、 そのコンパイルオプションのセットを識別する必要があります。 –lctags-target オプションを指定することで、 コンパイルオプションを識別する名前を設定することができます。
上で説明した解析方法では、ソースの登録と解析を同時に行ないます。 この場合、ソースファイルが大量にある場合に解析効率が悪くなることがあります。
ソースの登録と解析を分割することで、解析効率を改善できます。
ソースの登録には、次の 2 つの方法があります。
- build に –lctags-only-reg オプションを追加する
- cmake が生成する JSON ファイルを利用する
–lctags-only-reg オプションを利用する場合、 全てのソースファイルに対して build –lctags-only-reg を実行後、 次のコマンドを実行することで、ソースの登録が行なえます。
lctags register -iJSON ファイルを利用する場合、 次のコマンドを実行することで、ソースの登録が行なえます。
$ cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON .
$ lctags register compile_commands.jsonソース登録後、次のコマンドを実行することでソースの解析を行ないます。
lctags update -j 3 dirこのコマンドは内部的に make を呼び出します。
ここで 3 は make の並列ジョブ数を指定します。 環境に合せて指定してください。 dir は、解析対象のディレクトリ、あるいはファイルを指定します。
ソースの追加・削除がなく、コンパイルオプションの変更がない限り、 lctags update コマンドだけでソース解析を行なえます。
ソース登録と解析に分けない build で解析した場合でも、 lctags update コマンドを使用することで、変更したファイルだけ解析することができます。
-j オプションで解析を並列処理することで、解析時間を短縮できます。
しかし、並列数が一定数を越えると、ほとんど効果がありません。
これは、DB ファイルにアクセスする処理に時間が取られているようです。
この処理を短縮するには、 DB ファイルを置くストレージのアクセス時間を短縮するのがもっとも効果的です。
linux で利用できる現在最もアクセス時間の早いストレージは、tmpfs でしょう。
そこで、解析時に tmpfs に DB ファイルを置く方法を説明します。
ここでは例として次の構成を前提に説明します。
- /dev/sdm が tmpfs
- /proj/top がプロジェクトのトップディレクトリ
$ cp /proj/top/lctags.sqlite3 /dev/sdm
$ cd /dev/sdm
$ lctags chg-proj /proj/top
$ cd /proj/top
$ lctags build gcc --lctags-db /dev/sdm/lctags.sqlite3 test1.c --lctags-only-reg
$ lctags build gcc --lctags-db /dev/sdm/lctags.sqlite3 test2.c --lctags-only-reg
$ lctags build gcc --lctags-db /dev/sdm/lctags.sqlite3 test3.c --lctags-only-reg
$ lctags update . -j 10 --lctags-db /dev/sdm/lctags.sqlite3
$ cp /dev/sdm/lctags.sqlite3 /proj/top
$ lctags chg-proj .- まず、 DB ファイルを tmpfs にコピーします。
- 3 行目で chg-proj コマンドを使って DB ファイルのプロジェクトディレクトリを /proj/top にセットします。
- 5〜7行目でファイルを登録し、8行目で解析します。
- 9 行目で、解析終了した DB ファイルを元のプロジェクトディレクトリにコピーします。
- 最後に 10 行目で、chg-proj コマンドを使って DB ファイルのプロジェクトディレクトリを . にセットします。
tmpfs を使用する注意事項として、RAM の使用量に十分を気を付けることです。 tmpfs は RAM を消費します。RAM の空きが無くなれば swap します。 そして、swap すれば当然パフォーマンスが落ちます。
よって tmpfs を利用する際は、swap が発生しない様に並列処理数を調整等が必要です。
次のパターンを利用できます。
$ lctags -x[r|t|P][a] [--use-global] symbol
$ lctags ref-at[a] [--lctags-target target] file line column
$ lctags def-at[a] [--lctags-target target] file line column-x は、 GNU global と互換のあるモードです。
r は、シンボルの参照場所をリストします。
t は、シンボルの定義場所をリストします。
P は、ファイルをリストします。
a は、表示する場所のファイルパスをフルパスにします。
-x を指定した場合、シンボル名だけを使用して問い合わせするので、 型を認識した検索には向きません。 ただし、完全限定名を指定することで型指定可能です。
–use-global を指定することで、 lctags の DB が存在しない場合に GNU global を実行します。
ref-at[a] は、指定ファイルの場所のシンボルを使用している参照箇所をリストします。
def-at[a] は、指定ファイルの場所のシンボルの定義箇所をリストします。
指定のファイルにコンパイルエラーがあると、正常に動作しません。
解析時に –lctags-target を指定している場合は、 –lctags-target を指定する必要があります。
emacs からアクセスする場合は、 lctags.el をロードしてください。
(add-to-list 'load-path "/hoge/foo/lctags" t)
(require 'lctags-conf)なお、 lctags.el は gtags.el が利用できることが前提になっています。
lctags.el は、マイナーモードの機能を提供します。
以下の説明では、上記キーバイドが設定されていることを前提としています。
- lctags-def (M-t)
- 指定シンボルの定義場所をリストします。
- gtags-find-tag と互換の動作です。
- C-u M-t とすることで、 lctags-def-at を実行します。
- C-u C-u M-t とすることで、 lctags ではなく GNU global を利用します。
- lctags-def-pickup-symbol (C-M-t)
- 指定文字列を含むシンボルをリストし、リストから選択されたシンボルの定義場所をリストします。
- 文字列が
^を先頭に持つ場合、先頭にその文字列を持つシンボルの定義場所をリストします。 - 文字列が
$を末尾に持つ場合、末尾にその文字列を持つシンボルの定義場所をリストします。 - 上記以外の場合、その文字列を含むシンボルの定義場所をリストします。
- lctags-ref (M-r)
- 指定のシンボルの参照場所をリストします。
- gtags-find-rtag と互換の動作です。
- C-u M-r とすることで、 lctags-ref-at を実行します。
- C-u C-u M-r とすることで、 lctags ではなく GNU global を利用します。
- lctags-def-at (C-c l d)
- lctags コマンドの def-at オプションを呼び出します。
- カーソル位置のシンボルの定義位置をリストします。
- #include にカーソル位置を合せると、インクルードしているファイルを開きます。
- 対象ファイルを事前に lctags で解析しておく必要があります。
- lctags-ref-at (C-c l r)
- lctags コマンドの ref-at オプションを呼び出します。
- カーソル位置のシンボルの参照位置をリストします。
- 対象ファイルを事前に lctags で解析しておく必要があります。
- lctags-call-at (C-c l c)
- lctags コマンドの call-at オプションを呼び出します。
- カーソル位置の関数の呼び出し位置をリストします。
- 対象ファイルを事前に lctags で解析しておく必要があります。
- lctags-graph-caller-at (C-c l g r)
- lctags コマンドの graph-at caller オプションを呼び出します。
- カーソル位置の関数の呼び出し元を辿るコールグラフを表示します。
- 対象ファイルを事前に lctags で解析しておく必要があります。
- lctags-graph-callee-at (C-c l g r)
- インタラクティブに操作可能な新しいコールグラフ表示方式に対応しています。
- 詳しくは以下を参照してください。
- https://qiita.com/dwarfJP/items/ef868813a7aaa2572468
- lctags コマンドの graph-at callee オプションを呼び出します。
- カーソル位置の関数の呼び出し先を辿るコールグラフを表示します。
- 対象ファイルを事前に lctags で解析しておく必要があります。
- インタラクティブに操作可能な新しいコールグラフ表示方式に対応しています。
- lctags-graph-symbol-at (C-c l g s)
- lctags コマンドの graph-at symbol オプションを呼び出します。
- カーソル位置のシンボルの参照元を辿るコールグラフを表示します。
- 対象ファイルを事前に lctags で解析しておく必要があります。
- lctags-graph-inc (C-c l g i)
- lctags コマンドの graph inc オプションを呼び出します。
- 現在のファイルがインクルードしているファイルのグラフを表示します。
- 対象ファイルを事前に lctags で解析しておく必要があります。
- lctags-graph-inc (C-c l g I)
- lctags コマンドの graph incSrc オプションを呼び出します。
- 現在のファイルをインクルードしているファイルのグラフを表示します。
- 対象ファイルを事前に lctags で解析しておく必要があります。
- lctags-list-incSrc-this-file (C-c l l I)
- 現在のバッファで開いているファイルをインクルードしているファイル一覧をリストします。
- デフォルトは、4 階層までの結果をリストします。
- C-c l l C-u N I で、解析する階層として N を指定できます。 N は数字キーです。
- lctags-list-inc-this-file (C-c l l i)
- 現在のバッファで開いているファイルがインクルードしているファイル一覧をリストします。
- デフォルトは、100 階層までの結果をリストします。
- C-c l l C-u N I で、解析する階層として N を指定できます。 N は数字キーです。
- lctags-update-this-file (C-c l u)
- 現在のバッファで開いているファイルを解析しなおします。
- バッファで開いているファイルがヘッダファイルの場合は動作しません。
- lctags-display-diag (C-c C-f)
- 現在のバッファで開いているファイルを構文エラーチェックする
DB ファイルをプロジェクトのルートディレクトリとは別のディレクトリに作成している場合、 プロジェクトを別のディレクトリに移動したりコピーした際、 次のコマンドを実行する必要があります。
$ lctags chg-proj .
次の 2 パターンのビルド方法があります。
- docker を利用し、 コンテナ内に lctags 環境を作成する
- ローカル環境でビルドする
なお、ローカル環境でビルドする場合は次の 2 パターンのビルド方法があります。
- apt が利用可能な環境でビルドする
- makefile を編集する
docker を利用する場合、次のディレクトリでそれぞれ docker-compose を利用してください。
- docker/ubuntu20.04
- libclang9 用
- docker/ubuntu22.04
- libclang15 用
利用したい libclang のバージョンに合せてどちらか一方をビルドしてください。
$ cd docker/ubuntu???? # <--- ubuntu???? は、目的の libclang のバージョンに合せる
$ sudo docker-compose builddocker 内の lctags を使用する場合は、以下のように実行してください。
$ sudo docker-compose up -d
$ ./lctags.sh 事前に docker-compose を編集し、 ホスト内で管理するプロジェクトディレクトリと、 docker 内のプロジェクトディレクトリが同名のパスになるように volume を設定しておいてください。
そうすることで、ホスト内のプロジェクトディレクトリで lctags.sh を実行すると、 docker 内で lctags が実行されます。
ローカル環境でビルドするには、事前にビルド用の環境を整えてから make を実行します。
- swig (3.0)
- lua, lua-dev(5.2 or 5.3)
- libclang-dev (r380 or r390)
- luasqlite3 (0.9.4)
- openssl
debian, ubuntu 等の apt でパケージ管理する OS の場合、 Makefile を編集せずに以下のコマンドでビルドできます。 ただし、動作を確認しているのは debian 9.1, ubuntu 17.04 の 64bit だけです。
$ make build_for_apt [PROXY=http://proxy.hoge:port/]
$ sudo make install必要なライブラリ等がインストールされていない場合は sudo apt コマンドでインストールします。
lua, libclang, luasqlite3 の環境にあわせて変更してください。
$ make build
$ sudo make installlctags の次の動作をカスタマイズできます。
- コンパイルオプションの変換
- 解析無視のファイルパターン指定
カスタマイズは Lua で行ないます。
次のファイルをコピーし、これを編集します。
src/lctags/config.lua
編集したファイルのパスを、lctags build 時の –lctags-conf conf オプションに指定します。
lctags の build に指定するコンパイラ名を gcc 以外の名前を指定してください。
コピーしたコンフィルファイルの convertCompileOption() メソッドを、 使用しているコンパイラにあわせて変更してください。
インクルードパスと define シンボルを、 clang が認識する -I, -D で与えるように変換してください。
-I, -D 以外のオプションは与えないようにしてください。
convertCompileOption() は、2 つの引数(compiler, arg)を持ちます。 compiler は、 build で指定したコンパイラ名です。 arg はコンパイラオプション文字列です。
convertCompileOption() は、コンパイルオプションの変換結果を返します。 変換結果は次のいずれかです。
- “opt”
- “src”
- “skip”
“opt” は、 arg が libclang に渡すべきオプションであること示します。 このとき、”opt” に続けて libclang に渡すオプションを返します。
“src” は、 arg が解析対象のソースファイルパスであること示します。 このとき、”src” に続けてソースファイルパス返します。
“skip” は、arg が無視すべきオプションであることを示します。
getDefaultOptionList() は、 libclang に追加で指定するコンパイルオプションのリストを返します。
lctags の build で指定されたファイルの解析を無視するかどうかを判定する ファイルパスのパターンを指定します。
パターンは、 2 つの文字列を要素に持つ table の配列を返します。
{
{ "simple", "ignore.c" }, -- this is simple match.
{ "lua", "^ignore.c$" }, -- this is lua pattern match.
}1つ目の文字列は “simple” か “lua” です。 2つ目の文字列は無視するファイルパスのパターンを指定します。
“simple” は、パターン文字列がファイルパス文字列そのものであることを示します。 なお、パターンが部分一致すると無視します。
“lua” は、パターン文字列が Lua のパターン文字列であることを示します。 パターンに一致すると無視します。





