Windows の STDIN を非同期で読み込む。
2010-12-22
こんにちわ。mattn です。
先日、Maki_Daisuke さんからお題を頂いた件の回答です。
https://gist.github.com/742977
use AnyEvent::Handle; my $c = AE::cv; my $out = AnyEvent::Handle->new(fh => \*STDOUT); my $in; $in = AnyEvent::Handle->new( fh => \*STDIN, on_read => sub{ my $hdl = shift; $out->push_write($hdl->rbuf . "\n"); $hdl->rbuf = ""; } ); $out->push_write("Start\n"); $c->recv;
これを Windows で実行してもウンともスンとも行かないという現象。色々調べている内に Windows の標準入力は ReadFile 系非同期読み込みではちゃんと扱えない事が分かりました。正しく読み取るには ReadConsole という API を使う必要があります。
またコンソールはデフォルトでライン入力になっているので、そのあたりのオプションも変えながら読み取らないといけません。
まずここまでをコードで
sub STD_INPUT_HANDLE () { 0xfffffff6 } sub FILE_TYPE_CHAR () { 0x0002 } sub FILE_TYPE_PIPE () { 0x0003 } sub ENABLE_PROCESS_INPUT () { 0x0001 } sub ENABLE_LINE_INPUT () { 0x0002 } sub ENABLE_ECHO_INPUT () { 0x0004 } my $GetStdHandle = Win32::API->new('kernel32.dll', 'GetStdHandle', 'N', 'N', ) or die ": $^E"; my $GetFileType = Win32::API->new('kernel32.dll', 'GetFileType', 'N', 'N', ) or die ": $^E"; my $_kbhit = Win32::API->new('msvcrt.dll', '_kbhit', '', 'I', ) or die ": $^E"; my $ReadConsole = Win32::API->new('kernel32.dll', 'ReadConsoleA', 'NPNPP', 'N', ) or die ": $^E"; my $GetConsoleMode = Win32::API->new('kernel32.dll', 'GetConsoleMode', 'NP', 'I', ) or die ": $^E"; my $SetConsoleMode = Win32::API->new('kernel32.dll', 'SetConsoleMode', 'NN', 'I', ) or die ": $^E"; my $handle = $GetStdHandle->Call(STD_INPUT_HANDLE); if ($GetFileType->Call($handle) eq FILE_TYPE_CHAR) { my $mode = 0; $GetConsoleMode->Call($handle, \$mode); $SetConsoleMode->Call( $handle, $mode & ~ENABLE_LINE_INPUT & ~ENABLE_ECHO_INPUT | ENABLE_PROCESS_INPUT ); while (1) { if ($_kbhit->Call()) { my ($buf, $num) = (' ', 0); if ($ReadConsole->Call($handle, $buf, 1, \$num, 0)) { # 読み取れた } } } }
こうなります。ただ自前でポーリングをしなければなりませんので厄介ですね。こんな時は
「使うと Marc Lehmann 先生に DIS られる」
という噂の threads を使いましょう。
async { while (1) { if ($_kbhit->Call()) { my ($buf, $num) = (' ', 0); if ($ReadConsole->Call($handle, $buf, 1, \$num, 0)) { # 読み取れた } } } }->detach;
さぁこれを後はメインスレッド側で使えるようにしたいのですが、何を使うかでハマりました。結果でいうと、AnyEvent::Handle は fileno を使って処理するので tie や capture 的な物を使っても実現出来ませんでした。実際 tie して GETC もしくは READ にフックしてみましたが、いっこうに呼ばれませんでした。
せっかくここまで解析したのでもったいないと思ったので getc だけでも使えるようにしましょう。
package Win32::Async::Stdin; use strict; use warnings; use threads; use threads::shared; use Win32::API; use IO::Scalar; our $VERSION = '0.01'; sub STD_INPUT_HANDLE () { 0xfffffff6 } sub FILE_TYPE_CHAR () { 0x0002 } sub FILE_TYPE_PIPE () { 0x0003 } sub ENABLE_PROCESS_INPUT () { 0x0001 } sub ENABLE_LINE_INPUT () { 0x0002 } sub ENABLE_ECHO_INPUT () { 0x0004 } my $GetStdHandle = Win32::API->new('kernel32.dll', 'GetStdHandle', 'N', 'N', ) or die ": $^E"; my $GetFileType = Win32::API->new('kernel32.dll', 'GetFileType', 'N', 'N', ) or die ": $^E"; my $_kbhit = Win32::API->new('msvcrt.dll', '_kbhit', '', 'I', ) or die ": $^E"; my $ReadConsole = Win32::API->new('kernel32.dll', 'ReadConsoleA', 'NPNPP', 'N', ) or die ": $^E"; my $GetConsoleMode = Win32::API->new('kernel32.dll', 'GetConsoleMode', 'NP', 'I', ) or die ": $^E"; my $SetConsoleMode = Win32::API->new('kernel32.dll', 'SetConsoleMode', 'NN', 'I', ) or die ": $^E"; my $inputs = &share([]); tie *STDIN, 'Win32::Async::Tied::Stdin', $inputs; my $handle = $GetStdHandle->Call(STD_INPUT_HANDLE); if ($GetFileType->Call($handle) eq FILE_TYPE_CHAR) { my $mode = 0; $GetConsoleMode->Call($handle, \$mode); $SetConsoleMode->Call( $handle, $mode & ~ENABLE_LINE_INPUT & ~ENABLE_ECHO_INPUT | ENABLE_PROCESS_INPUT ); async { while (1) { if ($_kbhit->Call()) { my ($buf, $num) = (' ', 0); if ($ReadConsole->Call($handle, $buf, 1, \$num, 0)) { push @$inputs, $buf; } } } }->detach; } package Win32::Async::Tied::Stdin; use strict; sub TIEHANDLE { my ($class, $ref) = @_; bless { ref => $ref }, $class; } sub GETC { my $self = shift; shift @{$self->{ref}}; } 1;
こんな感じに配列のリファレンスをスレッド間共有してみました。使う側は
use Win32::Async::Stdin; while () { my $a = getc(STDIN); warn $a if $a; # 高速ループするので sleep いれてね! }
こんな感じでしょうか。あまり汎用性もなく、AnyEvent に食わせたり出来ないので有益では無いですが使う側のコードは幾分減らせるかと思いました。