一部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さんちゃんとしてください。