contents

  1. STLとDLLの相性問題
  2. リソースの外部DLLへの分離
  3. MAKEINTRESOURCEの謎

DLLについてあれこれ

Windowsにおいて、DLLを使用する際の問題やテクニックを紹介します。

STLとDLLの相性問題

なぜ問題が発生するのか?

例えば、std::stringをDLL関数に参照渡しをし、それをDLL関数内で変更を加えると、segmentation fault が発生してしまいます。 これは、EXE側で確保したメモリをDLL側で解放してしまった(もしくは、その逆)ためです。 通常のライブラリならば、オブジェクトに加える操作によって発生する内部的なメモリの確保・解放は、「コンテナライブラリのメモリ空間」で行われます。 しかし、STLはインライン展開されますので、呼び出すコードにメモリ操作ルーチンが埋め込まれます。 よって、メモリの確保・解放がDLLとEXEにまたがってしまうことがあるのです。

DllAllocator

この問題を防ぐには、コンテナやstringがDLL・EXEともに同じメモリ空間を使うようにする必要があります。 STLはすべてallocatorを使用してメモリを確保しますので、allocatorを独自に定義してやればよいことになります。 このためには、まず、DLL内部でメモリの確保・解放する関数を用意します。 この関数は、単純に内部でmalloc(), free()するだけでかまいません。 もしくは、CRTを使用しないメモリ確保関数(CoTaskMemAlloc(), GlobalAlloc())を使う手もあります。

次に、std::allocatorからクラスを派生させ、allocate()deallocate()をオーバーライドして、先ほどの関数を呼んでやります。 詳しくはソースコードをご覧ください。 使い方は、typedef std::basic_string<TCHAR, std::char_traits<TCHAR>, DllAllocator<TCHAR> > String;のようにしてやれば、 このStringはDLL, EXE間で渡すことができるようになります。

DllAllocator.h DllAllocator.cpp

そもそもSTLはバイナリをまたがるべきではない

実際には、メモリに加え、クラスのバイナリ配置が一致しなくてはなりません。 つまり、DLLとそれを使用するEXEを、同一のSTLを使い同一のコンパイラで作成する必要があります。 しかし、そもそものDLLの目的から言って、これは仮定できないことです。 DLLからエクスポートしたり、引数・返値として使われる型は、インタフェースかPOD型であるべきです。 バイナリ標準が規定されていないC++の不十分な部分であることは否めません。

リソースの外部DLLへの分離

概要

ゲームなどを作るとき、使用する画像や音などのリソースを普通のファイルのままにしておくのは少し気が引けます。 EXEにリソースとして持たせてしまうと、EXEを少し修正しただけでリソースを含むすべてを置き換えなくてはなりません。 そこで、リソースを外部に分離する要求が生じるのですが、このリソースアーカイブにはDLLを使うのが手軽です。

メリット・デメリット

○管理の容易さ
リソース名・リソースタイプをキーとし、データポインタ・データサイズを検索する連想コンテナのように扱えます。 また、LockResource()したリソースは、UnlockResource()しなくてもかまいません;データの明示的な解放は必要ありません。 カスタムリソースの使用は、以下のような流れになります。
HINSTANCE hDllRsc = LoadLibrary("resource.dll");
HRSRC hRsc = FindResource(hDllRsc, rscName, rscType);
void* data = LockResource(LoadResource(hDllRsc, hRsc)); // Unlock()は必要なし。
DWORD size = SizeofResource(hDllRsc, hRsc);
DoSomething(data, size);
FreeLibrary(hDllRsc);
○賢い?読み込み機構
5MBほどのリソースDLLをLoadLibrary()しても、その時点で5MB消費するわけでは無いことを確認しました。 どうやら、DLL全体を一気に読み込むのではないようです。 それほどシビアなリソース管理が必要無ければ、楽できます。
○基本的なデータタイプならば、さらに簡単に扱える
文字列、ビットマップ、アイコン、メニュー、Waveなどは、それぞれ専用の関数 LoadString(), LoadBitmap(), LoadImage(), LoadMenu(), PlaySound()を使うことで、直接データを取得できます。 特に文字列はテーブル化されるので扱いやすいでしょう。
また、Direct3D(テクスチャとして)や、DirectMusic(演奏データとして)も使用できます。 ただし、MCIとの相性はいまいちでした。
×巨大なDLLをうまく扱えるかどうかは未確認
たとえば、ノベル系のゲームなどで、全画像ファイル100MBぶんを一つのDLLにしてしまったとき、 それをうまく扱えるかは確認していません。 数十MBレベルになると、さすがに心配ですが……どうなんでしょう?
×DLLを更新するのに再コンパイルが必要
ほかの形式でもリソースコンパイラは必要なので、これ自体は回避不可能な問題ではあります。 ただ、VC++のリソースエディタは使いにくいので、.rcをテキストとして直接編集する必要があるかもしれません。
×Windowsに依存している
せっかくSDLなどのマルチプラットホームライブラリをつかっても、データがOSに依存したら意味がありません。

