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 に食わせたり出来ないので有益では無いですが使う側のコードは幾分減らせるかと思いました。