Windows2000コンソールアプリでのCntl-C割込みの処理

LinuxやFreeBSDといったUNIXで、コンソールプログラムでユーザから入力された Cntl+CやTERMといったシグナルは、signal(2)でシグナルハンドラを指定できる。

しかしWindows2000以降のWindowsでは、MSDNのsignal関数のページによると、 コンソールプログラムでのSIGINT割込みはサポートされないとある。 そこで、Windows2000以降におけるSIGINTの代替になるものを探してみたところ SetConsoleCtrlHandlerという関数があった。サンプルコードを以下に示す。


#include <windows.h>
#include <wincon.h>
#include <iostream>

// ハンドラ関数
BOOL handler(DWORD ctrlChar){
  if(CTRL_C_EVENT == ctrlChar){
    std::cout<< "Ctrl+C pressed"<< std::endl;
    return FALSE;
  }

int main(void){
  ::SetConsoleCtrlHandler(handler, TRUE);

  while(true)
    ::Sleep(100);

  return 0;
}

やっていることは、基本的にsignal(2)と変わらない。::SetConsoleCtrlHandlerは、第二引数がTRUEであれば、第一引数で指定したハンドラを追加し、FALSEなら削除する。

ハンドラ

ハンドラは、以下のプロトタイプを持つ。


BOOL WINAPI HandlerRoutine(DWORD dwCtrlType);

dwCtrlTypeには、Table.1に示す割込みの種類が代入される。

Table.1: ハンドラの引数とその意味
dwCtrlType意味
CTRL_C_EVENTCntl+Cを受け取った
CTRL_BREAK_EVENTCntl+BREAKを受け取った
CTRL_CLOSE_EVENTユーザがコンソールを閉じた
CTRL_LOGOFF_EVENTユーザがログオフしようとして、システムが全コンソールプロセスに送信したシグナルを受け取った
CTRL_SHUTDOWN_EVENTユーザがシャットダウンしようとして、システムが全コンソールプロセスに送信したシグナルを受け取った

戻り値としてFALSEを返すと、次のハンドラが呼ばれる。すなわちプロセスが強制終了される。なのでシグナルを処理するだけで終了する必要がない時は、TRUEを返す。

終了処理

実際、この仕組みを使うときは終了処理がしたいときだろう。例えばハードウェアのハンドルやネットワーク接続閉じるときだろう。クラスで隠蔽されたハードウェアやネットワーク接続のインスタンスをグローバル変数にしておけば問題ないが、あんまりやりたくない。そういった時はCommandパターンを使うとよいだろう。終了処理が必要なクラスは、Terminatorクラスを継承しておく。Terminatorクラスはterminateという純粋仮想関数だけを持つクラスで、terminate関数は各クラスでオーバーライドし、終了処理を記述する。 std::list<Terminator*>型の変数をグローバル変数にしておき、Terminatorのサブクラスのインスタンスを、このグローバル変数にpushしておく。 あとはハンドラで、listに入っている全ての要素のterminate関数を呼ぶことで、必要な終了処理を行うことができる。サンプルを以下に示す。


#include <list>

class Terminator{
public:
  void terminate(void) = 0;
};

typedef std::list<Terminator*> terminator_list;
terminator_list gTerminators;  // Terminatorポインタのリスト

// 何か終了処理が必要なデバイス
class Device : public Terminator{
private:
  HANDLE hDevice;
  ....

public:
  // terminate関数で終了処理を記述
  void terminate(void){::CloseHandle(hDevice);}
  ....
};

// シグナルハンドラ
BOOL WINAPI SignalHandler(DWORD dwCtrlType){

  // gTerminatorsに入っている全ての要素のterminate関数を呼ぶ
  for(terminator_list::iterator i=gTerminators.begin();
      i != gTerminators.end(); i++)
  {
    (*i)->terminate();
  }
}


void main(void){
  // デバイスをインスタンス化
  Device dev1;
  Device dev2;
  Device dev3;

  // インスタンスのポインタをリストに入れる
  gTerminators.push_back(&dev1);
  gTerminators.push_back(&dev2);
  gTerminators.push_back(&dev3);

  // シグナルハンドラを追加
  ::SetConsoleCtrlHandler(SignalHandler, TRUE);
  
  // デバイスを使って、割込みが入るまで繰り返し処理
  while(true){
    ....
    ::Sleep(100);
  }
}