Windows API関数呼び出しの心得

Windows API関数定義方法のススメ

#func による関数定義の原則

これまで説明してきたように、関数の呼び出しには「命令形式」と「関数形式」の2通りの方法がありました。また、API関数のパラメータに文字列やメモリブロックのアドレスを渡したい場合にも、その方法が何通りもありました。これでは、API関数を呼び出すプログラムを書くたびに定義方法・呼び出し方法が異なり、まったく統一されていない書き方になってしまう可能性があります。そこで、ある程度、関数定義や呼び出し時のパラメータ指定方法の方針を決めておくべきです。

このAdvanced HSPのドキュメント内では、実数型などの特殊なパラメータが含まれる場合を除き、原則として以下の方針にしたがっていきます。

Windows API関数は、戻り値が重要であるもの(ウィンドウハンドルを取得するなど)と、そうでないもの(戻り値なしのものや、単にBOOL型で関数の成否のみを返すもの)に分類することができます。戻り値が重要な関数に対して命令形式の定義をした場合、システム変数statに格納された戻り値を参照しなければなりませんが、その戻り値を意図せず使用しないままになってしまう可能性があります。関数形式で書くことによって、そのような危険性を少なくすることができるでしょう。ただし、HSP3に標準添付されているWindows API関数一括定義ファイル(後述)ではすべて#funcで定義されているため、一括定義ファイルを用いる場合には常に命令形式で記述し、statを参照して戻り値を取得しなければなりません。また、一部の関数では、使用目的によって戻り値が重要である場合とそうではない場合があり(例:SendMessage関数など)、そういった関数を使用する場合には状況に応じて関数形式と命令形式を使い分けることにします。

次の点として、API関数に変数アドレスを渡すのにvar型指定を使わないことを推奨します。理由の一つは、前回にも述べましたが、varを使うとパラメータにNULLポインタを渡せなくなるためです。アドレスを渡すパラメータの中には、NULLポインタを渡せるものが存在しますが、var型指定ではNULLポインタを含め任意の数値を渡すことができません。

もう一つの理由は、API関数のリファレンスにおけるパラメータの説明との整合性が取れなくなってしまうからです。MSDNやPlatform SDKのドキュメントの説明、Windows APIを解説しているさまざまなWebページの説明、そして、このAdvanced HSP内での関数の説明など、多く場合は関数パラメータの説明として「~のアドレスを渡す」「~へのポインタを渡す」という説明がされているはずです。それに対しては、スクリプト側でもvarptrを使って、(変数に格納されている値ではなくて)変数のアドレスを渡しているのだということをはっきりと示した方が分かりやすくなります。var型指定を多用してしまうと、変数に格納されている値を渡しているのか、それとも変数のアドレスを渡しているのかがあいまいになってしまう恐れがあります。

ただし、関数に文字列を渡す場合には、その文字列を直接記述できるようにしても問題はないと思われます。というのは、パラメータに文字列(または文字列型変数)を指定した場合、引数として渡すのは文字列のアドレス以外にありえないからです。このような場合には、sptrを使うことでNULLポインタの問題も解決できますね。もちろん、関数に文字列を渡す場合でなく、関数から文字列を受け取る場合には、明示的にvarptrを使って文字列変数のアドレスを指定しなければいけません。

ここでは、var型指定を使わないことを推奨していますが、禁止しているわけではありません。例えば、特にモジュール内でAPI関数を呼び出す場合、var型指定でも問題ないと判断できる場合には、var型指定を使用してもよいと思います(HSPのモジュール機能については後述)。モジュール空間内での#func定義は(global指定子を付けない限り)モジュールの外に影響を与えませんので、上で述べたようなパラメータのあいまいさは、さほど現れないでしょう。varptrを介さないぶん、ほんのわずかでも速度が向上するかもしれませんし。ただ、グローバル空間(モジュールの外)や大きなモジュール空間では、混乱を避けるためにvar型指定の使用を控えた方がよいと思います。そのほか、オリジナルのAPI関数名とは異なる名前を新しく付ける場合には、var型指定を使っても問題ないでしょう。

全部 sptr でもOK?

