Macのrubyのreadlineに不具合がある!?(5)

Macのrubyのreadlineに不具合がある!?(4)の続き。

Ruby の Readline ライブラリをデバッグする。
問題は以下。
http://redmine.ruby-lang.org/issues/show/212

Ruby の Readline ライブラリが GNU Readline を使用するようにしてコンパイルされた場合、以下のように振る舞う。

require "readline"

puts Readline::VERSION
Readline::HISTORY.push("1", "2", "3")
Readline::HISTORY.each { |i| puts i }
# 1、2、3を順番に出力する。

しかし、Ruby の Readline ライブラリが Editline Library を使用するようにしてコンパイルされた場合、以下のように振る舞う。

require "readline"

puts Readline::VERSION
Readline::HISTORY.push("1", "2", "3")
Readline::HISTORY.each { |i| puts i }
# 1は出力せずに、2、3を順番に出力する。

Readline::HISTORY.each は、ヒストリの最初から最後まで histroy_get で取り出しブロックに渡す、というのを繰り返す。

static VALUE
hist_each(self)
    VALUE self;
{
    HIST_ENTRY *entry;
    int i;

    rb_secure(4);
    for (i = 0; i < history_length; i++) {
        entry = history_get(history_base + i);
        if (entry == NULL)
            break;
	rb_yield(rb_tainted_str_new2(entry->line));
    }
    return self;
}

これが期待通りに動作していない。libedit のヒストリには魔物がすんでいるようだ。
C でコードを書いて、GNU Readline と libedit の動作を確認してみよう。

#include <stdio.h>

#ifdef HAVE_EDITLINE_READLINE_H
#include <editline/readline.h>
#else
#include <readline/readline.h>
#include <readline/history.h>
#endif /* HAVE_EDITLINE_READLINE_H */

int main(int argc, char **argv)
{
  HIST_ENTRY *entry;
  int i;
  
  using_history();

  /* puts Readline::VERSION */
  printf("%s\n", rl_library_version);

  /* Readline::HISTORY.push("1", "2", "3") */
  add_history("1");
  add_history("2");
  add_history("3");

  /* Readline::HISTORY.each { |i| puts i } */
  for (i = 0; i < history_length; i++) {
    entry = history_get(history_base + i);
    if (entry == NULL) {
      break;
    }
    printf("%s\n", entry->line);
  }
  
  return 0;
}

コンパイル

$ gcc no212_on-macosx.c -o no212_on-macosx.gnu_readline -L/opt/local/lib -lreadline
$ gcc no212_on-macosx.c -DHAVE_EDITLINE_HEADER_H -o no212_on-macosx.editline -L/Developer/SDKs/MacOSX10.5.sdk/usr/lib -lreadline

実行。

$ ./no212_on-macosx.gnu_readline
5.2
1
2
3

$ ./no212_on-macosx.editline 
EditLine wrapper
2
3

想定通りの結果になった。これから回避策を探そう。

調査の結果、 history_base の扱いが GNU Readline と libedit では違うことが分かった。
GNU Readline の history_base から 1 を引いたものが libedit の history_base のようだ。
この想定が正しいならば、以下のパッチで問題が回避できる。

Index: ext/readline/readline.c
===================================================================
--- ext/readline/readline.c	(revision 18037)
+++ ext/readline/readline.c	(working copy)
@@ -511,6 +511,12 @@
 #endif /* HAVE_RL_FILENAME_QUOTE_CHARACTERS */
 }
 
+#ifdef HAVE_EDITLINE_READLINE_H
+#define HISTORY_BASE (history_base - 1)
+#else
+#define HISTORY_BASE (history_base)
+#endif
+
 static VALUE
 hist_to_s(self)
     VALUE self;
@@ -531,7 +537,7 @@
     if (i < 0) {
         i += history_length;
     }
-    entry = history_get(history_base + i);
+    entry = history_get(HISTORY_BASE + i);
     if (entry == NULL) {
 	rb_raise(rb_eIndexError, "invalid index");
     }
@@ -649,7 +655,7 @@
 
     rb_secure(4);
     for (i = 0; i < history_length; i++) {
-        entry = history_get(history_base + i);
+        entry = history_get(HISTORY_BASE + i);
         if (entry == NULL)
             break;
 	rb_yield(rb_tainted_str_new2(entry->line));

パッチを当てた後、動作を確認する。

$ cd /path/to/ruby_1_8
$ ./configure --prefix=${HOME}/local --program-suffix=18trunk --enable-libedit
$ ~/local/bin/ruby18trunk -rreadline -e 'puts Readline::VERSION; Readline::HISTORY.push("1", "2", "3"); Readline::HISTORY.each { |i| puts i }'
EditLine wrapper
1
2
3

よっしゃ。これでうまくいった。
とりあえず、ITSやruby-dev MLに現状やパッチを報告する。

http://redmine.ruby-lang.org/issues/show/212

今後は、configure の --enable-libedit を指定しなくてもいいようにしたい。
コンパイル時に GNU Readline と libedit を判定したいが方法が分からない。
とりあえず、実行時に判定することにしよう。