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 ライブラリを使うようにした。

これで、

わあい。

  1. GNU Readline ライブラリ自体ではなくて、それと Ruby の橋渡しをする「拡張ライブラリ」。Readline モジュールを定義している。 

  2. ここで1文字消えずに、ひらがな1文字を構成する3バイトのうち右端の1バイトだけが欠けて、プログラムに壊れた文字列が入るということもある。