#func命令のsptr型指定は、渡されたパラメータが数値の場合はそのまま、文字列の場合はそのアドレスを渡すというものでしたね。ということは、(実数型などを除けば)パラメータ定義を全部sptrで指定しても問題ない、ということになりますよね。

実際に、前回のメッセージボックス表示の例において、MessageBox関数の定義で第1パラメータから第4パラメータまですべてsptrを指定したとしても問題なく動作します。

#uselib "user32.dll"
#func MessageBox "MessageBoxA" sptr, sptr, sptr, sptr

MessageBox hwnd, "API関数を呼び出しています", "テスト", 0

関数のパラメータ型指定にすべてsptrを指定するという手法は、関数のパラメータ数さえ分かると簡単に書くことができますし、割と常套的な手段といえます。公開されているHSP3用のWindows API関数一括定義ファイルの中には、そのような定義方法を用いているものもあります。sptrintを区別して記述しても、すべてsptrと記述しても、関数呼び出し時の記述は同じになりますので、どちらの形式で定義するかは各自で決めてよいと思います。

関数名の最後に "A" を付けるべきか

パラメータに文字列が関係する関数にはANSI版とUnicode版が存在し、ANSI版は関数名に "A" が、Unicode版には "W" がそれぞれ付けられているのでしたね。では、#func命令でその名前をどのように定義するのか、つまり、名前に "A" を付けて定義するかどうか、と考えますよね。

多くのWebページや関数のリファレンスでは、一般的に "A" を付けていない関数名の方で書かれています。それに、呼び出し時にわざわざ "A" を付けるというのは面倒ですよね。ですから、ここでは、原則として "A" を付けない名前で定義していくことにします。すなわち、これまで何度も書いてきたように

#uselib "user32.dll"
#func MessageBox "MessageBoxA" int, sptr, sptr, int

とすることにします。しかし、Unicode版ではなくANSI版の関数を呼び出しているのだということを明示したい場合に、あえて "A" を付けてた名前で記述したい場合があるかもしれません。そのような場合には、#define命令を使って、 "A" を付けたものと付けないもので同じ結果になるようにしておく方が望ましいと思います。すなわち、

#uselib "user32.dll"
#func MessageBoxA "MessageBoxA" int, sptr, sptr, int
#define MessageBox MessageBoxA

としておくことで、 "A" を付けた関数名と付けていない関数名の両方を用いることができます。

Windows API関数の一括定義ファイル

HSP3では、主要なWindows API関数の定義を一括で行うためのヘッダファイルがcommonフォルダ内にあらかじめ含まれています。定義ファイルはWindows API関数を提供するシステムDLLごとに分けて準備されています。

例えば、「user32.as」ファイルには「user32.dll」が提供するすべてのAPI関数の#func定義が含まれています。これにより、「user32.as」をインクルードすることによって、自分で#func定義を記述することなくuser32.dllが提供する関数を呼び出すことができるようになります。

また、パラメータに文字列が関係する関数の場合には、関数名に "A" を付けたものと付けていないものの両方が定義されます。したがって、例えば「user32.as」をインクルードするとMessageBox関数とMessageBoxA関数の両方が定義され、どちらを使っても同じように動作させることができます。

この標準添付の一括定義ファイルでは、上でも述べたような「引数型としてすべてsptrを指定する」という方針で記述されています。(実際には、Unicode版関数ではsptrではなくwptrが使用され、文字列が自動的にUnicode文字列に変換されるようになっています。)

#include "user32.as"

MessageBox hwnd, "API関数を呼び出しています", "テスト", 0

この一括定義ファイルを使用する際に注意しておくべきことは、この一括定義ファイルでは#funcによる定義のみで、#cfuncによる定義はされていないため、API関数の呼び出しは「命令形式」でのみ行うことができるということです。

もう1つ注意しておくべきこととして、一括定義ファイルをインクルードすると、含まれているAPI関数の定義情報があまりにも多いために、コンパイル時の処理にやや時間がかかってしまう可能性があるということです。

また、初期のHSP3.0では、一括定義された関数の登録情報がすべてオブジェクトファイル(.ax)に記録されてしまうために、最終的な実行ファイルのサイズが大きくなってしまうというデメリットがあります。ただし、最新バージョンのHSPでは、コンパイル時の最適化により、一括定義された関数のうち実際にプログラム中で使用されたものだけがオブジェクトファイルに記録されるようになっています。

