俺と Win32 コンソール(2):半分冗談で msvc/mingw32 用のオレオレ getch を模索してみた

いわゆる「澤」問題。よりくわしく言うと「getch でカーソルキーとか押すと 0xE0 のプレフィックスがついちゃって DBCS 入力にものすごくさしさわりがってゆうかこれじゃカーソルだか 2byte 文字だか判別しようがねーだろゴルァ」問題。(詳しいの?それって詳しいの?)。
ちなみにプレフィクスが 0xE0 になるのって実はマイクロソフトのランタイムだけっぽいのさ…そもそも <conio.h> の関数って dos 時代に作られた dos 特化のコンソール入出力関数なのに、この件に限って言えばマイクロソフトだけが dos 時代の Microsoft C/C++ と挙動が違うんですよ(dos 時代の _getch はカーソルキー押してもプレフィクスは 0xE0 にならない。0 のまま)。ちなみに Borland とかの他社製は dos 時代の仕様に合わせてたりする。テンキー統合のカーソルキーと独立のカーソルキーを別個に判別したいという需要があったのかもしれないけど、それならそれ相応の関数を別に作っても良かったじゃん…。

つまり何が言いたいかと申しますと、MS の _getch は DBCS 入力のことなんてなーんも考えておらんということです。ここで採りうるソリューション:

  1. _getch をあきらめる。Win32 のコンソールAPI を自前で使う(_getch 代替関数の作成も含めて)
  2. Microsoftmingw をあきらめる。Borland, DigitalMars, OpenWatcom の検討
  3. DBCS をあきらめる(その1)。コンソールで日本語なんて入力しませんよJK
  4. DBCS をあきらめる(その2)。_getwch で Unicode 受け(さようなら Win9x)。

いちばん正しい道は 1. だと思うのですが、個人的な趣味により、もっとも変態的なアプローチを考えてみました。


ということでいきなりソース:

/*
  変態 getch() test その1 
  
  int __cdecl my_kludge_getch(void)
*/

#include <windows.h>
#include <conio.h>
#include <stdio.h>

int __cdecl my_kludge_getch(void);
int __cdecl my_kludge_kbhit(void);
int __cdecl my_kludge_ungetch(int);

static HMODULE hm_crtdll;
static int (__cdecl *lpfn_getch)(void);
static int (__cdecl *lpfn_getwch)(void);
static int (__cdecl *lpfn_kbhit)(void);
static int (__cdecl *lpfn_ungetch)(int);

#define COUNT_OF_CONBUF 16
static unsigned char conbuf[COUNT_OF_CONBUF];
static int in_conbuf, out_conbuf;

#define ENTER_THREAD_ATOMIC
#define LEAVE_THREAD_ATOMIC


static void load_crtdll(void)
{
  if (!hm_crtdll) {
    HMODULE hm;
    hm = LoadLibrary(TEXT("MSVCR71.dll")); /* VS2003 extra runtime DLL */
    if (hm) {
      hm_crtdll = hm;
      lpfn_getch = (int (__cdecl*)(void))GetProcAddress(hm,TEXT("_getch"));
      lpfn_getwch = (int (__cdecl*)(void))GetProcAddress(hm,TEXT("_getwch"));
      lpfn_kbhit = (int (__cdecl*)(void))GetProcAddress(hm,TEXT("_kbhit"));
      lpfn_ungetch = (int (__cdecl*)(int))GetProcAddress(hm,TEXT("_ungetch"));
    }
  }
  return;
}

static int push_conbuf(int k)
{
  int rc;
  int next_conbuf;
  
  ENTER_THREAD_ATOMIC;
  next_conbuf = in_conbuf + 1;
  if (next_conbuf == out_conbuf) {
    rc = -1; /* buffer full */
  } else {
    conbuf[in_conbuf] = (unsigned char)(k & 255);
    if (next_conbuf >= COUNT_OF_CONBUF) next_conbuf = 0;
    in_conbuf = next_conbuf;
    rc = k;
  }
  LEAVE_THREAD_ATOMIC;
  
  return rc;
}

#if 0
static int peek_conbuf(void)
{
  return (in_conbuf == out_conbuf) ? -1 : conbuf[out_conbuf];
}
#endif

static int get_conbuf(void)
{
  int k;
  if (in_conbuf == out_conbuf) k = -1;
  else {
    ENTER_THREAD_ATOMIC;
    k = conbuf[out_conbuf++];
    if (out_conbuf >= COUNT_OF_CONBUF) out_conbuf = 0;
    LEAVE_THREAD_ATOMIC;
  }
  return k;
}

static int push_wchar_into_conbuf(WCHAR wc)
{
  int pushcnt;
  if (wc == 0xe0) wc = 0; /* dike enhanced key sign. */
  if (wc >= 0x20) {
    unsigned char mbs[8];
    int i;
    pushcnt = WideCharToMultiByte(CP_OEMCP,0,&wc,1,mbs,sizeof(mbs),NULL,NULL);
    for(i=0; i<pushcnt; ++i) push_conbuf(mbs[i]);
  } else {
    pushcnt = 1;
    push_conbuf(wc);
  }
  return pushcnt;
}


int __cdecl my_kludge_getch(void)
{
  int k;
  
  if (!hm_crtdll) load_crtdll();
#if 0
  if (!lpfn_getwch) {
    return lpfn_getch ? (*lpfn_getch)() : -1;
  }
#endif
  for(;;) {
    k = get_conbuf();
    if (k != -1) break;
    k = (*lpfn_getwch)();
    if (k == -1) break;
    push_wchar_into_conbuf(k);
  }
  return k;
}


#ifdef TEST

int main(int argc, char **argv)
{
  int c;
  
  printf("Press Esc (or Ctrl-[) to exit.\n");
  
  while(1) {
    c = my_kludge_getch();
    if (c == -1 || c == 0x1b) break;
    printf("%c", c);
  }
  
  return 0;
}
#endif


_getwch でキー入力を受けて、0xe0 だったら 0 にする(0xe0 が拡張キーのプレフィクスではなく、ほんとうの文字 à という可能性もあるが、すくなくとも日本語じゃねーしというわけでひとまず黙殺)。それから MBCS に変換。msvcrt.dll には _getwch がないので、とりあえず VS2003 の dll から引っ張ってきています。

…自分でやっといて「何だかなあ」という感じではある。