後半戦
履歴全削除ボタンが押された時に発生するイベントフローを追跡することで履歴スタックへアクセスしているコードを発見しよう、という試みはなかなか決定的な成果が得られず倦怠感が漂い始めた。そこで気を改めて、履歴スタックにアクセスするはずである別のユーザー操作、ペイントツールによるお絵かきに狙いを定めた。
ユーザーがエアブラシで線を一筆描けば、線の書き始め、あるいは書いている最中あるいは書き終えてマウスをアップした瞬間のどこかで必ずその作業を履歴に保存するコードが実行されるはずである。より具体的にはユーザーの操作に応じたイベントが発生し、そのイベントに対するコールバック関数が実行され、その関数の内部で履歴スタック操作が行われるはずである。最初に考えるべきことは問題はそのコールバック関数から連なる関数呼び出しをどこで(デバッガが)捉えるかということである。我々はこれまでの試行錯誤から、app/widgets/gimpundoeditor.c内で定義されているgimp_undo_editor_undo_eventが履歴スタックの操作に伴って呼ばれることがわかっていた(前か後かは定かでない)ので、GDBを起動してとりあえずこの関数にブレイクポイントを貼った。そしてgimpを起動しエアブラシで線を描いてみると都合よくブレイクポイントが引っかかった。バックトレイスをすると以下のようになった
Breakpoint 1, gimp_undo_editor_undo_event (image=0xdbaba0,
event=GIMP_UNDO_EVENT_UNDO_PUSHED, undo=0x2ad30f0, editor=0x7fffd97f5510)
at gimpundoeditor.c:349
(gdb) bt
#0 gimp_undo_editor_undo_event (image=0xdbaba0,
event=GIMP_UNDO_EVENT_UNDO_PUSHED, undo=0x2ad30f0, editor=0x7fffd97f5510)
at gimpundoeditor.c:349
#1 0x000000000072c125 in gimp_marshal_VOID__ENUM_OBJECT (closure=0x26a9fb0,
return_value=0x0, n_param_values=3, param_values=0x7fffffffd520,
invocation_hint=0x7fffffffd4c0, marshal_data=0x0) at gimpmarshal.c:611
#2 0x00007ffff43033b8 in g_closure_invoke (closure=0x26a9fb0,
return_value=0x0, n_param_values=3, param_values=0x7fffffffd520,
invocation_hint=0x7fffffffd4c0)
at /build/buildd/glib2.0-2.40.2/./gobject/gclosure.c:768
#3 0x00007ffff4314d3d in signal_emit_unlocked_R (node=node@entry=0xe427f0,
detail=detail@entry=0, instance=instance@entry=0xdbaba0,
emission_return=emission_return@entry=0x0,
instance_and_params=instance_and_params@entry=0x7fffffffd520)
at /build/buildd/glib2.0-2.40.2/./gobject/gsignal.c:3551
#4 0x00007ffff431ca29 in g_signal_emit_valist (instance=<optimized out>,
signal_id=<optimized out>, detail=<optimized out>,
var_args=var_args@entry=0x7fffffffd6d8)
at /build/buildd/glib2.0-2.40.2/./gobject/gsignal.c:3307
#5 0x00007ffff431cce2 in g_signal_emit (instance=<optimized out>,
signal_id=<optimized out>, detail=<optimized out>)
at /build/buildd/glib2.0-2.40.2/./gobject/gsignal.c:3363
#6 0x0000000000793faf in gimp_image_undo_event (image=0xdbaba0,
event=GIMP_UNDO_EVENT_UNDO_PUSHED, undo=0x2ad30f0) at gimpimage.c:2434
#7 0x00000000007b77ec in gimp_image_undo_group_end (image=0xdbaba0)
at gimpimage-undo.c:353
#8 0x000000000082db35 in gimp_paint_core_finish (core=0x17ad4e0,
drawable=0x1e2e830, push_undo=1) at gimppaintcore.c:473
#1-5はglibの関数である。これらはプログラム中でsignal発信(イベント発火のようなもの)が行われた際に裏で動く関数である。シグナルを発 信しているのは#6のgimp_image_undo_eventであるがこれはこれまでにも見たことのある関数で、やっていることはシグナルの発信だけ である。見るべきは#8で、これが毎度エアブラシで線を描写してマウスをあげた瞬間に呼び出される関数らしい。その中で#7のgimp_image_undo_group_endは以下の文脈で呼び出されている。
if (push_undo)
{
gimp_image_undo_group_start (image, GIMP_UNDO_GROUP_PAINT, core->undo_desc);
GIMP_PAINT_CORE_GET_CLASS (core)->push_undo (core, image, NULL);
gimp_drawable_push_undo (drawable, NULL,
core->x1, core->y1,
core->x2 - core->x1, core->y2 - core->y1,
core->undo_tiles,
TRUE);
gimp_image_undo_group_end (image);
}
この文脈から察するにif(push_undo) は「もし履歴スタックに追加すべき描写作業があれば」という条件を示し、ifの中身はエアブラシで行った描写を履歴スタックに追加する操作であると思われる。そこでgimp_image_undo_group_startの中身を見てみることにした。gimp_image_undo_group_startの中で見るべきであろう記述として以下のものを見つけた。
gimp_image_undo_free_redo (image);
undo_group = gimp_undo_stack_new (image);
gimp_object_set_name (GIMP_OBJECT (undo_group), name);
GIMP_UNDO (undo_group)->undo_type = undo_type;
GIMP_UNDO (undo_group)->dirty_mask = dirty_mask;
gimp_undo_stack_push_undo (private->undo_stack, GIMP_UNDO (undo_group));
free_redoはredoスタック(「戻る」の逆の履歴のスタック)を空にする処理に関係しているのではないか、gimp_undo_stack_newは今行った描写作業のために新たに履歴データを作成する処理ではないか、gimp_undo_stack_push_undoはundoスタック(履歴の「戻る」に該当する配列)に今行った描写作業を追加する処理ではないかなどと推測できる。gimp_image_undo_free_redoの内容は以下のようになっていた。
gimp_image_undo_free_redo (GimpImage *image)
{
GimpImagePrivate *private = GIMP_IMAGE_GET_PRIVATE (image);
GimpContainer *container = private->redo_stack->undos;
if (gimp_container_is_empty (container)) return;
while (gimp_container_get_n_children (container) > 0)
{
GimpUndo *freed = gimp_undo_stack_free_bottom(private->redo_stack,GIMP_UNDO_MODE_REDO);
gimp_image_undo_event (image, GIMP_UNDO_EVENT_REDO_EXPIRED, freed);
g_object_unref (freed);
}
見るからにredoスタックのクリアを行っている。スタックの要素を一つ削除することをスタックの要素数が0になるまでwhile文で行っていることがわかる。次にundoスタックに要素を追加していそうなgimp_undo_stack_push_undo(private->undo_stack, GIMP_UNDO (undo_group)); の内容を見てみる。これは意外に単純で内容は実質次の一行だけだった。
gimp_container_addのソースコードは若干複雑であるが、やっていることはcontainer(スタック/配列)に要素を追加することだろうと名前から推測できる。実際にこの関数をgrepでgimpの全コードを対象に検索すると非常にたくさんヒットする。つまりこれはundoスタックに限らず一般的な配列あるいはスタックに要素を追加する目的でgimpでよく用いられる関数なのだろうと推察できる。とにかくここが履歴スタックに新たな作業を登録するコード(のひとつ)であることは間違いない。
どこにコードを追加する?
さてgimp_container_addはgimp_undo_stack_push_undoの中で呼ばれているが、そこまでのまでの流れは
#0 gimp_undo_stack_push_undo (stack=0x283c080, undo=0x7fffc8005e00)
at gimpundostack.c:147
#1 0x00000000007b75eb in gimp_image_undo_group_start (image=0xdbaba0,
undo_type=GIMP_UNDO_GROUP_PAINT,
name=0x175ee70 "エアブラシで描画") at gimpimage-undo.c:315
#2 0x000000000082da96 in gimp_paint_core_finish (core=0x17b5820,
drawable=0x1e349a0, push_undo=1) at gimppaintcore.c:462
のようになっている。我々の目標は「履歴スタックが一定以上の要素数にならないようにすること」であるからコードの変更は「履歴スタックに新たな要素が追加されるごとに、スタック中の要素数を数え、一定数を超えてたら最も古いものを削除する」となる。つまり「履歴スタックに新たな要素を追加するコードの場所」を捉えることがさしあたっての目標であり、すでに述べたようにそのようなコードのひとつを見つけたわけだが、具体的にどこに新しいコードを挿入するかが問題となる。最有力候補はもちろん関数gimp_undo_stack_push_undoの中で、
を呼び出した直後ということになるが、必ずしもそれでうまく行くとは限らない。なぜならgimp_undo_stack_push_undoが履歴スタックへの作業内容追加の際に必ず呼ばれる関数であり、かつ、それ以外の目的では呼ばれないという保証がないからである。実際この関数にブレイクポイントを設置してエアブラシの描写をしてみると何と一回の描写で3回呼ばれることがわかる。
一回目はgimp_image_undo_group_start内で呼ばれる。
#0 gimp_undo_stack_push_undo (stack=0x283c080, undo=0x307ea60)
at gimpundostack.c:147
#1 0x00000000007b75eb in gimp_image_undo_group_start (image=0xdbaba0,
undo_type=GIMP_UNDO_GROUP_PAINT,
name=0x175ee70 "エアブラシで描画") at gimpimage-undo.c:315
二回目はgimp_image_undo_push内で呼ばれる。
#0 gimp_undo_stack_push_undo (stack=0x307ea60, undo=0x7fffec3baec0)
at gimpundostack.c:147
#1 0x00000000007b7bdb in gimp_image_undo_push (image=0xdbaba0,
object_type=41772736, undo_type=GIMP_UNDO_PAINT,
name=0x7ffff7fbdbe1 "描画", dirty_mask=GIMP_DIRTY_NONE)
at gimpimage-undo.c:438
#2 0x000000000082d01d in gimp_paint_core_real_push_undo (core=0x17b5820,
image=0xdbaba0, undo_desc=0x0) at gimppaintcore.c:296
#3 0x000000000082dab6 in gimp_paint_core_finish (core=0x17b5820,
drawable=0x1e349a0, push_undo=1) at gimppaintcore.c:465
三回目の呼び出しも以下のようにgimp_image_undo_push内で呼ばれる。
#0 gimp_undo_stack_push_undo (stack=0x307ea60, undo=0x30788a0)
at gimpundostack.c:147
#1 0x00000000007b7bdb in gimp_image_undo_push (image=0xdbaba0,
object_type=42552320, undo_type=GIMP_UNDO_DRAWABLE,
name=0x7ffff7fbd8ca "レイヤー/チャンネル", dirty_mask=80)
at gimpimage-undo.c:438
#2 0x00000000007b8f61 in gimp_image_undo_push_drawable (image=0xdbaba0,
undo_desc=0x0, drawable=0x1e349a0, tiles=0x2e63600, sparse=1, x=382,
y=95, width=31, height=207) at gimpimage-undo-push.c:241
#3 0x000000000076ad8a in gimp_drawable_real_push_undo (drawable=0x1e349a0,
undo_desc=0x0, tiles=0x2e63600, sparse=1, x=382, y=95, width=31,
height=207) at gimpdrawable.c:910
#4 0x000000000076d4be in gimp_drawable_push_undo (drawable=0x1e349a0,
undo_desc=0x0, x=382, y=95, width=31, height=207, tiles=0x2e63600,
sparse=1) at gimpdrawable.c:1624
#5 0x000000000082db29 in gimp_paint_core_finish (core=0x17b5820,
drawable=0x1e349a0, push_undo=1) at gimppaintcore.c:467
上記のバックトレースを見ればわかるように、2回目、3回目とも大元は描写終了時に呼ばれる関数gimp_paint_core_finishであるが、 gimp_image_undo_pushに至る過程はいくつか関数をはさんでおり若干複雑である。gimp_paint_core_real_push_undo関数は初めて見る ものだが、実は先ほどgimp_paint_core_finishの中で見た以下のコードの赤字の部分がこの関数の呼び出しに相当する。
if (push_undo)
{
gimp_image_undo_group_start (image, GIMP_UNDO_GROUP_PAINT, core->undo_desc);
GIMP_PAINT_CORE_GET_CLASS (core)->push_undo (core, image, NULL);
gimp_drawable_push_undo (drawable, NULL,
core->x1, core->y1,
core->x2 - core->x1, core->y2 - core->y1,
core->undo_tiles,
TRUE);
gimp_image_undo_group_end (image);
}
3回目の呼び出しはその下のgimp_drawable_push_undoから始まるフロー内にある。
つまりif(push_undo)内の最初の3つの関数呼び出しは全て内部でgimp_undo_stack_push_undo の呼び出しにつながっているのである。エアブラシで線を一本描いた時に新しくできる作業履歴は一つのはずなので、gimp_undo_stack_push_undo は作業履歴追加以外の目的のために2回呼び出されている可能性が高いということになる。ここでバックトレースの内容を詳しく見てみると、gimp_undo_stack_push_undoに与えられる引数の違いに気づく。
1回目 : #0 gimp_undo_stack_push_undo (stack=0x283c080, undo=0x307ea60)
2回目 : #0 gimp_undo_stack_push_undo (stack=0x307ea60, undo=0x7fffec3baec0)
3回目 : #0 gimp_undo_stack_push_undo (stack=0x307ea60, undo=0x30788a0)
gimp_undo_stack_push_undoとはスタックとundoを引数に与えられ、スタックにundoを追加する関数であるが、1回目は2,3回目とスタックもundoも異なっており、さらに1回目のundoが2,3回目のスタックになっているのである。そこで2,3回目の呼び出しに係る関数を詳しく見ていくと履歴スタックの要素がさらにスタックになっていることに気づく。つまり、一つの作業履歴は複数のさらに小さな単位の作業内容からなる。例えばエアブラシで線を一本描くという一つの作業は、「描写」と「レイヤー/チャネルの操作」という2つのより小さな単位の作業から成っており、1回目のgimp_undo_stack_push_undo呼び出しでは、「エアブラシで線を描く」という作業が履歴スタックに追加され、2,3回目の呼び出しでは「エアブラシで線を描く」という作業のスタックに「描写」と「レイヤー/チャネルの操作」という2つの作業が追加されるというわけである。
というわけで、gimp_undo_stack_push_undoは履歴スタックへの新たな履歴の追加以外の処理にも使われるため、この関数の中に目的のコードを挿入するわけには行かない。ではどこへ挿入すればいいかといえば、履歴スタックへの新たな履歴要素追加が目的でgimp_undo_stack_push_undoを呼び出している箇所を探し、gimp_undo_stack_push_undo呼び出しの直後に挿入する必要がある。エアブラシの例で言えばgimp_image_undo_group_start内での呼び出しがそれに該当するのでその直後に挿入すれば良い。そこだけかと言えば、ややこしいことにそういうわけでもない。というのはいくつかのペイント作業(例えば文字入力)はエアブラシでの描写のように複数の小さな単位の作業から成っている「グループ的作業」ではなく、単一の作業と見なされている。それらの作業の履歴登録はgimp_image_undo_group_startではなく、gimp_image_undo_pushからのgimp_undo_stack_push_undo呼び出しで行われる。ただしこのgimp_undo_stack_push_undo呼び出しはエアブラシ描写時のの2,3回目の呼び出しとは違う行である。以下はgimp_undo_stack_push_undoの該当部分のコードである。
404 if (private->pushing_undo_group == GIMP_UNDO_GROUP_NONE)
405 {
406 gimp_undo_stack_push_undo (private->undo_stack, undo);
408 gimp_image_undo_event (image, GIMP_UNDO_EVENT_UNDO_PUSHED, undo);
410 gimp_image_undo_free_space (image);
412 /* freeing undo space may have freed the newly pushed undo */
413 if (gimp_undo_stack_peek (private->undo_stack) == undo)
414 return undo;
415 }
416 else {
418 GimpUndoStack *undo_group;
420 undo_group = GIMP_UNDO_STACK (gimp_undo_stack_peek (private->undo_stack));
422 gimp_undo_stack_push_undo (undo_group, undo);
424 return undo;
425 }
青字が履歴スタックへの新たな作業の登録であり、赤字がすでに履歴スタックへ登録されたグループ的作業へ小さな単位の作業を追加するものである。
結局目的のコードは、gimp_image_undo_group_start内のgimp_undo_stack_push_undoの直後と、上記青字の関数呼び出しの直後の二カ所に挿入すれば良いことがわかった。
コードの挿入
以下のコードを挿入した。
int stacksize = gimp_undo_stack_get_depth(private->undo_stack);//履歴スタックにある要素をカウント
if(stacksize > 5){ //要素数が5より多ければ(仮に5とする。実用上はもっと多くていい)
GimpUndo* undo = GIMP_UNDO (gimp_container_get_last_child (private->undo_stack->undos)); //一番古い要素を取得
gimp_container_remove(private->undo_stack->undos, GIMP_OBJECT (undo)); //取得した要素を削除
gimp_image_undo_event(image, GIMP_UNDO_EVENT_UNDO_EXPIRED, undo); //履歴スタックをいじったことをプログラム全体に知らせる
gimp_undo_free(undo, GIMP_UNDO_MODE_UNDO); //メモリの解放?
g_object_unref(undo); //メモリ参照の解消??
}
挿入するコードの作成に当たっては先に示したgimp_image_undo_free_redoのコードを参考にした。gimp_image_undo_free_redoはペイント作業が行われた時に、redoスタック(「やり直し」履歴のスタック)にある要素を全て削除する関数である。これをundoスタックに適用すると思えば良い。
gimp_image_undo_free_redo (GimpImage *image)
{
GimpImagePrivate *private = GIMP_IMAGE_GET_PRIVATE (image);
GimpContainer *container = private->redo_stack->undos;
if (gimp_container_is_empty (container)) return;
while (gimp_container_get_n_children (container) > 0)
{
GimpUndo *freed = gimp_undo_stack_free_bottom(private->redo_stack,GIMP_UNDO_MODE_REDO);
gimp_image_undo_event (image, GIMP_UNDO_EVENT_REDO_EXPIRED, freed);
g_object_unref (freed);
}
終章
これで実装は完了である。makeしてmake installすれば履歴が5個以上たまらないgimpが出来上がる。もちろん現実的な100個などに設定することも可能である。
我々の目標は達成された、と思った矢先に発見してしまった。↓の関数を。
533 static void
534 gimp_image_undo_free_space (GimpImage *image)
/* [previous][next][first][last][top][bottom][index][help] [+534 app/core/gimpimage-undo.c] */
535 {
536 GimpImagePrivate *private = GIMP_IMAGE_GET_PRIVATE (image);
537 GimpContainer *container;
538 gint min_undo_levels;
539 gint max_undo_levels;
540 gint64 undo_size;
541
542 container = private->undo_stack->undos;
543
544 min_undo_levels = image->gimp->config->levels_of_undo;
545 max_undo_levels = 1024; /* FIXME */
546 undo_size = image->gimp->config->undo_size;
547
548 #ifdef DEBUG_IMAGE_UNDO
549 g_printerr ("undo_steps: %d undo_bytes: %ld\n",
550 gimp_container_get_n_children (container),
551 (glong) gimp_object_get_memsize (GIMP_OBJECT (container), NULL));
552 #endif
553
554 /* keep at least min_undo_levels undo steps */
555 if (gimp_container_get_n_children (container) <= min_undo_levels)
556 return;
557
558 while *1
560 {
561 GimpUndo *freed = gimp_undo_stack_free_bottom (private->undo_stack,
562 GIMP_UNDO_MODE_UNDO);
563
564 #ifdef DEBUG_IMAGE_UNDO
565 g_printerr ("freed one step: undo_steps: %d undo_bytes: %ld\n",
566 gimp_container_get_n_children (container),
567 (glong) gimp_object_get_memsize (GIMP_OBJECT (container),
568 NULL));
569 #endif
570
571 gimp_image_undo_event (image, GIMP_UNDO_EVENT_UNDO_EXPIRED, freed);
572
573 g_object_unref (freed);
574
575 if (gimp_container_get_n_children (container) <= min_undo_levels)
576 return;
577 }
578 }
発見の経緯はよく覚えていないが、恐らく同じソースファイルの近い場所を見ている時に偶然見つけたとかだったと思う。この関数が何をしているかといえば、履歴スタックの要素数とメモリサイズにそれぞれ上限を設け、どちらか一方でも上限を超えれば古い履歴を削除する。よく見るとこの関数はペイント作業実行後に必ず呼び出され、メモリの過度な消費を抑えるためのものだった。さらにここで使われる履歴に割り当てられるメモリの上限はGimpの設定画面からユーザーが設定可能だったのである。
今回の課題を通じてこの設定画面の存在を知りました。