WINAPI とは何か?

今日は WINAPI というキーワードをとりあげます。

先日の WinMain 関数の前に WINAPI というキーワードがついていたことに、 お気づきでしょうか?

こんな感じです。

#include <windows.h>

int WINAPI WinMain (
        HINSTANCE hInstance,
        HINSTANCE hPrevInstance,
        LPSTR lpCmdLine,
        int nCmdShow ) {

        MessageBox(
                NULL,
                TEXT("Hello, world!"),
                TEXT("Hello"),
                MB_OK | MB_ICONINFORMATION );

        return 0;
}

これは、なんでしょうか?

普通に C 言語を勉強してきても、こんな場所に何かを書いたり普通しませんよね。 この辺が Windows プログラミングの難しいところです。

WINAPI は「WinMain や Windows API を使うときの呪文だ」 と開き直っても 構わないと思います。それでもあまり問題になることはないからです。

ですが、全く問題にならないかというとそうでもなく、時々問題になったりしますから たちが悪いです。

実は WINAPI はマクロで __stdcall というキーワードを WINAPI という名前に してあります。

呼び出し規則について

__stdcall は関数を呼び出すときの、呼び出し規約 (Calling Convention) の種類のひとつです。

呼び出し規約というのは、関数を呼ぶ側 (caller) と呼ばれる側 (callee) で、スタックに変数をどのように 積むか、スタックを誰がクリーンアップするかなどを取り決めるものです。

x86 (32ビット) の頃は、呼び出し規約として何種類も混在していたので、外部モジュールを呼び出す場合にはある程度意識しておく 必要がありました。

ところが x64 向けのプログラムでは呼び出し規約はひとつしかなくなりました。 このため、呼び出し規約の違いという点については意識する必要がなくなりました。

x64 のプログラムをターゲットに開発することが多い現状では、 WINAPI などのキーワードは下位互換のためにつけると考えて OK です。

呼び出し規約について

以下、x86 での呼び出し規約と、その違いについて説明します。

現代向けの x64 アーキテクチャでの呼び出し規約では、レジスタを活用する形に変わりました。 これについては、また別の機会に書きたいとおもいます。

以下、一応、興味があるという方は目を通しておいたらいいのでは?程度に読んでください。

__stdcall とは?

__stdcall は、引数を右から左のパラメータを順に積み上げ、呼び出された側が スタックをクリーンアップします。

誤った呼び出し規則を指定した場合

これはどういうことかというと、具体的に考えるとわかりやすいです。

例えば、次のコードがあったとします。

void main() {
        int x = 1, y = 2;
        Foo(x, y);
}

Foo はあるベンダーから買ってきた foo.dll に実装されていると思ってください。 (main は自分の関数とします)

そして、Foo は __stdcall として実装されているとします。

Foo が __stdcall として実装されているということは、foo.dll の中には、Foo 関数 から戻るときに、スタックを巻き戻すコードが埋め込まれているということになります。

さてここで、Foo 関数を誤って、__cdecl 呼び出し規約に従うと思って使った場合に どうなるか考えてみましょう。

__cdecl 呼び出し規約では、引数を積み上げる順番は同じですが、スタックの巻き戻し が違います。__cdecl では呼び出し側がスタックを巻き戻します。

すると、Foo を呼び出した後、Foo は実際には __stdcall ですから、Foo 関数から 戻るときにスタックが巻き戻されます。そして、main 関数に制御が戻ると、 main 関数は Foo 関数を __cdecl の関数と認識していますから、スタックをさらに 操作して積み上げた引数分だけ巻き戻します。

必要以上に巻き戻されたスタックは破損してしまいます。

運良く実行を続けるときもありますが、たいていはプログラムがクラッシュしてしまいます。

このため、呼び出し規則は正しく指定する必要があります。

なぜ複数の呼び出し規則を使うの?

__cdecl は可変個引数をサポートするときに有効な呼び出し規則ですす。なぜなら、 引数の個数が呼び出すたびに変るとなると、呼び出し側しかスタックの巻き戻し量を 知らないからです。しかし、__stdcall の方が全体のプログラムサイズがグッと 小さくなります。

このため、あちこちで何度も呼び出される Windows API は __stdcall にしたいのです。

以上、呼び出し規則について簡単に見てきました。