Windows
Win32API

一部Win32APIの__declspec(dllimport)抜け問題

Windows SDKにあるヘッダーで宣言されている一部のWin32 APIには、必要な宣言が抜けていたりすることがあります。

生成されたコードの動作には問題ありませんが、ループなどで高頻度の呼び出しが行われる場合にパフォーマンスの低下が気になる可能性はあります。

こういった宣言のうちの一つである__declspec(dllimport)について述べます。

__declspec(dllimport)の機能

__declspec(dllimport)を関数の宣言に追加し、その関数を呼び出すコードを記述すると、ネイティブコードでは関数ポインタを直接参照するようになります。

実例

次のソースコードをコンパイルしてみるとします。
APIを2つ呼び出すだけのシンプルな処理です。

test.cpp
#include <Windows.h>
void mainCRTStartup()
{
  Sleep(0);
  ImmDisableIME(0);
}

期待されるコードはこのように___imp__が付いているシンボルを直接参照している形式です。
関数名に___imp__が付いたシンボルは、実行時にDLLファイルのエクスポートされている関数のアドレスが格納される変数(関数ポインター)として配置されます。

expected.asm
start:
  push esi
  xor  esi, esi
  push esi
  call [___imp__Sleep@4]
  push esi
  call [___imp__ImmDisableIME@4]
  pop  esi
  ret

x86をターゲットにした場合に生成されるコードはこのようになります。
x86_64では、関数名の最後に付与される@と数字がありません。

generated.asm
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関数はこのように宣言されています。

synchapi.h
WINBASEAPI VOID WINAPI Sleep(DWORD);

ImmDisableIME関数はこのように宣言されています。

imm.h
BOOL WINAPI ImmDisableIME(DWORD);

違いを見比べるとWINBASEAPIの記述の有無に気づきます。
WINBASEAPIはマクロとなっていて、その宣言はこのようになっています。

apisetcconv.h
#define WINBASEAPI DECLSPEC_IMPORT

これもまたマクロなのでさらに追いかけてみます。

winnt.h
#define DECLSPEC_IMPORT __declspec(dllimport)

この宣言です。
Sleep関数には、__declspec(dllimport)が宣言に含まれているため、期待したコードになっています。
ImmDisableIME関数は宣言が含まれていないので、期待したコードにはなりませんでした。

期待通りのコードにするには

__declspec(dllimport)を追加する。

それだけで済めばどんなに楽だったか

しかし、既に宣言されているプロトタイプを上書きすることは出来ません。
たとえばImmDisableIME関数のプロトタイプを修正しようとしても

test.cpp
/* 元々の宣言 */
BOOL WINAPI ImmDisableIME(DWORD);
/* 修正後 */
__declspec(dllimport) BOOL WINAPI ImmDisableIME(DWORD);

このままだとシンボルの重複でコンパイルエラーとなります。

C++言語での解決法

名前空間とextern "C"の合わせ技で同じ関数をプロトタイプを変更して宣言できます。

declare.cpp
namespace Microsoft
{
  extern "C"
  {
    __declspec(dllimport) BOOL WINAPI ImmDisableIME(DWORD);
  }
}

使う方は名前空間を指定して呼び出します。

test.cpp
void mainCRTStartup()
{
  Sleep(0);
  Microsoft::ImmDisableIME(0);
}

C++言語で開発していれば、これで修正された方の関数宣言を参照するようなります。
なお、Visual Studioではこの方法を用いると「関数本体の記述がない」という修正項目が出てしまうのが難点です。

C言語での解決法

C++と違い、名前空間はありません。
そこで、LIBファイルのシンボル名を直接指定する方法を用います。
これは外部の関数ポインタを宣言するのと同じです。

declare.c
extern BOOL (WINAPI * __imp_ImmDisableIME);

使う方は関数を__imp_を付与して関数ポインタと同じ要領で呼び出します。

test.c
void mainCRTStartup()
{
  Sleep(0);
  (*__imp_ImmDisableIME)(0);
}

x86_64やARMなら…

これでおしまい。

x86は…

関数が__cdecl宣言で記述されていれば問題ありません。

__stdcall宣言は残念ながらどうしようもありません。
__stdcall宣言の仕様として、関数名の最後に@と引数の数を4倍した数字が付加されてしまいます。
Win32 APIは総じて__stdcall宣言がなされており、名前として使用できない@を記述できないので参照できません。

じゃあインラインアセンブラ使えば…

インラインアセンブラでも@を使うとエラーになります。

test.cpp
__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さんちゃんとしてください。