玩式草子─ソフトウェアとたわむれる日々

第2回 「あなたを,犯人です」 ─ デバッグという名のミステリー

この記事を読むのに必要な時間:およそ 9.5 分

関数の呼び出し関係の追跡

まず,copy_remote_fileを手掛りに,Cのソースコードをgrepしてみます。

% grep 'copy_remote_file ' *.c
fr-archive.c:copy_remote_file (FrArchive  *archive,
fr-archive.c:	copy_remote_file (archive, password);

この結果を見ると copy_remote_file()はfr-archive.cの2ヵ所にしか存在しないようです。前者は上記1218行目の定義している箇所のようですが,後者を調べてみると,1256行目からのfr_archive_load()という関数中にありました。

リスト6 fr-archive.c(1260行目付近)

1255 gboolean
1256 fr_archive_load (FrArchive  *archive,
1257                  const char *uri,
1258                  const char *password)
1259 {
1260         g_return_val_if_fail (archive != NULL, FALSE);
1261
1262         g_signal_emit (G_OBJECT (archive),
1263                        fr_archive_signals[START],
1264                        0,
1265                        FR_ACTION_LOADING_ARCHIVE);
1266
1267         fr_archive_set_uri (archive, uri);
1268         copy_remote_file (archive, password);
1269         
1270         return TRUE;
1271 }       

この部分にもprintf()文を入れて調べると,すでにarchiveという構造体中のファイル名が壊れているようなので,fr_archive_load()を呼び出している部分に遡って調べていきました。

リストは省きますが,fr_archive_load()はfr-window.cのfr_window_archive_open()という関数から呼び出され,この fr_window_archive_open()はmain.cのprepare_apps()から呼び出されていました。

リスト7 main.c(918行目付近)

907                 int i = 0;
908                 while ((filename = remaining_args[i++]) != NULL) {
909                         GtkWidget *window;
910                         GFile     *file;
911                         char      *uri;
912
913                         window = fr_window_new ();
914                         gtk_widget_show (window);
915
916                         file = g_file_new_for_commandline_arg (filename);
917                         uri = g_file_get_uri (file);
918                         fr_window_archive_open (FR_WINDOW (window), uri, GTK_WINDOW (window)); 
919                         g_free (uri); 

このコードを見ると,引数として指定したファイル名がremaining_args[]経由でfilenameに入り,そのfilenameからfileとuriという変数を用意して,fr_window_archive_open()を呼び出す,という流れのようです。ここにもデバッグ用の printf() 文を埋めこんでみましょう。

リスト8 main.cにprintfを追加

917                         uri = g_file_get_uri (file);
918                         printf("my_debug/main.c: filename:%s, file:%s, uri:%s\n", filename, file, uri);
919                         fr_window_archive_open (FR_WINDOW (window), uri, GTK_WINDOW (window)); 

再度コンパイルし直して動かしてみると,図4のような結果になりました

図4 main.c の時点でエンコーディングがUTF-8になっている

図4 main.c の時点でエンコーディングがUTF-8になっている

Google等で調べると,g_file_new_for_commandline_arg()やg_file_get_uri()という関数はGNOMEが元にしているGTK+のベースとなるGlibで提供されている関数で,前者が引数として指定したファイル名からGFileの構造体を作る機能,後者がGFile構造体からファイル名のURI表現を作る機能のようです。

出力結果にfileは正しく表示されていませんが,この変数は本来GFileという構造体なので,単純に文字列として表示しようとしたことが間違いなのでしょう。

ここで気になるのはfilenameがUTF-8形式で表示されていることです。g_file_new_for_commandline_arg()を調べると,ドキュメントには以下のように説明されています。

Creates a GFile with the given argument from the command line. The value of arg can be either a URI,
an absolute path or a relative path resolved relative to the current working directory. This operation
never fails, but the returned object might not support any I/O operation if arg points to a malformed path.

この関数が引数に取るのはURIか絶対パス,あるいは相対パスということですが,現在のPlamoではlocaleにja_JP.eucJP を使っているので,ファイルシステム上のファイル名は絶対パスでも相対パスでもEUC-JPのエンコーディングになっているはずです。ところがfile-rollerに埋めこんだデバッグ文を見ると,引数から受けとったfilenameがUTF-8として表示されています。

どうやらファイル名のエンコーディングの扱いに齟齬が生じている気配なので,UTF-8化されているfilenameをEUC-JPに変換してみることにしました。

リスト9 filenameを強制的にEUC-JPに変換する処理を追加

916                         gchar *tmpname = g_filename_from_utf8(filename, -1, NULL, NULL, NULL);
917                         file = g_file_new_for_commandline_arg (tmpname);
918                         uri = g_file_get_uri (file);
919                         printf("my_debug/main.c: filename:%s, file:%s, tmpname:%s, uri:%s\n", filename, file, tmpname, uri);
920                         fr_window_archive_open (FR_WINDOW (window), uri, GTK_WINDOW (window));

UTF-8化されているfilenameをg_filename_from_utf8()という関数を使ってファイルシステムのlocaleに戻してtmpnameに格納し,以後の処理はtmpnameを対象にしてみます。g_filename_from_utf8()はGlibに用意されている関数で,UTF-8な文字列をlocaleに合わせて変換してくれます。

g_filename_from_utf8()が正しく機能するには, G_FILENAME_ENCODINGという環境変数にファイル名のエンコーディング形式を指定してやる必要があります。解説等を見ると複数のエンコーディング形式を指定することもできるようですが,通常は'@locale'を設定してデフォルトのlocaleに合わせておくのが良いでしょう。

このように修正すると,コンソールに出力されているデバッグメッセージでもtmpnameはEUC-JPで表示され,日本語名の書庫ファイルも開けるようになりました。

図5 日本語名の書庫ファイルも開けるようになった

図5 日本語名の書庫ファイルも開けるようになった

どうやら今回のバグは,file-rollerに引数としてあたえたファイル名が,ファイルシステム上のエンコーディング(EUC-JP)ではなく,内部処理用のエンコーディング(UTF-8)に変換されてしまったため,ファイルが正しく読めなかったことが原因のようです。

後日談

これらの内容を簡単に整理してPlamo Linuxのメンテナ日記に書いてみたら,最近新しくメンテナにご参加いただいた本多さんから,⁠ファイル名がUTF-8になるのは,GOptionEntryのオプション処理の指定によるのでは」という指摘をいただきました。

改めてmain.cのオプション処理部を調べてみると,こうなっていました。

リスト9 main.c(オプション処理関係の部分)

173 static const GOptionEntry options[] = {
174         { "add-to", 'a', 0, G_OPTION_ARG_STRING, &add_to,
175           N_("Add files to the specified archive and quit the program"),
176           N_("ARCHIVE") },
177
178         { "add", 'd', 0, G_OPTION_ARG_NONE, &add,
179           N_("Add files asking the name of the archive and quit the program"),
180           NULL },
181
182         { "extract-to", 'e', 0, G_OPTION_ARG_STRING, &extract_to,
....
198         { "force", '\0', 0, G_OPTION_ARG_NONE, &ForceDirectoryCreation,
199           N_("Create destination folder without asking confirmation"),
200           NULL },
201
202         { G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_STRING_ARRAY, &remaining_args,
203           NULL,
204           NULL },
205
206         { NULL }
207 };

ここで"add-to"や"extract-to"はfile-roller コマンドに与えることができるオプションパラメータです。それらあらかじめ指定されたオプション以外の部分は,202行目のG_OPTION_ARG_STRING_ARRAYの指定で文字列としてremaining_args[] を経由して扱うようになっています。

一方,Glibのドキュメント等を調べてみると,この部分はG_OPTION_ARG_FILENAME_ARRAYという指定も可能で,両者の違いはSTRING_ARRAYが文字列として内部処理用のUTF-8に変換するのに対して,FILENAME_ARRAYではファイル名としてファイルシステムのlocaleを保つようになっているようです。

そこで,この部分をG_OPTION_ARG_FILENAME_ARRAYにしてやれば,tmpnameでファイルシステムのlocaleに戻すような処理を加えなくても,正しく日本語の書庫ファイル名も扱えるようになりました。

結論として,今回のfile-rollerの問題は,本来はlocaleを保ったファイル名として扱うべき引数を,UTF-8な文字列として処理してしまったためにファイル名が読めなくなっていた,というものでした。この問題はlocaleをUTF-8にしている環境では,ファイル名が内部処理用の文字列と一致するので,なかなか気づきにくい類のものでしょう。

今回の記事だけを見れば,ずいぶんスムーズにデバッグが進んだように思われるかも知れませんが,実際のデバッグ作業は迷路の中を手探りで進んだり戻ったりするようなもので,ここに紹介した流れは,試行錯誤の末にようやく見つけた最適通路の紹介,といったところでしょうか。

先にデバッグ作業には探偵のような推理力が必要と述べましたが,カンや推論,仮説を積みあげながらソースコードのどこかに潜んでいるバグを見つけだして退治する作業は,まさに犯人のわからないミステリー小説を読むようなものです。

デバッグ作業は,小説のようにうまくは行かず,結局最後まで犯人(バグ)がわからずに投げ出してしまうことも多々ありますが,その分,今回のようにきれいに犯人やその動機までわかった際には,自分自身で小説を書きあげたような感動を味わうことができます。ミステリー小説は一度読んでしまえば終わりですが,バグを退治したソフトウェアは,一層便利に使えるというおまけもついてきます。

与えられたソフトウェアを,作者が想定した範囲内で使うだけではなく,このように能動的にソフトウェアに関わっていけるのがOSSの面白さでしょう。

著者プロフィール

こじまみつひろ

Plamo Linuxとりまとめ役。もともとは人類学的にハッカー文化を研究しようとしていたものの,いつの間にかミイラ取りがミイラになってOSSの世界にどっぷりと漬かってしまいました。最近は田舎に隠棲して半農半自営な生活をしながらソフトウェアと戯れています。

URLhttp://www.linet.gr.jp/~kojima/Plamo/index.html