Win32 APIのエラー処理

戻り値からのエラー判断

Win32 API関数が呼び出されると、関数はそれぞれで決められた処理を行ないます。このとき、API関数を呼び出したプログラムが無効な引数を渡したり、何らかの理由で処理を行なうことができなかったりした場合には、そのAPI関数は、戻り値を通して、エラーが発生したことを呼び出し側プログラムに知らせます。

Win32 API関数を呼び出したプログラムが、エラーが発生したかどうかを判断してそれに応じた処理を行なわずに、関数が成功したとみなして処理を続けていってしまうと、最悪の場合、プログラムが暴走するという危険性さえ出てきます。絶対にエラーは起こらない、ということが分かっている場合には必要ありませんが、そうでない場合には適切なエラー処理を行なうべきです。

では、Win32 API関数がどのようなエラー値を返すのかを見ていきましょう。関数のデータ型によってエラーを示す値はさまざまです。いくつか例をあげてみましょう。

戻り値なしのAPI関数

VOID型の関数は、戻り値を持ちません。これらの関数は、呼び出し側に返す情報はなく、また、ほとんどエラーを起こす可能性もないものです。したがって、これらの関数の後でエラー処理を行なう必要はほとんどありません。(というより、エラーかどうかの判断を行なうこと自体ができませんが。)

ブール型を返すAPI関数

ブール型(BOOL型やBOOLEAN型)を返す関数は、関数が正常に処理を行なった場合には0以外の値が返ります。また、関数がエラーを起こした場合、戻り値は0になります。

ブール型とは、もともと真か偽かを表す型であり、0以外の値である場合を真とみなし、0の場合を偽とみなします。Win32では、便宜上、真(TRUE)を1と定義し、偽(FALSE)を0と定義していますが、実際に真偽を判断する(エラーかどうかを判断する)には、「01か」ではなく、「0か、それとも0以外の値か」で判断しなくてはなりません。

ポインタを返すAPI関数

PVOID型やLPSTR型などといった、メモリブロックへのポインタ型を戻り値として返す関数は、関数が正常に処理されると、目的のメモリブロックへのポインタ(メモリアドレス)が返されますが、関数が失敗すると0 (NULL) を返します。このような関数は、エラーが起こっていないかどうかを確認しておかないと、返されたアドレスのメモリ領域にアクセスしようとした時に、アドレス0x00000000の領域にアクセスしようとしてアクセス違反が発生し、プログラムが強制終了してしまう可能性があります。

オブジェクトハンドルを返すAPI関数

HANDLE型、HGDIOBJ型、HDC型、HMENU型、HWND型などといった、Windowsオブジェクトのハンドルを返す関数は、関数が成功するとオブジェクトのハンドルを返し、関数が失敗すると、ほとんどの関数は0 (NULL) を返します(Windowsオブジェクトについては後述)。しかし、一部の関数には、エラーが発生したときに0 (NULL) ではなくて-1 (INVALID_HANDLE_VALUE) を返すものがあります。例えば、ファイルオブジェクトなどを作成するCreateFile関数は、エラーが発生したときには-1 (INVALID_HANDLE_VALUE) を返します。このような関数に対して、戻り値が0 (NULL) であるかどうかでエラー判断を行なってしまうと、正しく処理できなくなってしまうので、気をつける必要があります。関数がエラー時に-1 (INVALID_HANDLE_VALUE) を返すかどうかはは、API関数のリファレンスを参照して判断しなくてはなりません。

その他のAPI関数

他にもDWORD型やLONG型など、上記のデータ型以外の戻り値を返すAPI関数も、多く存在します。これらの関数の戻り値は関数によってさまざまで、エラーが発生したときにどのような戻り値が返るかはそれぞれの関数によります。これらについてはAPI関数のリファレンスに記述されているとおりにしなければなりません。

拡張エラー情報

スレッドのエラーコード

さて、Win32 API関数の中には、エラーが発生したときに戻り値でエラーがあることを通知すると同時に、発生したエラーがどのようなものであるかを表すエラーコードを設定するものがあります。

これらの関数の処理中にエラーが発生したとき、呼び出したスレッドにエラーコードが割り当てられます。このエラーコードはスレッドごとに割り当てられるものであるため、マルチスレッドアプリケーションのある1つのスレッドのエラーコードが置き換わっても、別のスレッドのエラーコードは影響を受けないようになっています。(HSPはシングルスレッドであるため、マルチスレッドについてはあまり関係ありませんが。)

関数を呼び出して、返された戻り値がエラーの発生を知らせてきた場合、それがどのようなエラーかを知るためには、GetLastError関数を呼び出します。

DWORD GetLastError();

この関数は、単純にスレッドのエラーコードを戻り値として返します。


このGetLastError関数を使う上で注意すべきことがあります。

まず1つは、関数がエラーを起こした場合には、直ちにGetLastError関数を呼び出すべきであるということです。なぜなら、他のAPI関数を呼び出した場合に、エラーコードが上書きされてしまう可能性があるためです。

次に、すべての関数がエラー発生時にエラーコードを設定するわけではないということです。特に、Windows 95/98/Meでは、関数のコードの多くが16ビット版Windowsの時代の16ビットコードをそのまま用いて実装されているため、そういった関数ではGetLastError関数によるエラーコード報告がされるように実装されていないのです。したがって、これらについては、Win32 API関数のリファレンスを参照して、関数がGetLastError関数によるエラーコード取得をサポートしているかどうかを確認する必要があります。