リソースのみのDLLの作成方法

もちろん普通のDLLとして作ってもかまわないのですが、どうせ実行部分がないのですから、無駄な部分を省いてしまいましょう。 VC++7.0の場合は、DLLプロジェクトのプロパティ>リンカ>詳細>リソースのみのDLLにチェックを入れます。 これは、リンカに /NOENTRY オプションを指定することと同じです。 VC++6.0以前の場合は直接このオプションを加えてやればよいと思われます。

リソースのIDには文字列も使えるんですよ!

VCのリソースエディタを使っていると、リソースIDには数値しか使えないように誤解しやすいですが、 名前の部分にダブルクォートで囲った文字列を打ち込んでやれば、IDとして文字列を使えます。 数値を使うと、リソース側とメイン側で値がずれないようにしなくてはいけませんので、文字列による管理のほうが楽でしょう。

実装例

プロジェクト+サンプル ResourceLoader.h ResourceLoader.cpp

以下のような特徴があります。

リソースDLLとファイルをシームレスに扱える
「デバッグ時には変更しやすいようにファイルから、リリース時にはユーザが触れないようにDLLから」 といったことが可能です。
リソースの部分アップデートが可能
ファイル名でソートしたときに後に来る名前のDLLを追加することで、そちらを先に検索するようになります。 同名のリソースを入れておけば、リソースを部分的に上書きすることができます。 アップデートファイルを配布する際、余計な分を配布せずに済みます。

MAKEINTRESOURCEの謎

MSDNを見ると、

#define MAKEINTRESOURCE(i) (LPTSTR)((DWORD)((WORD)(i)))

と書いてあるんですよね(VS.NETではWin64対応のためか少し異なる)。 普通は、こんなキャストをしたら間違いなくsegfaultです。 しかし、Windowsではユーザーが0x0000xxxxの位置にあるメモリを使用しないことを利用して、HIWORD(arg) == 0x0000の場合は文字列ではない=リソースIDであるとみなして処理してくれるみたいです。 IS_INTRESOURCE()はまさに上位ワードが0かどうかを調べるマクロです。 リソースを数値IDと文字列IDの2通りで管理できるのは、この機構のためです。 便利なんですが、気持ち悪くないって言えばウソになりますね……。

これに関連するのですが、VC++では、多重継承をしなければ、たぶん、sizeof(メンバ関数ポインタ) == 4 です。 ですが、単純に考えると、virtualの場合と非virtualの場合があるので、DWORDひとつで管理できるわけがありません。 実際、gccでは8バイトですし。 これも、上記の判別法を用いているんじゃないかと思います。 いくらなんでも仮想関数テーブル内でのインデックスが65535を超えることはないでしょうから。 ……と思ったのですが、thunk が作成され、そのアドレスが渡されるようです。 gccの場合は、マルチプラットホームに対応させないといけないので、汎用的な管理方法をとっているのでしょう。

おまけ。リソースを読み込む関数の引数にこんなのはいかが?