定義検索ツール『Windows32 API 関数検索(apifunc)』(さくらさん作)

さくらさんにより、Windows API関数定義の検索ツール『Windows32 API 関数検索(apifunc)』が公開されています。このツールは非常に強力で、膨大な数のAPI関数の定義をデータベース化して保持しており、関数名から#uselib#func(または#cfunc)の組を検索することが可能になっています。検索方法も、前方検索、部分検索、完全検索の3通りを選択することができ、出力される定義についても、「#func」「#func global」「#cfunc」「#cfunc global」から選択することができるという高機能ぶりです。出力結果をクリップボードにコピーすることが可能で、あとは、そのままスクリプトに貼り付けてしまえば、スクリプト中でAPI関数を使用することができるようになります。

Windows APIへの苦難の道

これまで、メモリアドレスやデータ型、構造体に関する知識を得て、HSPでのWindows API関数を呼び出す方法についても、ある程度は理解できたことでしょう。では、これだけで、HSPでWindows APIを扱っていくのに困ることがないかというと、必ずしもそうではないのです。Windows APIを使いこなすには、いくつもの困難が待ち構えています。

英語のドキュメントを読もう

Windows APIを使うには、必要な関数や構造体の知識を手に入れる必要があります。Web検索で関数名や構造体名を探すことで、主要なAPIの使い方が見つかることでしょう。

さらに、より詳細な情報が必要であれば、Microsoftが公開しているMSDNのオンラインライブラリを参照してみましょう。

MSDNライブラリのドキュメントはもともと英語で書かれているものですが、現在はかなり日本語への翻訳が進んでいます。そのため、日本語のドキュメントだけでも、それなりの情報が得られるかもしれません。

しかし、それでもまだ多くの関数や構造体に関するリファレンスや解説ドキュメントが日本語に翻訳されておらず、場合によっては英語のドキュメントを読まなければならなくなることがあります。また、英語版ドキュメントに書かれていることが日本語版の方には書かれていないこともあります。さらに、おそらくは誤訳なのでしょうが、英語のドキュメントと日本語のドキュメントで記述が違っていることすらあります。ですから、Web上の翻訳サイトや辞書サイトの力を借りながらでもよいですので、ある程度は英語のドキュメントを読む力を付けておいた方がよいでしょう。

定数名はどう調べる?

次に問題として挙げられるのが、定数名の値をどのようにして知るのかということです。

Windows APIでは、いくつかの値が定数名という形で定義されています。例えば、以前にも出てきましたが、定数名「NULL」は0という値で定義されています。そのほか、最も簡単なところでは、定数名「FALSE」は0、「TRUE」は1という値を持った定数名として定義されています。HSPスクリプト中では、

#define NULL    0   ; #define の代わりに #const でもよい
#define TRUE    1
#define FALSE   0

という形で定義することで、これらの定数名を使用することができます。

しかし、実際にAPIを使っていくと、定数名として定義されたさまざまな値が関数のパラメータとして使われることになります。そういったものには、特定の関数の特定のパラメータのみで使われるようなものも数多く存在しています。C/C++言語を用いてWindows APIプログラミングを行う場合には、これらの定数名が定義されたヘッダファイルがあらかじめ準備されています。

一方、他の言語ではそのようなヘッダファイルがないため、必要な定数名の値を何らかの方法で調べなければいけません。以前はHSPでも自分で調べ上げなければならず、それがHSPでWindows APIを使う際の面倒な部分の1つでしたが、現在では簡単に定数名の値を調べる方法も存在します。その方法を紹介していきましょう。

定義検索ツール『Windows32 API Constance 検索(apiconst)』(さくらさん作)

先ほどの関数検索ツールの姉妹ツールとして、さくらさんによりWindows API定数定義の検索ツール『Windows32 API Constance 検索(apiconst)』が公開されています。このツールもまた、膨大な数の定数定義をデータベース化して保持しており、定数名から値を検索することが可能になっています。検索方法も、前方検索、部分検索、完全検索の3通りを選択することができ、出力される#defineの定義についても、「#define」「#define global」「#const」「#const global」から選択することができます。

