経緯
Vim とか触っていると、一文字入力するたびに何かアクションが発生したりするので、どういう仕組みで動いているんだろうという技術的興味があったので、ソースコードリーディングをしてみた。
知識レベル
getc() とか gets() とかで 行単位の入力ができるのは知っているが、キー入力ごとに処理をする方法を知らない。
予想していた動作
予想というか自分の知識で簡単に考えられたのは、別スレッドで文字入力を監視してキー入力があったらメインスレッドに通知するみたいなのを想像してた。
wincons.rb を眺める
wincons.rb という Ruby製のコンソールライブラリ、一文字入力したら終了するみたいな処理をすごい昔に書いた覚えがあって、そういえば、これはどうやって実現しているんだろうと気になりました。
ソースは以下からゲットできます。
http://texcell.co.jp/ruby/Lib/winconsole.html
見てみると、win32apiのPeekConsoleInputWという関数を呼び出すことで実現しているよう。
http://texcell.co.jp/ruby/PLC/rubyc24.html
↑ここらへんの例を見ると、ループの中で常にキー入力チェックをして、q
が来たら終了するようにしているみたいです。
Vim のソースコードを眺める
wincons.rb のやり方が本当に正しいのか?という疑問もあったので、Vimのソースコードも眺めてみることにしました。
ソースコードはgithubから手に入れることができます。
https://github.com/vim/vim
先ほど出てきたPeekConsoleInputWが使われている箇所を検索すると、src/os_win32.c
のread_console_input
という関数の中で使われていることがわかりました。
このread_console_input
というのが結構いろんなところから呼ばれているようで、どれがキーボード入力に直結するのかが私にはわからなかったので、あきらめました。
※ mch_inchar
=> tgetch
あたりがあやしいかなと踏んでいます。mch_inchr
はui_inchar
から呼ばれていて、ui_inchar
はinchar
関数のforループの中で呼ばれています。forループはui_incharからキー入力がゲットできるまで回り続けるので、ここら辺がキー入力のキモなのかなと思いますが、これだと、inchar関数の中でループしてほかの処理が動かなくなってしまうので、inchar
を呼んでいる親元でスレッドになっているんだと思いますが、憶測です。
readline のソースコードを眺める
rubyに付属しているreadlineはTABキーを押すと補完用のProcを呼び出します。おそらくTABキーが押されたらProcを呼び出すといった処理が入っているに違いないと推測。
まずは、rubyに付属のreadlineをコードリーディング
rubyのソースは以下から見れます。
https://github.com/ruby/ruby
readlineのソースはext/readline
あたりにあります。
色々見た結果、Procを呼び出す関数はreadline_attempted_completion_function
ぽいことがわかりました。その関数はrl_attempted_completion_function
にバインドされています。
rl_attempted_completion_function
はrubyのコードではなく、GNU readlineのコードの中にあるため、そちらを読み解きます。
GNU readlineのソースは以下から見れます。
https://github.com/JuliaLang/readline
正確にはGNU readlineの本当のソースではありませんが、今回の理解を進める分には問題ないと思います。
rl_attempted_completion_function
はcomplete.c
の中にありますが、関数をさかのぼってもそれらしいところに到達できませんでした。
readlineの使い方を調べる
ここら辺を見てみるとreadline
がルートっぽいので、ルートから順を追って調べることにしました。
http://d.hatena.ne.jp/cocoatomo/20071112/1194855728
readline()
はreadline.c
にあります。その関数の中をどんどん辿っていくと、input.c
のrl_gather_tyi()
の中にあるkbhit()
がキー入力のキモとなる部分のようです。
kbhit()とは?
ここを見てみると
http://tricky-code.net/mine/c/mc07kbhit.php
kbhit関数とは、何かキーが押された場合0以外の値を返し、
何もキーが押されていない場合は0を返す関数です。
とのこと、これ自体がどのキーが押されたかを判定するものではないようです。
実際とっているのはrl_getc(stream)
の中で、read()
関数を呼んで、引数として渡されているIOから一文字読んでいるようです。
それをループでぐるぐる回すことで、常にキー入力をチェックして1文字単位でチェックしてるようです。
スレッドなんてなかった。
まとめ
readlineはスレッドを使わずにループでキー入力をチェックしている。
キー入力があったかどうかの判定にはkbhit()
を使っている
文字取得にはread()
を使って1文字ずつ取得している。