ホーム < ゲームつくろー! < C++踏み込み編 < スタティックライブラリとUnicodeとマルチ文字の罠
その6 スタティックライブラリとUnicodeとマルチ文字の罠
自作のクラスをスタティックライブラリ(もしくはDLL)にまとめていくのは非常に楽しくて、また大切な作業です。しかし、外部リンクと言うのは非常に分かりにくいバグを生む要因の1つでもあります。そして、その理由を他の人に聞いても殆ど回答を得られない1つでもあります。それは、構文的なエラーではなくて、宣言と実装の結合(リンク)の問題であることから、エラー箇所を簡単には指摘できないからです。
今回はそんな中でVisula Studio 2003辺りから面倒なことになってきたUnicodeとマルチバイトで大いにはまった例をお伝えします。これ、実録です。
@ スタティックライブラリとは?
スタティックライブラリとは「実装部分をまとめたファイル」です。実装部分と言うと「.cpp」という拡張子が付いた実装ファイルがありますが、それとスタティックライブラリは何が違うのでしょうか?また、実装ファイルがあるのにどうしてスタティックライブラリが必要なのか?この答えは結構明確です。ヘッダーファイル(.h)は1つのフォルダにまとめ、そのフォルダへパスを通すことによってプロジェクトに追加しなくても自動参照されるようになります(インクルードは必要です)。一方、実装ファイル(cpp)はフォルダにまとめてパスを通しても、プロジェクト内でそれを参照してくれません。ですから、新しいプロジェクトを立ち上げる時には、毎回必要な実装ファイルをプロジェクトに追加する必要が出てきます。これが非常に大規模なプロジェクトでになると、プロジェクトは実装ファイルだらけになってしまうんです。そうなると、何が新しくて何が既存なのかさっぱりわからなくなります。ところがスタティックライブラリは、次のようにすることでコンパイラが自動的に参照してくれます:
#pragma comment(lib, "MyLibrary.lib")
MyLibraryには必要な実装部分がごっそり入っていますが、プロジェクトにこのライブラリを追加する必要はありません。これにより、プロジェクト内には新しく追加したヘッダーファイルや実装ファイルだけしか無い事になります。これは非常にすっきりとするんです。
別の理由として、他の人に実装部分を隠蔽したい事があります。プログラムを勝手に変えられると困るわけです。プロジェクトに追加されなければヘッダーファイルも変更される事がないため、安全にライブラリを提供できます。DirectXは正にそういう形で全てのライブラリを提供してくれています。
スタティックライブラリの具体的な作成方法につきましては、クラス構築編「ライブラリ化の勧め」をご覧下さい。
A スタティックライブラリの天敵「リンカエラー」
実装部分の塊であるスタティックライブラリーは、使用者からその内容の一切を隠蔽してしまいます。使用者が確認できるのは、関数やクラスのプロトタイプ宣言をしているヘッダーファイルだけです。こうなりますと、見えないスタティックライブラリ内に実装部が無いことも考えられます。この時に出現するのが「リンカエラー」です。リンカエラーは、宣言部で宣言した関数やクラスの実装が無い場合に出現します。
リンカエラーの理由は様々ですが、ひっくるめて「実装部が無い」という事に原因は帰着します。実装が無いという意味は様々です。例えば、
void MyFunction( int val );
という宣言部に対して、
void MyFunction( int &val )
{
val = 100;
}
という実装をスタティックライブラリに入れてしまうとリンカエラーとなります。引数の型が違いますね。「でも、これはコンパイルエラーになるでしょ?」と思われるかもしれませんが、実はスタティックライブラリは実装ファイルのみで作成できます。ヘッダーファイルはいらないのです。ですから、上のような食い違いはありえます。
他には次のような場合も考えられます。
namespace MYLIB
{
void MyFunction( int val );
}
namespaceでくくった宣言部を定義した状態で、
void MyFunction( int val )
{
val = 100;
}
と実装します。関数の引数や戻り値の関係は正しいのですが、実装部はグローバルスコープで定義されているため、MYLIB::MyFunction関数の実体は「ありません」。これもリンカエラーが出ます。
大変分かりにくいのがタイプミスです。例えば次のような宣言と実装です。
namespace MYLIB
{
void MYFunction( int val );
}
namespace MYLIB
{
void MyFunction( int val )
{
val = 100;
}
}
間違い探しのようですが、MYFunction関数がMyFunction関数として実装されてしまっています。これは本当に分からないもんなんです。
このように、スタティックライブラリはリンカエラーとの戦いでもあります。
B 汎用変数の罠
VC++やVisual Studio 2003, 2005という辺りから、Unicodeに対して汎用化が頻繁に行われるようになりました。実際、マルチ文字を扱うchar型とワイド文字を扱うwchar_t型という2つの型が存在するようになりました。char型は1バイト、wchar_t型は2バイトで、双方に互換性はありません。大抵はマルチバイト文字コードかUnicode文字コードのどちらかしか扱いませんが、どちらでも対応できるように「汎用変数」が沢山用意されています。char型とwchar_t型をコンパイラが(正しくは統合環境が与えるマクロ定数_UNICODEの有無で)自動的に切り替えるのが_TCHAR汎用型です。これがありますと、
_TCHAR *str = _T("テスト");
という記述で両方の型に対応が出来ます。非常に便利です。
ところが、これがスタティックライブラリになりますと、もう本当に分からないバグになることがあります。事実、私はこのバグが発生してからまるまる3日も悩まされ続けました(T_T)
次のような宣言と実装をしたとします(これは実装例です)。
// ヘッダーファイル内
namespace MYLIB
{
size_t StrLen( _TCHAR *str );
}
// 実装ファイル内
namespace MYLIB
{
size_t StrLen( _TCHAR *str )
{
return _tcslen( str );
}
}
両方ともプロジェクトに追加してコンパイルすると完璧に通ります。つまり、宣言と実装の関係は揃っています。この状態でライブラリファイルMyLib.libを作成しました。さて、このライブラリファイルを実際に使ってみます。
#pragma comment(lib, "MyLib.lib")
#include <MyLib.h>
using namespace MYLIB;
int main()
{
_TCHAR *MyName = _T("IKD");
size_t NameSize = StrLen( MyName );
return 0;
}
MyLib.libライブラリをpragmaディレクティブで読み込み、ヘッダーファイルを宣言しています。ついでにnamespaceもMYLIBとしています(using namespace)。これで、StrLen関数はグローバル関数であるかのように使うことが出来ます。main関数内は実際それを見込んで関数を使っています。
ところが、これでコンパイルするとリンカエラーが出てしまいました。宣言も、実装も、namespaceも全てしっかりと正しくしているんです。しかし、リンカエラーが出ます。さらに不思議なことに、別のプロジェクトを立てて上と同じように実装すると、なんと今度はリンカをパスしてしまいました。実行すると、ちゃんと文字数が返ります。うまく行く時と行かない時があるバグほどやっかいなものはありません。それはそれは悩みました。
C 汎用は両用ではなく、スタティックは「静的」なものでした
このリンカエラーの原因がわかったのは実装から2日以上経過した時でした。わかってしまえば「なんだよぉ〜」と思うことなんですが、わからないとホント酷いもんです。原因は「使用時の文字コードのフラグ」にありました。
まず、スタティックライブラリは「静的な関数定義」です。簡単に言えば関数の引数や戻り値はライブラリ生成時に固定されます。一方、_TCHAR型というのは「汎用型」ではありますが、マルチ文字とワイド文字を両方扱える両用型ではなく_UNICODEマクロの有無で振り分けられています。
tchar.h内
#if _UNICODE
#define wchar_t _TCHAR
#else
#define char _TCHAR
#endif
スタティックライブラリを作成する時、デフォルトのコンパイラオプションでライブラリ作成を行っていました。この時のデフォルトは「Unicode文字列を使用する」という設定でして、_TCHAR型=wchar_t型としてライブラリに固定されます。それに対して、リンカーエラーが出た時のプロジェクトは「ワイド文字を使用する」という状態だったんです。スタティックライブラリは静的なものなので、StrLen関数の引数はwchar_t型と決められてしまっています。それに対してプロジェクトで宣言したヘッダーファイル内のStrLen関数の引数は、_UNICODEフラグが無いため普通のchar型としてコンパイラが認識しました。ライブラリとヘッダーファイルで文字変数の引数の型が異なるため、実装部がない→リンカエラーとなったわけです。
わかってしまえば「なんだ〜」ですが、本当にこれを見つけるのは大変でした。これは1つの教訓でもあります。すなわち、
「スタティックライブラリを作成する時に、1つの関数で両文字コードに対応させてはいけない」
ということです。両方のモードに対応させるには、面倒でも両方の引数を取れる関数を用意しなければならないわけです。例えばDirect3DXのID3DXFont::DrawText関数はUNICODEフラグがあるか無いかで次のように関数を振り分けています。
#ifdef UNICODE
#define DrawText DrawTextW
#else
#define DrawText DrawTextA
#endif // !UNICODE
DrawText関数の実体はDrawTextW関数かDrawTextA関数のどちらかであるわけです。そして、両関数をライブラリ内で実装しています。そうすれば、どちらの文字コードが使用されても同じ関数名で振舞うことができるようになります。大変勉強になる実装例だと思います。
今後汎用的なライブラリを作成するのであれば、Unicode文字コードとワイド文字コードの両方に対応しなければなりません。この時、汎用変数を使っても良いのですが、両方の文字コードに対応する関数を定義して、上のように1つの関数名に「define」する。これが定石になってくるでしょう。本当に、今回のバグ取りは泣きました(T_T)