本を読む

読書やコンピュータなどに関するメモ

RubyのReadline.readlineで初期文字列を与える

 readlineライブラリは、行編集機能つきで文字列を入力できるようにするCのライブラリです。便利なので、多くの言語がreadlineを呼び出すライブラリをサポートしています。Rubyでは、Readlineモジュールがそうです。

 このreadlineを呼び出すときに、初期文字列を与えたいと思いました。呼び出したときに最初からデフォルトの文字列が入っていて、不要なら編集して消すというパターンです。ダイアログとかHTMLのinputとかではよくあるやりかたですね。

 が、調べてみると、Readlineモジュールはおろか、元のreadlineライブラリにもその機能はないようです。

 以下、試行錯誤してやってみた結果です。それ違うんじゃない? とか、もっといい方法あるよ! とかありましたら教えて偉い人!

PerlのTerm::ReadLine::Gnuを見てみる

 Perlにはreadlineを呼び出すモジュールがいくつかあります。そのひとつ、Term::ReadLine::Gnuモジュールでは、readline()の第2引数(REPUT)として初期文字列を与えられるようになっています。

"PREPUT" is an optional argument meaning the initial value of input.

 これがどう処理されているか、ソースを見てみます。

        $Attribs{startup_hook} = sub {
            $self->rl_insert_text($preput);
            &$saved_startup_hook
                if defined $saved_startup_hook;
        };
        $line = $self->rl_readline($prompt);

 startup_hookに、PREPUT文字列をinsertする手続を設定しているようです。調べた途中をはしょると、readlineのrl_startup_hookという変数に設定するようです。infoでは、rl_startup_hookは以下のように説明されています。

 -- Variable: rl_hook_func_t * rl_startup_hook
     If non-zero, this is the address of a function to call just before
     `readline' prints the first prompt.

 readlineを呼んでプロンプトが表示される前に最初に実行するCの関数へのポインタ、を設定するようですね。

Ruby 1.9.1で試してみる

 termtterでは、Readlineモジュールにreadlineのダイナミックリンクライブラリを読み込んでreadline関数を呼び出すメソッドを追加しています。そこで、これをまねてみます。

 Rubyのダイナミックリンク呼び出し方法は、主にRuby 1.9で使われているdl2(DL::Importer)と、主にRuby 1.8で使われているdl(DL::Importable)という2系統があるそうです。termtterでは両立するように書かれていますが、ダイナミックリンクライブラリ中の変数を参照するといったことをすると、大きく違ってしまうようです。

 動作検証のコードなので、ひとまずRuby 1.9.1で動く範囲で試してみました。たぶんIA32のLinux限定のコードだと思います。

require 'readline'
require 'dl/import'

module Readline
  module LIBREADLINE
    extend DL::Importer
    dlload('/usr/lib/libreadline.so')
    extern 'int rl_insert_text (char *)'
    RL_STARTUP_HOOK = import_symbol 'rl_startup_hook'
    STARTUP_HOOK_CALLBACK = bind('int startup_hook_callback()', :temp)
  end

  def self.insert_text(str)
    LIBREADLINE.rl_insert_text(str.to_s)
  end

  def self.set_startup_hook(&blk)
    ptr = LIBREADLINE::RL_STARTUP_HOOK
    cf = LIBREADLINE::STARTUP_HOOK_CALLBACK
    cf.bind { blk.call }
    ptr[0, DL::SIZEOF_VOIDP] = [cf.to_i].pack('I!')
  end
end

Readline.set_startup_hook {
  Readline.insert_text('default text')
}
str = Readline.readline('> ')
p str

 rl_startup_hookのアドレスは、import_symbolで取り出せました(たぶん)。一方、DL::Importer#bindでDL::Functionオブジェクトを作り、DL::Function#bindで実行するRubyのブロックを割り当てました。最後に、DL::Functionオブジェクトのアドレスを、rl_startup_hookに書き込みました。

 これを実行すると、初期化文字列つきでReadline.readlineが呼ばれたようです。

$ ruby preput.rb
> default text

 めでたしめでたし(たぶん)。

まとめ

  • readlineにpreput文字列を与えるには、rl_startup_hookに設定したCの関数からrl_insert_textする
  • Ruby 1.9.1では、ダイナミックリンクライブラリを読み込むのにDL::Importerを使う(らしい)
  • ダイナミックリンクライブラリ中の変数名シンボルからアドレスを取り出すには、import_symbolを使う(たぶん)
  • Rubyの処理からCの関数ポインタを作るには、DL::Functionオブジェクトを作ってブロックをbindする(たぶん)

変更2009-05-31:set_startup_hookをブロック引数を取る形に変更

コメント

コメントの投稿

管理者にだけ表示を許可する

トラックバック

http://emasaka.blog65.fc2.com/tb.php/618-703a218e

 | HOME | 

Categories

Recent Entries

Recent Comments

Recent Trackbacks

Appendix

emasaka

emasaka

フリーター。
連絡先はこのへん

Monthly


FC2Ad