Win32 API定数検索ツール(blueleafさん作)

blueleafさんにより、同様のWindows API定数の検索ツールが公開されています。このツールは、Windows APIで使用される主要な定数名を網羅しており、定義を知りたい定数名を入力すると、その#defineによる定義を表示し、クリップボードにもコピーしてくれます。

このツールは、もともとHSP2.61用のWindows API一括定義マクロ『MA_MACRO』のアーカイブに同梱されていたものです。現在ではHSP3でのWindows API呼び出しでも簡単に利用できるよう、ツールのみ独立させたアーカイブとして公開されています。

Webサイトを検索する

Windows APIを扱っている多くのWebサイトではC/C++言語が使用されていますが、他のプログラミング言語を使用している所も存在します。特にVisualBasic (VB) でAPIを扱っている所は割と多いようです。そのような言語でもやはり定数名を定義したヘッダファイルが存在しないので、ソースプログラムに直接書かれていることが多いです。そこで、それらの値を引用してくるのも1つの手です。さらに関数の解説も載っていれば、一挙両得ですね。

以下のサイトではVBプログラマ向けにAPI関数が解説されていますが、解説されている関数の数が豊富なので役に立つことでしょう。

WinAPI Database for VB Programmer
http://www.winapi-database.com/

Windows SDKをインストールしC/C++用のヘッダファイルを検索する

最も確実な方法は、Windowsプログラミングが可能なC/C++言語開発ツールに含まれているWindows用のヘッダファイルを参照することです。特に、最新のWindowsでしか使用できないAPIで必要となる定数定義のなかには、上記の検索ツールでもヒットしないものがある可能性があります。この場合には、マイクロソフトのWebサイトで公開されている『Microsoft Windows SDK』をインストールし、それに含まれているC/C++のヘッダファイルの中を検索しなければならないでしょう。こちらの方ではWindows APIに関するドキュメントも手に入ります。ただし、インストールに必要なディスク容量が非常に大きいので、注意が必要です。

Windows SDK for Windows Vista (ISOイメージ ダウンロード)(日本語)
http://www.microsoft.com/downloads/details.aspx?FamilyID=7614FE22-8A64-4DFB-AA0C-DB53035F40A0&displaylang=ja
Windows SDK for Windows Server 2008 (Web Setup)(英語)
http://www.microsoft.com/downloads/details.aspx?FamilyID=e6e1c3df-a74f-4207-8586-711ebe331cdc&DisplayLang=en
Windows SDK for Windows Server 2008 (ISO Image Download)(英語)
http://www.microsoft.com/downloads/details.aspx?FamilyID=f26b1aa4-741a-433a-9be5-fa919850bdbf&DisplayLang=en

Windows API関数のエラー処理

Windows APIを扱う上で注意すべきことは、関数の呼び出しが常に成功するとは限らないということです。したがって、関数を呼び出した後は、成功しているかどうかを判断し、もし呼び出しに失敗していた場合には、適切なエラー処理をしなければなりません。

戻り値からのエラー判断

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

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

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

戻り値なしのAPI関数

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

ブール型を返すAPI関数

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

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

もちろん、ブール型を返すすべてのAPI関数が、エラー時に0を返すわけではありません。例えばEnableWindow関数の戻り値の型はBOOL型ですが、戻り値はエラーが発生したかどうかには関係ありません。

ポインタを返す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) であるかどうかでエラー判断を行ってしまうと、正しく処理できなくなってしまうので、注意しなくてはいけません。関数がエラー時にどちらの値を返すかは、API関数のリファレンスを参照して判断しなくてはなりません。

その他のAPI関数

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

拡張エラー情報

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

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

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

DWORD GetLastError();

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


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

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

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

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


とはいっても、このAdvanced HSPの中で、エラー発生時にそのエラーを特定するためにGetLastError関数を呼び出すことはほとんどありません。ほとんどの場合は、関数の戻り値だけで判断すれば十分だからです。実際に必要となるのは、成功した場合でもエラーコードを書き換えるタイプの関数に対してです。これについては後で述べていくことになると思います。