LoginSignup
6
3

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-12-19

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

6
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
3