Windowsでは、プログラマが画面出力やユーザー入力などの処理を簡単に行なうことができるように、基本的な機能をWindowsがはじめから用意しておき、プログラマがその機能を自由に使うことができるようにしています。これらの機能にアクセスするための手段が、32ビット版のWindowsでは『Win32 API』というインターフェースの形で与えられています。これらのインターフェースの多くは関数のセットとして与えられていて、一般的には、この関数のセットまたは関数一つ一つのことを「Win32 API」と呼びます。また、個々の関数を「API関数」と呼ぶこともあります。
単に「API」といった場合、32ビットWindowsプラットフォームの下ではWin32 APIのことを指します。
Win32 APIの実際の関数コードは、Windowsシステムが提供するDLL (Dynamic Link Library) に格納されています。
DLLとは、簡単にいえば、ある特定の機能を実現する関数(プログラムコード)の入っているバイナリファイルのことで、一般的にはファイル名の拡張子が「.dll」になっています。アプリケーションは、実行した時にこのファイルをメモリ内に読み込んで、中に含まれている関数を呼び出すことができます。HSPの拡張プラグインも、このDLLであることは知っているでしょう。拡張プラグインは、DLLの中に拡張機能を実現するための関数を含んでいて、HSPの実行時にDLLをメモリに読み込み、HSP側からこれらの関数を呼び出すことができるようになっているのです。
Win32 APIは、先に述べたとおり、Windowsが持っているDLLにその関数コードが格納されています。特に、以下に示すDLLはWindowsの最重要なもので、これらのDLLが提供するAPIはWindowsアプリケーションの開発には不可欠です。
DLLファイル名 | 主なAPIの機能 |
---|---|
kernel32.dll | プロセス、メモリや周辺装置を管理 |
user32.dll | ウィンドウベースのユーザー・インターフェースを管理 |
gdi32.dll | 文字列やグラフィックスの描画に関するサービスを提供 |
また、これらのほかにも、Windowsシェルの中核をなすshell32.dllや、コモンコントロールの機能を提供するcomctl32.dllなど数多くのDLLがあり、それぞれが多くのAPI関数を提供しています。
さて、Win32 API関数を呼び出すとは言ったものの、HSPでは「関数」という概念がないので、「そもそも、関数って何?」などと思っている方もいるでしょう。そこで、関数について簡単に説明してみることにしましょう。
プログラミングの世界で関数(function)というと、独立して機能するひとまとまりのプログラムコードのブロックのことを言います。そして、別のプログラムコードでこの関数のコードを実行する(関数のコードブロックにジャンプしてくる)ことを、「関数を呼び出す」といいます。ちょうど、HSPのモジュール命令を定義して、それを実行することに似ていますね。
関数を呼び出す時に、もととなる情報を関数のコードに渡さなければならない場合があります。例えば、簡単な関数として、2つの整数の和を求める関数というものを考えてみましょう。この場合には、もとの2つの数を情報として渡さなくてはなりませんね。関数に渡されるこのような情報のことを引数(ひきすう)またはパラメータ(parameter)といいます。関数がどのような情報を必要とするかは関数の機能によりますから、引数の数や種類は関数によって異なります。10個以上もの引数を必要とするような関数もあれば、引数が必要ない(引数の数が0個の)関数もあります。
関数コードブロックでの処理が終わると、はじめに関数を呼び出したプログラムコードに戻ってきます(このことを「関数から(制御が)返る/戻る」とか、「関数が制御を返す」などと言います)。このとき、関数によっては、関数を呼び出したプログラムコードに情報を渡さなければならないものがあります。例えば、先ほどの2つの整数の和を求める関数の場合には、求めた結果(和)を渡さなければなりません。このように、関数が呼び出し側のコードに値を渡すことを、「関数が結果を返す」または「関数から結果が返る」と言い、返されたデータのことを戻り値(return value)と呼びます。戻り値を持たない(戻り値を返さない)関数もありますが、戻り値を持つ関数でも、戻り値の数は1個までです。
ところで、関数においては、2つの点でデータ型が関わってきます。
まず1つは、関数に渡される引数のデータ型です。引数には、先ほどの例のような整数だけでなく、さまざまな型のデータが渡されます。どんな型のデータが渡されるかは関数によってさまざまです。例えば、Win32 APIの中にはウィンドウに対して操作を行なうものがありますが、そのような関数ではウィンドウハンドル(ウィンドウを識別する32ビット値)が必要になるので、必ずHWND型(ウィンドウハンドルのデータ型)の引数を持っています。また、メモリブロックに格納されているデータにアクセスすような関数は、そのメモリブロックのアドレスを引数に持ちます。例えば、文字列操作を行なう関数は、文字列へのポインタ型(例えばLPSTR型など)の引数を持っていて、関数はこのポインタが指すメモリブロックに対して文字列操作を行ないます。
もう1つは、関数自体のデータ型です。実は関数自身もデータ型を持っています。関数のデータ型とは、すなわち、関数の戻り値のデータ型のことなのです。例えば、ウィンドウを作成するようなWin32 API関数は、戻り値としてウィンドウハンドルを返しますから、この関数の型はHWND型ということになります。他にも、メモリブロックを確保するようなAPI関数の型はPVOID型(データへのポインタ型)であり、この関数は戻り値として確保したメモリブロックのアドレスを返します。
データ型についての詳細は、『データ型』の項を参照してください。
次は、関数の記述形式について説明します。
例えば、先ほどの例にあった2つの整数の和を求める関数があると考えましょう。この関数の名前がadd_integerであるとします。この場合、C/C++などの言語では、関数の宣言/定義時などには、次のような記述がされます。
int add_integer(int n1, int n2);
上の記述は、この関数名が「add_integer」であり、2つのint型の引数n1とn2を持つこと、そして、戻り値もまたint型であることを表しています。実際には、引数のn1やn2という変数名はあまり意味はなく、データ型がint型であるということが重要なのです。引数の変数名は何でもいいのです。一般的には、何か引数の意味が分かるような変数名がつけられています。
では、今度は実際のWin32 API関数を例にとってみましょう。Win32 APIの1つにMessageBeep関数というものがあります。この関数の宣言は以下のようになります。
BOOL MessageBeep(
UINT uType // サウンドの形式
);
ここでは複数行で記述されていますが、1行で記述した時と違いはありません。また、2つのスラッシュ「//」は、この行のそれ以降がコメントであることを表します。
この関数の場合は、関数名が「MessageBeep」であり、1つのUINT型の引数を持つこと、そして、関数の型がBOOL型でありBOOL型の戻り値を返すことを示します。BOOL型とは、真か偽かを表す型であり、0以外の値(通常は1)である場合を真とみなし、0の場合を偽とみなします。BOOL型の戻り値を返すWin32 API関数の多くは、関数が成功すると真(0以外の値)を返し、エラーが発生して関数が失敗すると偽(0)を返します。
API関数のエラーについては、次の『Win32 APIのエラー処理』の項で説明します。
HSPスクリプト中からWin32 API関数を呼び出す手順を説明しましょう。
API関数の呼び出し方法は何通りかあるのですが、まず最初に、モジュール定義命令dllprocを使用しない、最も低レベルな(抽象化されていない)方法から説明しましょう。これが最も基本的な方法で、C/C++などの言語でDLLの動的なロードを行なう場合もこのような手順で行なわれます。dllproc命令を使うのは、これを理解した後でのほうがいいでしょう。
スクリプトの先頭では、プリプロセッサ命令#includeで"llmod.as"ファイルををインクルード(結合)しておく必要があります。このファイルではllmodモジュール命令の定義と、関数呼び出し時の戻り値が、変数dllretに格納されるな設定がされています。
#include "llmod.as"
実際には、後で戻り値を取得する際に、ll_ret命令を使って取得するのであれば、あえてインクルードする必要はありませんが。
あるWin32 API関数を呼び出したい場合、まず、そのAPI関数が含まれているDLLをメモリにロードしなくてはなりません。DLLをロードするには、loadlib関連命令の1つll_libload命令を実行します。
v1:DLLのハンドルを格納する変数
s2:DLLのファイル名を表す文字列
この命令によって、s2で指定された名前のDLLがメモリ上に読み込まれます。メモリ上にロードされたDLLは、DLLのインスタンスハンドル(単にDLLのハンドルともいう)と呼ばれるものによって識別されます。これは、ロードされたDLLを識別するための32ビット値のことです。あとで関数のアドレスを取得するときには、このDLLのハンドルが必要になります。DLLが正常にロードされた場合は変数v1にDLLのハンドルが格納されますが、DLLが見つからないなどのエラーが起こった時はv1に0が格納されます。
指定されたDLLがまだロードされていなかった場合は、ll_libload命令は、DLLをメモリにロードしてハンドルを返すと同時に、Windowsが内部で保持しているDLLへの参照カウンタ(アプリケーションがDLLを何回ロードしようとしたかを記憶しておくための内部変数)を1にセットします。すでにロードされているDLLをll_libload命令でさらにロードしようとした場合、DLLはすでにメモリ上にあるので、Windowsは新しいメモリ領域にDLLをロードしようとはせずに、すでにロードされているDLLのハンドルを返します。このとき、DLLへの参照カウンタをインクリメント(1だけ増加)します。
ll_libload hdllUser, "user32.dll"
上のスクリプトが実行されると、メモリ上にuser32.dllがロードされ、変数hdllUserにはuser32.dllを表すハンドルが格納されます。
関数の呼び出しには、まず、関数のアドレス(関数ポインタ)、つまり、その関数のプログラムコードの先頭部分がメモリ上のどの位置にあるのかということを取得しなくてはなりません。関数のアドレスの取得はll_getproc命令によって行ないます。
v1:関数のアドレスを格納する変数
s2:関数名を表す文字列
n3:DLLのハンドル
この命令は、n3で指定されたDLLの中にs2で指定した名前の関数があるかどうかを調べて、もしあれば、その関数のアドレスを変数v1に格納します。もし指定された名前の関数が見つからなかった場合にはv1には0が格納されます。
ll_getproc pfunc, "MessageBeep", hdllUser
上の例では、user32.dllに含まれるMessageBeep関数のアドレスを取得し、変数pfuncに格納しています。
関数の呼び出しは、関数のアドレスを指定して行ないます。
呼び出す関数が引数を持たない場合には、ll_callfnv命令を実行します。また、呼び出す関数が引数を持つ場合には、ll_callfunc命令を実行します。
n1:呼び出す関数のアドレス
v1:引数を格納した配列変数
n2:引数の数
n3:呼び出す関数のアドレス
ll_callfunc命令は、通常、引数を持つ関数の呼び出しに使用しますが、引数を持たない関数を呼び出す場合でも、v1に適当な変数を指定して、n2に0を指定することで呼び出すことができます。
これらの命令を使って関数を呼び出すと、変数dllretに関数の戻り値が格納されます。
prm = 0 ll_callfunc prm, 1, pfunc if dllret == 0 { mes "関数呼び出し時にエラーが発生しました" } else { mes "関数呼び出しは成功しました。" }
上の例では、先ほど取得したMessageBeep関数のアドレスを呼び出しています。この関数の引数は1つですから、変数prmに引数(ここでは0)を格納して、引数の数1を指定して関数を呼び出しています。
関数の戻り値は変数dllretに格納されますが、この関数は、呼び出しが成功すると、戻り値として 0 以外の値を返すので、成功した場合と失敗した場合とでそれぞれメッセージを表示します。戻り値としてどのような値が返されるのかは、関数ごとに異なります。
関数の戻り値を取得するのに、ll_ret命令を使用することもできます。この命令は、直前に呼び出した関数の戻り値を取得し、変数v1に格納します。
n1:戻り値を格納する変数
prm = 0
ll_callfunc prm, 1, pfunc
ll_ret vret ; 直前に呼び出した関数の戻り値を変数 vret に格納
if vret == 0 {
mes "関数呼び出し時にエラーが発生しました"
} else {
mes "関数呼び出しは成功しました。"
}
DLLに含まれている関数を呼び出す必要がなくなった場合には、メモリ上にロードされているDLLをメモリから解放します。DLLの解放には、ll_libfree命令を実行します。
n1:解放するDLLのハンドル
この命令はll_libload命令と対をなすもので、実行するたびにn1のハンドルで指定されたDLLへの参照カウンタをデクリメント(1ずつ減少)していきます。そして、参照カウンタが0になったらメモリ上からDLLが解放されるのです。したがって、同じDLLに対して複数回ll_libload命令を実行した場合には、それと同じ数だけll_libfree命令を実行しなければ、DLLは解放されません。
ll_libfree hdllUser hdllUser = 0
ロードしたDLLは、たとえ解放しなくても、アプリケーション終了時にはWindowsによって完全に解放されることになっていて、これを解放しなかったからといって何ら問題が発生するわけではありません。そのため、アプリケーション終了時にわざわざDLLの解放を行なう必要はありません。
DLLのハンドルは、DLLがロードされてから完全に解放されるまでの間は、常に一定の値になり、変化することはありません。したがって、1つのDLLが持っているいくつかの関数をスクリプト中で何度も呼び出さなければならないような場合には、はじめに1度だけDLLをロードしてそのハンドルを取得しておくことで、関数呼び出しのたびにDLLのロードをしなくてもいいようにすることができます。
同様の手法として、DLLがロードされている間は、そのDLLが持っている関数のアドレスも変化しませんから、同じ関数を何度も呼び出さなければならない場合は、あらかじめ関数のアドレスを取得しておくことで、関数呼び出しのたびにアドレスを取得する必要がなくなります。特に速度が要求される部分では、この手法によって実行速度が改善されるかもしれません。
さて、今度はllmod.asでモジュール命令として定義されているdllproc命令を使ったAPI関数の呼び出しです。手順は以下のようになります。
上の手順を見れば分かりますが、以前のll_getproc命令による関数アドレス取得とll_callfunc/ll_callfnv命令による関数呼び出しの部分がなくなって、代わりにdllproc命令による関数呼び出しがあります。すなわち、dllproc命令とは、関数アドレス取得と関数呼び出しを一度に行なうための命令ということです。
さらに、llmodモジュールでは、よく使われる代表的なシステムDLLがあらかじめロードされています。したがって、それらのDLLによって提供されるAPI関数を呼び出す場合には、関数呼び出しのたびにDLLのロードや解放を行なう必要がなくなるのです。
今述べたとおり、dllproc命令は関数アドレス取得と関数呼び出しを一度に行ないます。
s1:関数名を表す文字列
v2:引数を格納した配列変数
n3:引数の数
n4:DLLのハンドル
それぞれのパラメータはll_getproc命令やll_callfunc命令で指定するパラメータと同じです。ただし、n4だけは少し異なります。DLLをロードしてそのハンドルを持っている場合にはそのハンドルを指定しますが、llmodモジュールがあらかじめロードしているDLLを使う場合、n4パラメータには以下の定数名または値のどちらかを指定することができます。
定数名 | 値 | DLL名 |
---|---|---|
D_KERNEL | 0 | kernel32.dll |
D_USER | 1 | user32.dll |
D_SHELL | 2 | shell32.dll |
D_COMCTL | 3 | comctl32.dll |
D_COMDLG | 4 | comdlg32.dll |
D_GDI | 5 | gdi32.dll |
dllproc命令によって関数を呼び出した場合、ll_callfunc命令などの時と同じく、関数の戻り値は変数dllretに格納されます。ll_ret命令を使って戻り値を取得することもできます。また、dllproc命令の場合には、システム変数statにも戻り値が格納されるので、こちらを参照してもいいでしょう。
例えば、user32.dllに含まれるMessageBeep関数を呼び出す場合は、以下のようにします。
prm = 0
dllproc "MessageBeep", prm, 1, D_USER
if dllret == 0 { ; dllret の代わりに stat でもよい
mes "関数呼び出し時にエラーが発生しました"
} else {
mes "関数呼び出しは成功しました。"
}
この場合には、DLLのロードや解放を行なう必要はまったくありません。
関数の中には、メモリブロックへのポインタ(メモリブロックのアドレス)を引数として渡さなければならないものがあります。これらの関数がポインタを要求するのは主に以下のような理由がある場合です。
例えば、Windowsのメッセージボックスを考えてみましょう。メッセージボックスとは、右のようなウィンドウのことです。(mesbox命令で作成されるHSPオブジェクトとは違うので注意。)
このメッセージボックスを表示させるには、2つの文字列データが必要なのが分かりますね。1つキャプションバーに表示されるタイトルの文字列、そしてもう1つは実際のメッセージの文字列です。
メッセージボックスを表示するWin32 API関数はMessageBoxです。
int MessageBoxA( HWND hWnd, // オーナーウィンドウのハンドル PCTSTR pszText, // 表示文字列のアドレス PCTSTR pszCaption, // キャプションバーの表示文字列のアドレス UINT uType // メッセージボックスのタイプ );
ここで、関数名が、MessageBoxAというように、最後に"A"がついているのに気づくと思います。通常は、文字列を扱うWin32 API関数でANSI文字列を受け渡すものは、このように関数名の末尾に"A"がつけられています。
文字列を渡すAPI関数については下のほうで説明しています。
この関数の2番目と3番目の引数に注目してみましょう。これらの引数の型はPCTSTR型になっているのが分かります。これは、文字列のアドレスを表すデータ型で、これらの引数にはそれぞれメッセージ文字列のアドレスと、タイトル文字列のアドレスを指定することになります。
PCTSTR(またはLPCTSTR/PCSTR/LPCSTRなど)に含まれる「C」の文字は“const”のことで、これは、このアドレスで指定されるメモリブロックに格納されている文字列が関数呼び出し時に変更されないということを保証するものです。すなわち、関数はこのメモリブロックから文字列を読み出すだけで、ここに書き込むことはしない、ということです。逆に、「C」の文字を含まないPTSTR(またはLPTSTR/PSTR/LPSTRなど)の型の場合は、そのアドレスで指定されるメモリブロックに新しい文字列が書き込まれたり、すでに格納されている文字列が変更されたりする可能性があるということです。
API関数によって、何らかの情報を取得する場合などがこれにあたります。例えば、プロセスのカレントディレクトリを取得するGetCurrentDirectory関数があります。
DWORD GetCurrentDirectoryA( DWORD nSize, // バッファサイズ PTSTR pBuffer // ディレクトリ名を格納するバッファのアドレス );
この関数の場合、カレントディレクトリ名という文字列データを関数側から呼び出し側に渡す必要がありますから、まず呼び出し側は文字列を格納するためのメモリブロックを準備しておき、そのアドレスを引数として渡さなくてはなりません。また、このような文字列データを返す関数は、多くの場合、メモリブロックのサイズも同時に指定しなくてはなりません。
前にも述べたとおり、関数は通常、戻り値を1つしか持つことができません。したがって、さらに多くの戻り値を返さなくてはならないような関数は、データを戻り値として返す代わりに、引数として変数のアドレスを指定させ、その変数にデータを格納する、という手段をとります。
例えば、あるメモリブロックに格納されている文字列の末尾に別の文字列を結合するlstrcat関数があります。
int lstrcatA( PTSTR pString1, // 文字列 1 PCTSTR pString2 // 文字列 2 );
この場合は、2つの引数にともに文字列のアドレスを指定することになりますが、最初の引数でアドレス指定されるメモリブロックには2つの文字列を結合させた新しい文字列が格納されて返されます。つまり、メモリブロックに格納されていた文字列データが、この関数によって書き換えられるのです。
さて、引数としてアドレスを渡すWin32 API関数には1つの原則があります。それは、データを格納するためのメモリブロックは呼び出し側が準備しなくてはならないということです。中には関数側がメモリブロックを確保するようなAPI関数も存在しますが、ほとんどの場合は呼び出し側が確保します。
そのため、これらのAPI関数を呼び出す際には、確実にデータを格納しうるだけのサイズのバッファを確保しておかなくてはなりません。構造体を指定する場合には、その構造体のサイズ分以上の、また、文字列バッファの場合には、文字列が格納できる分以上の大きさのバッファを準備しましょう。
また、指定するアドレスも有効なものを指定しなくてはなりません。確保していない無効なアドレスを指定したり、アドレス以外のいいかげんな値を指定したりしてはいけません。
これらの事項を守らないと、プログラムが暴走したり、強制終了したり、また、特にWindows 95/98/Meなどでは、最悪の場合にはWindowsのシステムが壊れてしまうことすらも考えられます。
HSPスクリプトで、これらのAPI関数を呼び出してみることにしましょう。アドレスを渡すといっても、難しいことはありません。getptr命令を使って数値型変数や文字列型変数のアドレスを取得し、それを引数として指定するだけです。
例として、カレントディレクトリを取得するGetCurrentDirectory関数を呼び出してみることにしましょう。カレントディレクトリはシステム変数curdirに格納されていますから、実際にプログラムを組むときに使用することはないと思いますが、ここではAPI関数呼び出しの練習ということで使ってみます。
通常、フルパス指定のファイル名の長さは最大260(MAX_PATHと定義されている)となっているので、その分の文字列バッファを確保しておきます。引数に文字列ポインタを含みますから、関数名に"A"をつけるのを忘れずに。
#include "llmod.as" ; 他のディレクトリ(ここでは windows ディレクトリ)に移動しておく chdir windir ; カレントディレクトリを格納する変数を確保 sdim bufdir, 260 ; 文字列変数のアドレスを取得 getptr p_bufdir, bufdir ; GetCurrentDirectory 関数を呼び出す prm.0 = 260 ; 文字列変数のサイズ prm.1 = p_bufdir ; 文字列変数のアドレス dllproc "GetCurrentDirectoryA", prm, 2, D_KERNEL mes "取得されたカレントディレクトリ:" + bufdir mes "curdir:" + curdir stop
上のスクリプトでは、ポインタ(アドレス格納用の変数)としての変数p_bufdirに、文字列型変数bufdirのアドレスを格納して、これらを関数の引数として指定しています。
このスクリプトを実行すると、GetCurrentDirectoryで取得されたディレクトリ名とシステム変数curdirにあるディレクトリ名が同じになっていることが分かります。
次に、上のスクリプトを一部書き換えてみます。以下のスクリプトを見てください。
#include "llmod.as" ; 他のディレクトリ(ここでは windows ディレクトリ)に移動しておく chdir windir ; カレントディレクトリを格納する変数を確保 sdim bufdir, 260 ; GetCurrentDirectory 関数を呼び出す prm.0 = 260 ; 文字列変数のサイズ getptr prm.1, bufdir ; 文字列変数のアドレスを取得 dllproc "GetCurrentDirectoryA", prm, 2, D_KERNEL mes "取得されたカレントディレクトリ:" + bufdir mes "curdir:" + curdir stop
ここでは上の時とは異なり、ポインタ(アドレス格納用の変数)を介さずに、変数bufdirのアドレスをprm.1に直接格納しています。変数用のメモリを節約するためのテクニックのようなものです。このページでは、この手法を多用しているので、いったい何をしているのかを分かるようにしておいてください。
データとして文字列を扱うタイプのWin32 API関数は、同じものが2種類存在します。具体的にはANSI文字列版の関数とUnicode文字列版の関数が存在するのです。扱っている文字列がANSI文字列(Shift-JISなどのマルチバイト文字列も含む)の場合はANSI版の関数を、Unicode文字列(ワイド文字列)の場合はUnicode版の関数を使うことになっています。
Win32 APIでは、ANSI版の関数は関数名の最後が"A"で、Unicode版の関数は関数名の最後が"W"で終わるようになっています。上でも述べましたが、MessageBox関数を呼び出すには、関数名をMessageBoxAとしなければなりませんでした。それは、実際には、ANSI文字列を渡すMessageBoxA関数と、Unicode文字列を渡すMessageBoxW関数が存在していて、そのうちのANSI文字列版を呼び出す、ということなのです。
しかし、すべての環境でこの2種類の関数が使えるというわけではありません。Windows NT/2000/XPなどのいわゆるNTカーネルといわれるOSではANSI版とUnicode版の両方を使うことができますが、Windows 95/98/Meなどの9xカーネルといわれるOSでは、ほとんどのAPI関数ではANSI版の関数だけしか使うことができないのです。また、HSP内部でも文字列を格納/処理するのにANSI文字列(より正確にはShift-JIS)の形で行なっています。したがって、HSPでの関数呼び出しは、ほぼすべてANSI版の関数を使用することになります。
このページでは、本文中で関数名を示すときに、基本的には関数名の最後に"A"を付けないで説明しています。ただし、各API関数の説明のページでは"A"を付けて説明してあります。文字列を扱うAPI関数を呼び出すときには、そのことに注意してください。