EternalWindows
DLL / DLLとUNICODE

今回は、DLLで文字列を要求する関数を実装します。 まず、次の例を考えます。

DLLAPI void ErrorMessage(LPTSTR lpsz)
{
	MessageBox(NULL, lpsz, NULL, MB_ICONWARNING);
}

この関数は、MessageBoxの呼び出しをラッピングしており、 エラー通知のメッセーボックスを表示することが目的です。 この関数は引数としてLPTSTRという型を受け取っていますが、 この型の実体はLPSTR(char *)かLPWSTR(wchar_t *)のいずれかでした。 このコンパイルによって型が変化するという点が、今回の重要なポイントになります。

まず、DLLをUNICODE定義でコンパイルしたとします。 これにより、LPTSTRはLPWSTRになります。 つまり、ErrorMessageはLPWSTRの型を受け取る関数になったということです。 次にEXEですが、こちらはUNICODEの定義なしでコンパイルしたものとします。 これにより、ヘッダーファイルに定義されているErrorMessageの引数はLPSTRとなり、 EXEはErrorMessageにLPSTR型のアドレスを指定してコンパイルするでしょう。 このコンパイルは問題なく成功するでしょうが、実行結果は期待通りにいきません。 実際に呼び出すことのなるErrorMessageは、LPWSTRの型を受けとることになっているため、 LPSTR型の文字列を受け取っても正しく機能しないのです。

DLLの視点から見れば、できるだけEXEに自由度を持たせないため、 UNICODEの定義に関わらず正しく動作しなければなりません。 そうするには、DLLがANSI版の関数とUNICODE版の関数の両方を実装するしかないと思われます。 そのようなコードは、たとえば次のようになるでしょう。

DLLAPI void ErrorMessageA(LPSTR lpszAnsi)
{
	MessageBoxA(NULL, lpszAnsi, NULL, MB_ICONWARNING);
}

DLLAPI void ErrorMessageW(LPWSTR lpszUnicode)
{
	MessageBoxW(NULL, lpszUnicode, NULL, MB_ICONWARNING);
}

このようにすれば、EXEはUNICODEが定義されていないときにはErrorMessageAを呼び出し、 定義されているときはErrorMessageWを呼び出せばよいことになります。 しかし、EXEが文字列を要求する関数を呼び出す度に、 UNICODEが定義されているどうかを調べるのは大変煩わしいものです。 そこで、ヘッダーファイルに少し工夫を施します。

DLLAPI void ErrorMessageA(LPSTR);
DLLAPI void ErrorMessageW(LPWSTR);

#ifdef UNICODE
#define ErrorMessage ErrorMessageW
#else
#define ErrorMessage ErrorMessageA
#endif

UNICODEが定義されているときは、ErrorMessageWをErrorMessageとして定義します。 これはつまり、コードでErrorMessageと書くとErrorMessageWと解釈されるということです。 ということは、EXEはErrorMessageと書くだけで、 UNICODEの定義に応じて適切な関数を呼び出したことになるのです。 これにより、関数の最後に冗長なAやWを付けることもなくなり、 UNICODEが定義されているかどうかを気にする必要もありません。 唯一、注意しなければならないのは、ErrorMessageという名前の関数は実際には存在しないということぐらいでしょう。 GetProcAddressで関数に明示的にリンクするときなどは、 ErrorMessageWやErrorMessageAという実際に存在する関数名を指定します。

以下、今回のプログラムです。 ヘッダーファイル、ソースファイルの順に示します。

#ifndef DLLAPI
#define DLLAPI extern "C" __declspec(dllimport)
#endif

DLLAPI void ErrorMessageA(LPSTR);
DLLAPI void ErrorMessageW(LPWSTR);

#ifdef UNICODE
#define ErrorMessage ErrorMessageW
#else
#define ErrorMessage ErrorMessageA
#endif

続いてソースファイルです。

#define DLLAPI extern "C" __declspec(dllexport)

#include <windows.h>
#include "mydll.h"

DLLAPI void ErrorMessageA(LPSTR lpszAnsi)
{
	MessageBoxA(NULL, lpszAnsi, NULL, MB_ICONWARNING);
}

DLLAPI void ErrorMessageW(LPWSTR lpszUnicode)
{
	MessageBoxW(NULL, lpszUnicode, NULL, MB_ICONWARNING);
}

プログラムの内容は既に説明しているので特に問題はないでしょう。 同じ目的の関数を2つ用意するというのは煩わしいことですが、 これによりEXEの開発を快適に行うことができるのです。

今回紹介した手順は、WindowsAPIを定義しているヘッダーファイルも用いています。 たとえば、LoadLibraryの定義は次のようになっています。

WINBASEAPI
HMODULE
WINAPI
LoadLibraryA(
    IN LPCSTR lpLibFileName
    );
WINBASEAPI
HMODULE
WINAPI
LoadLibraryW(
    IN LPCWSTR lpLibFileName
    );
#ifdef UNICODE
#define LoadLibrary  LoadLibraryW
#else
#define LoadLibrary  LoadLibraryA
#endif // !UNICODE

※VC 2003のwinbase.hより抜粋

このように定義を工夫していますが、その実装にはさらなる工夫が施されています。 関数を2つ用意するというのは、そっくりなコードが2つ存在するのと同じことです。 そのため、一方の関数に実際に処理するコードを書き、 もう一方の関数がその関数を呼び出すようにしたほうが明らかに効率的です。 WindowsAPIでは原則として、関数の実際の処理を行うのはUNICODE版の関数です。 ANSI版の関数を呼び出すと、その内部で文字列をUNICODEに変換してから、 UNICODE版の関数を呼び出すという処理が用いられています。 この変換過程には、メモリがプロセスのデフォルトヒープから確保されるなどの わずかなパフォーマンスの低下が発生しているようなこともあり、 基本的にプログラムはUNICODEとしてコンパイルするほうが好ましいと思われます。


戻る