Windows SDKにあるヘッダーで宣言されている一部のWin32 APIには、必要な宣言が抜けていたりすることがあります。
生成されたコードの動作には問題ありませんが、ループなどで高頻度の呼び出しが行われる場合にパフォーマンスの低下が気になる可能性はあります。
こういった宣言のうちの一つである__declspec(dllimport)
について述べます。
__declspec(dllimport)
の機能
__declspec(dllimport)
を関数の宣言に追加し、その関数を呼び出すコードを記述すると、ネイティブコードでは関数ポインタを直接参照するようになります。
実例
次のソースコードをコンパイルしてみるとします。
APIを2つ呼び出すだけのシンプルな処理です。
#include <Windows.h>
void mainCRTStartup()
{
Sleep(0);
ImmDisableIME(0);
}
期待されるコードはこのように___imp__
が付いているシンボルを直接参照している形式です。
関数名に___imp__
が付いたシンボルは、実行時にDLLファイルのエクスポートされている関数のアドレスが格納される変数(関数ポインター)として配置されます。
start:
push esi
xor esi, esi
push esi
call [___imp__Sleep@4]
push esi
call [___imp__ImmDisableIME@4]
pop esi
ret
x86をターゲットにした場合に生成されるコードはこのようになります。
x86_64では、関数名の最後に付与される@と数字がありません。
start:
push esi
xor esi, esi
push esi
call [___imp__Sleep@4]
push esi
call ImmDisableIME_0 // ワンクッション
pop esi
ret
ImmDisableIME_0:
jmp [___imp__ImmDisableIME@4]
Sleep
関数は___imp__Sleep@4
を参照していて、期待通りのコードになっています。
一方ImmDisableIME
関数は期待していたコードと異なっています。
call
命令でImmDisableIME_0
にいったん飛んだ後、直後にjmp
命令で___imp__ImmDisableIME@4
を参照するコードになり、ワンクッション置いた形となっています。
期待通りにならなかった理由
Sleep
関数とImmDisableIME
関数の呼び出しが、ネイティブコードで形式が変わってしまう原因は、これらの関数宣言にあります。
Sleep
関数はこのように宣言されています。
WINBASEAPI VOID WINAPI Sleep(DWORD);
ImmDisableIME
関数はこのように宣言されています。
BOOL WINAPI ImmDisableIME(DWORD);
違いを見比べるとWINBASEAPI
の記述の有無に気づきます。
WINBASEAPI
はマクロとなっていて、その宣言はこのようになっています。
#define WINBASEAPI DECLSPEC_IMPORT
これもまたマクロなのでさらに追いかけてみます。
#define DECLSPEC_IMPORT __declspec(dllimport)
この宣言です。
Sleep
関数には、__declspec(dllimport)
が宣言に含まれているため、期待したコードになっています。
ImmDisableIME
関数は宣言が含まれていないので、期待したコードにはなりませんでした。
期待通りのコードにするには
__declspec(dllimport)
を追加する。
それだけで済めばどんなに楽だったか
しかし、既に宣言されているプロトタイプを上書きすることは出来ません。
たとえばImmDisableIME
関数のプロトタイプを修正しようとしても
/* 元々の宣言 */
BOOL WINAPI ImmDisableIME(DWORD);
/* 修正後 */
__declspec(dllimport) BOOL WINAPI ImmDisableIME(DWORD);
このままだとシンボルの重複でコンパイルエラーとなります。
C++言語での解決法
名前空間とextern "C"の合わせ技で同じ関数をプロトタイプを変更して宣言できます。
namespace Microsoft
{
extern "C"
{
__declspec(dllimport) BOOL WINAPI ImmDisableIME(DWORD);
}
}
使う方は名前空間を指定して呼び出します。
void mainCRTStartup()
{
Sleep(0);
Microsoft::ImmDisableIME(0);
}
C++言語で開発していれば、これで修正された方の関数宣言を参照するようなります。
なお、Visual Studioではこの方法を用いると「関数本体の記述がない」という修正項目が出てしまうのが難点です。
C言語での解決法
C++と違い、名前空間はありません。
そこで、LIBファイルのシンボル名を直接指定する方法を用います。
これは外部の関数ポインタを宣言するのと同じです。
extern BOOL (WINAPI * __imp_ImmDisableIME);
使う方は関数を__imp_
を付与して関数ポインタと同じ要領で呼び出します。
void mainCRTStartup()
{
Sleep(0);
(*__imp_ImmDisableIME)(0);
}
x86_64やARMなら…
これでおしまい。
x86は…
関数が__cdecl
宣言で記述されていれば問題ありません。
__stdcall
宣言は残念ながらどうしようもありません。
__stdcall
宣言の仕様として、関数名の最後に@と引数の数を4倍した数字が付加されてしまいます。
Win32 APIは総じて__stdcall
宣言がなされており、名前として使用できない@を記述できないので参照できません。
じゃあインラインアセンブラ使えば…
インラインアセンブラでも@を使うとエラーになります。
__asm
{
push 0;
call [___imp__ImmDisableIME@4];
}
error C2018: unknown character '0x40'
error C2400: inline assembler syntax error in 'first operand'; found 'constant'
諦めろ。
諦めきれない場合は、該当する関数に__declspec(dllimport)
を付け加えた自作ヘッダを書きましょう。
最後に
Microsoftさんちゃんとしてください。