演習5¶
はじめに¶
ソースコード読解のツールとしてctagsを紹介しました。ここではctagsを読解の対象とします。
ctagsを読解の対象としたのは筆者がctagsに詳しいこともありますが、理由はそれだけではありません。
他のライブラリへの依存が少ない。
標準Cライブラリ(glibc)の範囲でだいたい理解できる。ファイルの読み書き以外にOSの固有の知識も必要無い。
内部にそれなりに複雑な構造がある。
プログラムのサイズはやたらには大きく無いが、小さくもない。
内部の知識があると改造でき、将来のコードリーディングに役立てることができる。
1. ビルド¶
7. Makefileの読解(3)を参照に universal-ctags プロジェクトからダウンロードしたソースコードから ctags コマンドをビルドして下さい。
Fedoraからもctagsがパッケージの形で提供されています。どちらも実行ファイルの名前はctagsなので、混乱する恐れがあります。universal-ctagsに由来するものは ~/upstream/ctags/ctags と具体的な実行ファイルを指定して実行するのが安全でしょう。universal-ctagsに由来するctagsに`—version`オプションをつけて実行すると 次のような出力となるはずです。
$ ~/upstream/ctags/ctags --version
Universal Ctags Development(4d32ace), Copyright (C) 2015 Universal Ctags Team
Universal Ctags is derived from Exuberant Ctags.
Exuberant Ctags 5.8, Copyright (C) 1996-2009 Darren Hiebert
Compiled: Nov 17 2015, 19:34:30
URL: https://ctags.io/
Optional compiled features: +wildcards, +regex, +debug, +option-directory, +coproc
一方、Fedoraのパッケージに由来するctagsでは次のような出力となります。
$ ctags --version
Exuberant Ctags 5.8, Copyright (C) 1996-2009 Darren Hiebert
Compiled: Jun 17 2015, 03:40:18
Addresses: <dhiebert@users.sourceforge.net>, http://ctags.sourceforge.net
Optional compiled features: +wildcards, +regex
以降で実行するctagsは全てuniversal-ctags プロジェクトに由来するものとします。
2. ユーティリティ関数の理解¶
vString型はctagsのソース中の様々なところで使われています。自動的に伸張する文字列を表現する型です。ctags本体を読む前にvString型の操作を知っておくと、読解がはかどります。
vString型を使って文字列を構築し、構築が完了したら最後に vStringValueマクロで、C言語の文字列(char *)を取り出して、他の関数に引き渡す、というのが一般的な使い方となります。
- main/vstring.c中に定義された vStringPut は文字列の末尾に1文字を追加します。実際のコードを読んで、そのことを確認して下さい。
- vStringCatSは何をやっているでしょうか?
- vStringClearは何をやっているでしょうか?
3. 入力言語の推測処理(1)¶
ctagsには実際にパースする前に、入力ファイルの言語を推測します。—print-languageオプションを使うと推測した言語の名前を印字してパースせずに実行を終了します。
$ cat ctags-input/lang.c
cat ctags-input/lang.c
int
main(void)
{
return 0;
}
$ ~/upstream/ctags/ctags --print-language ctags-input/lang.c
ctags-input/lang.c: C
この例では lang.cがC言語であると推測しています。この場合仕掛けは簡単でファイルの名前の拡張子部分 (.c) を見て判断しています。
しかし複数の言語が同じ拡張子を想定している場合があり、拡張子だけから判断できないことがあります。たとえば .h は C++言語のヘッダファイルですが、他に Objective-C言語のヘッダファイルでもあります。ctagsは同じ.h拡張子に持つ2つのファイルについてもその中を調べて、適切にパーザを選択します。
$ cat ctags-input/a.h
class Object {
void *data;
};
$ cat ctags-input/b.h
@interface Object
{
void *data;
}
@end
$ ~/upstream/ctags/ctags --print-language ctags-input/a.h
ctags-input/a.h: C++
$ ~/upstream/ctags/ctags --print-language ctags-input/b.h
ctags-input/b.h: ObjectiveC
ヒント:—print-languageに加えて—verboseオプションをつけると推測処理の過程を含めた様々な情報を出力できます。この出力を痕跡文字列に使い読解箇所を限定すると良いでしょう。
4. 入力言語の推測処理(2)¶
拡張子を持たなファイル /etc/init.d/network について ctagsは言語を何と推測するか確認して下さい。
$ ~/upstream/ctags/ctags --print-language /etc/init.d/network
/etc/init.d/network: Sh
ここで Shであると推測していますが、ctagsは何を根拠にそのように推測したか調べて下さい。
5. 出力書式の切り替え処理¶
ctagsは出力として viからの利用を主に想定したtags形式、emacsからの利用を想定したtags形式、クロスリファレンスの作成を想定したxref形式の3つをサポートしています。
同じ入力(lang.c)に対して、3つの形式で出力した例:
$ ~/upstream/ctags/ctags -o - ctags-input/lang.c
main ctags-input/lang.c /^main(void)$/;" f
$ ~/upstream/ctags/ctags -e -o - ctags-input/lang.c
^L
ctags-input/lang.c,20
main(void)^?main^A2,4
$ ~/upstream/ctags/ctags -x -o - ctags-input/lang.c
main function 2 ctags-input/lang.c main(void)
それぞれの出力形式を担当する関数があり、与えられたオプションの内容によって切り替えています。実際にソースコード箇所を引用して切り替えている箇所を説明して下さい。
6. マクロに対するシグネチャーの収集処理¶
C++には、同じ名前を持つ関数を引数の数と型(シグネチャー)に応じて複数定義することができます。
$ cat ctags-input/lang.cpp
int foo (void)
{
return foo (1)
}
int foo (int i)
{
return foo (2, i);
}
int foo (int i, int j)
{
return i + j;
}
$ ~/upstream/ctags/ctags -n -o - ctags-input/lang.cpp
foo ctags-input/lang.cpp 11;" f
foo ctags-input/lang.cpp 1;" f
foo ctags-input/lang.cpp 6;" f
ctagsは主に名前を採取します。同じ名前が複数採取されてもうれしくありません。そのため、オプションで signature(S)フィールドを有効にすると(—fields=+Sを追加すると)、シグネチャーの情報が得られればそれを追加します。
$ ~/upstream/ctags/ctags -n --fields=+S -o - ctags-input/lang.cpp
foo ctags-input/lang.cpp 11;" f signature:(int i, int j)
foo ctags-input/lang.cpp 1;" f signature:(void)
foo ctags-input/lang.cpp 6;" f signature:(int i)
この出力形式を真似て、筆者はマクロについてシグネチャーを採取することを考えました。
例を示します。
#define PROC(A) int A(void) { return 0; }
という入力に対して
PROC ctags-input/macro.c /^#define PROC(/;" d file: signature:(A)
を出力できるようにすることを考えました。関数と同様に signature:(A)と記録されます。
この機能の実現した差分が以下のページにあります。
https://github.com/masatake/ctags/commit/b2b7ac6457e571f768ad5191cfbdf885fc225ad1?diff=unified
変更履歴とパッチファイルを参考にこの差分(main/get.cに対する変更点)を解説して下さい。
ヒント
- makeTagEntry関数に渡すデータの型tagEntryInfoについて、定義が main/entry.h にあります。tagEntryInfo型のextensionFields中のsignatureについてここがNULLで無い場合でかつ、—fields=+Sが指定されている場合、すなわち signature フィールドが有効になっている場合、出力にsignature:を含めます。
- fileGetCは現在の入力ファイルから1文字を読み込みます。