もう1つは、Win32 API関数は、エラーが発生したときのみエラーコードを書き換えるということです。呼び出されたAPI関数が成功した場合には、このエラーコードは以前のままで、変更されることがありません。したがって、API関数の戻り値がエラーを示す値であった場合にだけ、GetLastError関数を呼び出してエラーコードを取得するようにしなくてはなりません。ただし、一部のWin32 API関数には、成功した場合にもエラーコードを書き換えるものがあります。このような関数はAPIのリファレンスにそのことが明記されています。(例:CreateFileMapping関数やCreateMutex関数など)

エラーメッセージの取得

GetLastError関数によって、エラーコードを取得することはできますが、ただそれだけではそのエラーコードが何を意味しているのかはわかりません。また、プログラム中でWin32 APIを呼び出したときにエラーを検出した場合には、テキストで書かれた説明を表示したい場合があります。Win32 APIには、エラーコードをテキストに変換するためのFormatMessage関数があります。

DWORD FormatMessageA(
    DWORD   dwFlags,       // 動作フラグ
    PCVOID  pSource,       // メッセージ定義位置
    DWORD   dwMessageID,   // メッセージID
    DWORD   dwLanguageID,  // 言語ID
    PTSTR   pszBuffer,     // バッファのアドレス
    DWORD   nSize,         // バッファのサイズ
    va_list *Arguments     // 挿入句の配列のアドレス
);

システムのエラーコードからテキストを取得する場合、この関数のdwFlagsパラメータにはFORMAT_MESSAGE_FROM_SYSTEMフラグ(0x00001000と定義されている)を指定する必要があります。dwMessageIDにはエラーコードを、pszBufferにはテキストを格納するバッファアドレスを、nSizeにはそのバッファサイズを指定します。

エラーコード・エラーメッセージ取得モジュール apierr.as

ここで、GetLastError関数を呼び出してエラーコードを取得したり、FormatMessage関数を呼び出してエラーコードからテキスト形式のエラーメッセージを取得したりするモジュール命令を定義してみましょう。エラー処理に役立つはずです。


このモジュールでは、2つのモジュール命令geterrcodegeterrtextを定義しています。

geterrcode

パラメータなし

このgeterrcode命令は、GetLastError関数を呼び出して、スレッドのエラーコードを取得し、システム変数statに格納します。

geterrtext n1

n1:エラーコード

このgeterrtext命令は、FormatMessage関数を呼び出して、n1パラメータで与えられたエラーコードからエラーテキストを取得し、システム変数refstrに格納します。また、システム変数statにはエラーテキストのサイズがバイト単位で格納されます。エラーテキストが取得できなかった場合にはstatの値が0になります。


以下はモジュールスクリプトです。

    ;###################################################################
    ; 拡張エラー情報取得モジュール  apierr.as
    ;###################################################################

    #module "apierr"

    ; apierr モジュール初期化ルーチン
    #deffunc _init_apierr
    mref stt, 64          ; stat
    mref rfst, 65         ; refstr
    ; refstr のアドレスを取得
    getptr pstr, rfst
    ; pfn.0 に GetLastError  関数のアドレスを取得
    dll_getfunc  pfn.0, "GetLastError", D_KERNEL
    ; pfn.1 に FormatMessage 関数のアドレスを取得
    dll_getfunc  pfn.1, "FormatMessageA", D_KERNEL
    return

    ;===================================================================
    ; geterrcode
    ; スレッドエラーコード取得し stat に格納
    ;===================================================================
    #deffunc geterrcode
    ll_callfnv  pfn.0
    stt = dllret@          ; 戻り値を stat に格納
    return

    ;===================================================================
    ; geterrtext  n1
    ;    n1 : エラーコード
    ; エラーコードからエラーテキストを取得し refstr に格納
    ; stat は取得文字列サイズ(stat=0 の時エラー)
    ;===================================================================
    #deffunc geterrtext int
    mref code              ; エラーコード
    prm = 0x00001000, 0, code, 0, pstr, 4096, 0
    ll_callfunc  prm, 7, pfn.1
    stt = dllret@          ; 戻り値を stat に格納
    return

    #global

    _init_apierr           ; モジュールの初期化

このモジュールスクリプトを見ると、初期化用の命令_init_apierrによって、あらかじめ2つの関数のアドレスの取得を行なっていることが分かります。これは、実行速度を速くするためというもののほかに、もう1つ理由があります。

これはgeterrcode命令内でのみの問題ですが、GetLastError関数の呼び出しにdllproc命令を使うと、関数アドレス取得のために内部でWin32 API関数GetProcAddressが呼び出されてしまい、「直ちにGetLastError関数を呼び出す」という原則を破ることになってしまうためです。あらかじめ関数のアドレスを取得しておき、ll_callfnv命令やll_callfunc命令によってアドレスから直接呼び出すことによって、この問題を回避することができます。

エラーテキスト変換サンプル

apierr.asモジュールを使って、指定されたエラーコードからエラーテキストを取得して表示する簡単なプログラムを作ってみましょう。

    #include "llmod.as"
    #include "apierr.as"

    sdim  buf, 4096
    screen  0, 300, 100
    title "Win32 API エラーテキスト表示"
    objsize 150, 25
    pos   0,  4 : mes "エラーコード"
    pos 100,  0 : input code, 50, 25
    pos 150,  0 : button "テキスト取得", *lb_gettext
    pos   0, 25 : mesbox buf, 300, 75
    stop

*lb_gettext
    ;エラーテキストを取得
    geterrtext code
    if stat == 0 {
        dialog "エラーテキストを取得できません。"
    } else {
        objprm 2, refstr
    }
    stop