FFI経由でReadlineを使ってChitに自動新着レス表示機能を付けた話
Chitは、したらば掲示 板をチャット風に使うコマンドラインプログラムである。「チャット風」とは、 ユーザーがプログラムに一行入力してエンターキーを叩くと、その行がレス として投稿されるという意味だ。PeerCast Plays Urahaku 企画でゲーム の操作コマンドを入力するのに簡便なインターフェイスが欲しかったので 作った。
Chit は Ruby で書かれており、Ruby 標準添付の Readline モジュールを使っ て行入力(および入力履歴の呼び出し)を実現していたのだが、新着レスの自動 読み込み機能は無かった。ユーザーは空行を入力して、新着レスのチェックを 自動で行う必要があった。
これは Readline モジュールの制限により技術的に実装が難しかったからだが、 「チャット風」を標榜するならば、新しい発言は自動的に表示されるべきであ ろう。
その機能を Ruby-FFI を使って、C で書かれた GNU Readline ライブラリを直接使用することで実現したのでご紹介する。
GNU Readline ライブラリ
行編集ライブラリと言えば GNU Readline ライブラ リである。事実 上 Linux のデフォルトシェルである bash で利用されており、Linux ユーザー なら知らず知らずであっても使ったことがあるはずだ。
左矢印のキーを押した時に ^[[D
のような文字が表示されるのではなく、き
ちんとカーソルが動いたり、上矢印キーで直前に打ったコマンドが呼び出せる
のは readline ライブラリのおかげである。
module Readline
Ruby には Readline モジュールが標準添付されており、これを利用すれば簡 単に readline を利用することができる。
irb (Ruby の REPL) を使って試してみよう。
$ irb
2.3.1 :001 > require 'readline'
=> false
2.3.1 :002 > Readline.readline
あいうえお
=> "あいうえお"
1行目の require 'readline'
は readline ライブラリ1をロードしている。実は、返り値の false
は、このライブラリが既に読み込まれていることを意味する。irb 自体が readline を使っているからだ(!)。
2 行目の Readline.readline
は Readline モジュールの readline 関数を呼び出している。ここでプログラムは入力待ちになって、端末から「あいうえお」と入力してエンターキーを押した結果、文字列 "あいうえお"
が readline 関数から返っている。
ここで、Ruby で一行の入力を受け取る最も単純な方法である gets 関数と比べてみよう。
2.3.1 :034 > gets
あいうえお
=> "あいうえお\n"
gets の場合は最後に改行文字(\n
と表示されている)が付く点が異なる。ま
た、この時カーソルキーを使ってカーソルを動かすことはできない。
次の例では「あいうえお」と打ってから2回バックスペースを押して、そ の後エンターを叩いている。
2.3.1 :004 > gets
あいうえ
=> "あいう\n"
プログラムに入力されたのは「あいう」と改行文字であるのに対して、画面上 の表示は「あいうえ」となっている事に注目して欲しい。あらゆる環境でこうなるとは限らないが、 ここで起こったことは、バックスペースを叩く度に実際には1文字消え ている2が、カーソルは半角分しか戻っていないために、実際に入力される文字 列と画面上の表示がずれたということで、よくある現象である。
Readlineはカーソルキーを使えるようにするだけでなく、日本語の文字列を正 しく編集するのにも一役買っているのだ。
さて、本題の Chit は Readline モジュールを利用して、ユーザーに投稿文を 入力させていたのだが、これでは困る点が出てきた。数秒ごとにサーバーにリ クエストを送って新着レスがあるかどうかチェックしたいのだが、readline 関数はユーザーから1行の入力を受け取るまでブロックするので、この間に別 の処理を挟むことはできないのだ。
Readlineの代替インターフェイス
実は GNU Readline ライブラリにはこのようなユースケースに対応した関数群 が用意されている。代替インターフェイス(Alternate Interface)と呼ばれる のがそれで、以下のC関数からなる。
void rl_callback_handler_install (const char *prompt, rl_vcpfunc_t *lhandler)
void rl_callback_read_char (void)
void rl_callback_sigcleanup (void)
void rl_callback_handler_remove (void)
やり方はこうだ。プログラムはまず入力される行を受け取るコールバック関数
を用意し、これを rl_callback_handler_install
で登録する。そして端末
の状態を調べて、入力された文字があるなら rl_callback_read_char
で
Readline に読み取ってもらう。Readline は一行の入力が完了した時にさきほ
ど登録したコールバック関数を呼び出し、プログラムはユーザーが入力した内
容を知ることができる。
問題は Ruby 標準添付の Readline モジュールからこれらの関数を使うことができないということである。そこで、 Ruby-FFIを使って Readline モジュー ルを経由せずに直接 GNU Readline ライブラリを使うようにした。
これで、 投稿文を編集しながら新着レスの表示ができるようになった。 わあい。