【MFC】GetLastError 6(ERROR_INVALID_HANDLE):無効なハンドルの原因と切り分け

【MFC】GetLastError 6(ERROR_INVALID_HANDLE):無効なハンドルの原因と切り分け

ワーカースレッドの停止用に作ったイベントを、スレッドがまだ使っている間にダイアログ側でCloseHandleしてしまう。その後でSetEventを呼ぶと戻り値は0、GetLastError()は6、つまりERROR_INVALID_HANDLEです。この記事では、閉じたハンドルを使い回したこの例を土台に、ハンドルがどこで無効になったのかを切り分けます。

目次

GetLastError 6で最初に確認するもの

見る場所見る値何を疑うか
失敗したAPI戻り値まず戻り値で失敗を確定させ、それからGetLastError()を読む
エラーコード60x6ERROR_INVALID_HANDLE。ハンドル値そのものが無効
生成箇所CreateEventなどの戻り値生成に失敗した値や、未代入のまま渡していないか
破棄箇所CloseHandleの位置閉じたあとの値が、別の変数やスレッドに残っていないか
所有者生成・利用・破棄する側同じハンドルを二か所以上が閉じていないか

Microsoftのシステムエラーコード一覧でも、6(0x6)はERROR_INVALID_HANDLEです。ただし、この番号が教えてくれるのは「渡したハンドルが無効だった」ことだけで、どこで無効になったかは別問題です。生成、受け渡し、破棄のどこで壊れたかを追うことになります。

CloseHandle後の再利用でエラー6になる

閉じたあとのハンドル値は、見た目は生きていたときと同じ数値のままです。だからSetEventにそのまま渡せてしまい、実行時に初めて失敗します。SetEventは失敗すると0を返すので、その値を見た直後にエラーコードを取ります。次のコードが、検証環境で6を再現させた最小の形です。

HANDLE hEvent = ::CreateEvent(nullptr, TRUE, FALSE, nullptr);
if (hEvent == nullptr)
{
    const DWORD error = ::GetLastError();
    return;
}

::CloseHandle(hEvent);

if (!::SetEvent(hEvent))
{
    const DWORD error = ::GetLastError();
    // 検証環境では error == ERROR_INVALID_HANDLE (6)
}

// 実コードではCloseHandle直後に無効化する
hEvent = nullptr;
有効なイベントハンドルではSetEventが成功し、CloseHandle後のハンドルではGetLastErrorが6になった比較結果

CloseHandleの仕様によれば、この呼び出しはハンドルを無効にします。つまり悪いのはCloseHandleではなく、そのあとも古い値を持ち続けている側です。なお、無効な値をもう一度CloseHandleへ渡すとデバッガ実行中に例外になるため、二重クローズは再現手順に入れていません。

無効になった場所を3段階で追う

  1. 生成。ハンドルを返すAPIの戻り値を見て、成功したときだけメンバー変数へ入れます。
  2. 受け渡し。代入や引数渡し、ワーカースレッドへ渡す経路をたどり、まだ使っている最中に別の場所が閉じていないかを見ます。
  3. 破棄。CloseHandleのすぐ後で変数をnullptrへ戻し、閉じる責任を一か所に寄せます。

MFCのダイアログやドキュメントクラスが生のHANDLEをメンバーで持つときは、生成したクラスが破棄まで面倒を見る、と決めておくのが安全です。別スレッドがそのハンドルを待っているなら、スレッドを止めて待機を終えてから閉じます。順番を逆にすると、待っている側が無効なハンドルをつかんだままになります。

void CWorkerOwner::CloseStopEvent()
{
    if (m_hStopEvent != nullptr)
    {
        ::CloseHandle(m_hStopEvent);
        m_hStopEvent = nullptr;
    }
}

nullptrへの代入は、残っている別名のハンドルまで無効化するものではありません。同じ値を複数のメンバーやスレッドへコピーしている場合は、コピー元を含めて所有関係を見直します。

CloseHandleはすべてのハンドル型に使う関数ではありません。たとえばソケットはclosesocket、レジストリキーはRegCloseKeyで閉じます。生成APIのドキュメントで対応する破棄関数も確認してください。

GetLastErrorは失敗直後に保存する

SetEventは、成功時に0以外、失敗時に0を返し、詳細はGetLastErrorで取得します。エラー表示やログ出力を先に呼ばず、失敗を判定した行の直後で保存してください。

if (!::SetEvent(m_hStopEvent))
{
    const DWORD error = ::GetLastError();

    CString message;
    message.Format(_T("SetEvent failed: %lu"), error);
    m_status.SetWindowText(message);
}

GetLastErrorが返すのは、そのスレッドで最後に記録された値だけです。あいだに別のAPI呼び出しやログ出力をはさむと、6ではなく無関係な値に上書きされていることがあります。読むのは失敗を判定した行の直後、というのが結局いちばん確実です。タイミングの話は「GetLastError()は失敗直後に読む」で詳しく扱っています。

まとめ

  • GetLastError 6はERROR_INVALID_HANDLEです。
  • コード6だけでは原因箇所は決まりません。生成、受け渡し、破棄の順にたどります。
  • CloseHandle後は変数をnullptrへ戻し、所有者を一つにします。
  • 失敗したAPIの戻り値を見てから、GetLastError()を保存します。
目次