Seaside Laboratory

Posts

Windows アプリケーションの 64 ビット化

CPU と OS の 64 ビット化が進み、気兼ねなく 64 ビットアプリケーションを作成できる環境は整ったが、何の問題もなく 32 ビットアプリケーションが動いてしまうので移行する必要性をあまり感じない。世の中には 32 ビット時代に作られたアプリケーションが大量に溢れているので、手厚いサポートが引き続き行われるであろう、という目算もある。

とは言え、32 ビットサポートがいつまでも続く保証がないのと、先延ばしをしていると移行する機会を失ってしまいそうなので、自作のアプリケーションを 64 ビットに対応させることにした。

64 ビット化の利点

多くのアプリケーションにとって 64 ビットはオーバースペックなので、64 ビット化で得られる恩恵は少ない。

扱えるメモリ量の増加

64 ビット化の大きな特徴はアドレス空間の拡大によって扱えるメモリの量が格段に増えること。

64 ビットオペレーティングシステムでは、32 ビットオペレーティングシステムよりもはるかに多くの物理メモリがサポートされています。たとえば、ほとんどの 32 ビット Windows システムでは、最大 4 ギガバイトの物理メモリがサポートされ、各プロセスで最大 3 ギガバイトのアドレス空間がサポートされますが、64 ビット Windows では、プロセスごとに最大 2 テラバイトの物理メモリがサポートされ、8 テラバイトのアドレス空間がサポートされます。

動画編集ソフトのような巨大なデータを扱うアプリケーションにとっては重要なポイントとなる。

オーバーヘッドの低減

64 ビット版 Windows には WOW64 というサブシステムがあり、このサブシステムのおかげで 32 ビットアプリケーションをシームレスに動かすことができる。

WOW64 は x86 エミュレーターであり、32 ビット Windows ベースのアプリケーションを 64 ビット Windows でシームレスに実行できます。

Win32 API の呼び出しを 64 ビット版に変換したり、レジストリのリダイレクトを行ったりするので、当然オーバーヘッドが存在する。

AMD64 および Intel 64 アーキテクチャのプロセッサは 32 ビット命令をネイティブに実行できるため、64 ビット OS でも 32 ビットアプリケーションをフルスピードで実行できます。オペレーティングシステム関数を呼び出すとき、32 ビットと 64 ビットの間でパラメーターを変換するのに多少のコストがかかりますが、このコストは通常無視できます。

オーバーヘッドは大きなものではないが、無いに越したことはない。

データモデル

データ型のサイズには ILP32 (Int/Long/Pointer 32 bits) と LLP64 (Long Long/Pointer 64 bits) があり、64 ビット版の Windows では LLP64 が採用されている。

32 ビットプログラミングモデル (ILP32 モデルと呼ばれる) では、int、long、ポインターのデータ型のサイズは 32 ビットです。ほとんどの開発者は、このモデルを意識せずに使用しています。Win32 API の歴史において、これは妥当な想定でした。

64 ビット Windows では、このデータ型のサイズが同じであるという想定は正しくありません。ほとんどのアプリケーションではサイズを増やす必要がないため、すべてのデータ型を 64 ビット長にすると領域が無駄になります。ただし、アプリケーションには 64 ビットデータへのポインターが必要であり、場合によっては 64 ビットデータ型を持つ機能が必要です。これらの点を考慮し、LLP64 (または P64) と呼ばれる抽象データモデルが選択されました。LLP64 データモデルでは、ポインターのみが 64 ビットに拡張されます。他のすべての基本データ型 (int と long) のサイズは 32 ビットのままです。

Windows におけるプログラムの 64 ビット対応は、ポインター関連項目の修正とほぼ同義であると言ってよい。

プロジェクト設定の変更

コードを修正する前にプロジェクト設定を 64 ビットに切り替える必要がある。設定方法は Visual Studio Express 2013 を前提としたものとなっているので、他のバージョンでは設定名等に違いがあるかもしれない。

プラットフォームの追加と変更

構成マネージャーを開き「アクティブソリューションプラットフォーム」にある「新規作成」から「x64」の追加を行い、「プロジェクトのコンテキスト」側でも同様の操作を行う。

32 ビット版バイナリを作る予定はなかったので「編集」から「Win32」を削除した。

出力先の変更

プラットフォームを x64 に変更すると、ビルド結果が "x64\Debug" といったフォルダーに出力されるようになる。プロジェクト設定を確認したところ、各出力先が自動的に切り替わっていた。

設定項目 Win32 x64
出力ディレクトリ $(SolutionDir)$(Configuration)\ $(SolutionDir)$(Platform)\$(Configuration)\
中間ディレクトリ $(Configuration)\ $(Platform)\$(Configuration)\

32 ビット版が不要な場合は、Win32 と同じ設定にしておくとフォルダー構成がシンプルになる。

マクロ

プラットフォームに応じて以下のマクロが自動的に定義される。

マクロ 説明
_WIN64 64 ビットプラットフォーム。これには、x64 と ARM64 の両方が含まれます。
_WIN32 32 ビットプラットフォーム。この値は、下位互換性のために 64 ビットコンパイラによっても定義されます。
_WIN16 16 ビットプラットフォーム

勘違いしがちなのが以下の 2 点。

  • _WIN32 はプラットフォームが x64 であっても定義される。
  • _WIN32 と WIN32 は別のマクロ。

WIN32 はプロジェクト作成時に自動設定されるマクロで、プラットフォームの切り替えとは全く関係がない。ドキュメントが存在しないので正確なところは分からないが、調べたところ「アンダースコアなしのマクロはユーザー定義のマクロと衝突するのでアンダースコア付きのマクロに移行された」といったことが書かれていた。

WIN32 マクロがなくともビルドは正常に行われたので削除することにした。

Win32 API の 64 ビット対応

Win32 API は汎用的なデータの受け渡しに DWORD や LONG を使っているので、インスタンスを渡したいときはポインターを整数にキャストしてから渡し、受け取り側で再び元の型にキャストして復元する、ということを行う。ただ、ポインターのサイズは LLP64 だと 64 ビットになるので、32 ビットの DWORD に代入すると切り捨てが発生してしまう。この問題に対応するため、64 ビットのポインターを保持できるデータ型や関数が用意されている。

新しいデータ型

主に使われているポインター関連のデータ型は以下の 4 つ。

項目 説明
DWORD_PTR ポインターの有効桁数の符号なし long 型。
LONG_PTR ポインターの有効桁数の符号付き long 型。
UINT_PTR 符号なし INT_PTR。
ULONG_PTR 符号なし LONG_PTR。
ウィンドウ関連の関数

ウィンドウ関連の関数は、末尾に Ptr が付いたバージョンが追加されている。

ポインターを含むウィンドウまたはクラスのプライベートデータがある場合、次の新しい関数を使用する必要があります。

・GetClassLongPtr
・GetWindowLongPtr
・SetClassLongPtr
・SetWindowLongPtr

以下は、SetWindowLong と SetWindowLongPtr の宣言になるが、

LONG SetWindowLong(
    HWND hWnd,
    int  nIndex,
    LONG dwNewLong
);
LONG_PTR SetWindowLongPtr(
    HWND     hWnd,
    int      nIndex,
    LONG_PTR dwNewLong
);

引数と戻り値の型が LONG から LONG_PTR に変更されているのが分かる。

また、

これらの関数は 32 ビットと 64 ビットの両方の Windows で使用できますが、64 ビット Windows では必須です。

と書かれているように 32 ビットコードでも動作するので、64 ビットに対応するつもりが無くても先行して切り替えておくのがおすすめ。

ウィンドウ関連の定数

古い形式の関数呼び出しを検出するため、ウィンドウ関連の定数は一部が無効化されている。

64 ビットコンパイル時、Winuser.h に次のインデックスを定義しません。

・GWL_WNDPROC
・GWL_HINSTANCE
・GWL_HWNDPARENT
・GWL_USERDATA

代わりに以下の新しいインデックスを定義します。

・GWLP_WNDPROC
・GWLP_HINSTANCE
・GWLP_HWNDPARENT
・GWLP_USERDATA
・GWLP_ID

実際の定義は以下のようになっている。

/*
 * Window field offsets for GetWindowLong()
 */
#define GWL_WNDPROC         (-4)
#define GWL_HINSTANCE       (-6)
#define GWL_HWNDPARENT      (-8)
#define GWL_STYLE           (-16)
#define GWL_EXSTYLE         (-20)
#define GWL_USERDATA        (-21)
#define GWL_ID              (-12)

#ifdef _WIN64

#undef GWL_WNDPROC
#undef GWL_HINSTANCE
#undef GWL_HWNDPARENT
#undef GWL_USERDATA

#endif /* _WIN64 */

#define GWLP_WNDPROC        (-4)
#define GWLP_HINSTANCE      (-6)
#define GWLP_HWNDPARENT     (-8)
#define GWLP_USERDATA       (-21)
#define GWLP_ID             (-12)

GetClassLong 用の GCL 系定数も同様の切り替えが行われる。

その他関数

一部の関数は引数の型だけ変更されていて、例えば SetTimer の UINT は UINT_PTR に、mciSendCommand の DWORD は DWORD_PTR に変更されている。

UINT_PTR SetTimer(
    HWND      hWnd,
    UINT_PTR  nIDEvent,
    UINT      uElapse,
    TIMERPROC lpTimerFunc
);
MCIERROR mciSendCommand(
    MCIDEVICEID IDDevice,
    UINT        uMsg,
    DWORD_PTR   fdwCommand,
    DWORD_PTR   dwParam
);

関数をラップするなどして引数のバケツリレーを行っている場合は、そちらも併せて修正しないと切り捨てが発生してしまう。以前は MSDN 上に変更された関数や構造体の一覧があったようだが、今は参照することができないので、インテリセンスなどに表示される引数の型を注意深く見るしかない。

メッセージパラメーター

ウィンドウプロシージャなどに登場する LPARAM と WPARAM も汎用的なデータ受け渡しに使われるので、こちらも同様にサイズが変更されている。

LPARAM、WPARAM、LRESULT はプラットフォームに応じてサイズが変更されます。

64 ビットコードをコンパイルする場合、これらの型はポインターや整数型を保持するため 64 ビットに拡張されます。DWORD、ULONG、UINT、INT、int、long と混在させないでください。

受け取ったらすぐに目的の型へキャストする、という使い方をしていたのでコードの修正は発生しなかった。

DirectX と 64 ビット

非推奨となった DirectX API の一部は 64 ビット版が提供されないため、別 API への置き換えが必要になる。DirectX SDK blog の「Whither DirectDraw?」という記事では以下のように書かれている。

While many legacy DirectX APIs were left behind in the 32-bit only world (DirectPlay, DirectMusic performance layer, etc.), DirectDraw (or at least DirectDraw7) is available to x64 native applications.

多くのレガシー DirectX API (DirectPlay、DirectMusic Performance レイヤーなど) が 32 ビットの世界に取り残されましたが、DirectDraw (少なくとも DirectDraw7) は、x64 ネイティブアプリケーションで使用できます。

C++ での 64 ビット対応

C++ の組み込み型も LLP64 に従いサイズが変更されている。

int と long は 64 ビット Windows オペレーティングシステム上で 32 ビット値です。ポインターは 64 ビットプラットフォームでは 64 ビットであり、ポインター値を 32 ビット変数に割り当てると切り捨てられます。size_t、time_t、ptrdiff_t は 64 ビット Windows オペレーティングシステム上で 64 ビット値です。

size_t

文字列の長さや STL コンテナの要素数といったサイズを表すものは全て size_t として定義されているので、深く考えずに int や unsigned int で受け取っている箇所は修正が必要になる。

64 ビットという広大な領域のサイズを表現するために size_t が使われるのは当然だが、領域へのアクセスに使うインデックスも size_t にする必要があり、オフセットも size_t を使った方が安全になる。つまり、配列のような連続した領域にアクセスする処理では、ほとんどの型が size_t になる。

改めてプログラムを見直してみると、あちこちで使われている int の大半は size_t として扱うのが適切で、int で受け渡しをしている enum 値を除外すると、純粋な int が必要になるケースがそう多くないことに気付かされた。

size_t と unsigned int

size_t から unsigned int への代入は、値の切り捨てを許容できるのであれば警告だけで済むが、テンプレート上で混在させて使っている場合は型が変わってしまうのでエラーになる。

unsigned int a = 1;
std::size_t b = 2; // 32 ビット環境のときは unsigned int 相当
auto c = std::min( a, b ); // コンパイルエラー (C2784) が発生
size_t と int

「基本的には正の値だが一時的に負の値をとる」という使い方をしている int は size_t への単純置き換えが難しい。例えば、以下のようなメニューカーソルをラップアラウンドさせる処理は一時的に負の値をとる。

// 左ボタンが押された
if ( IsLeftButtonPressed() )
{
    // メニューカーソルを左へ移動
    nMenuIndex--;

    // 左端を越えたか
    if ( nMenuIndex < 0 )
    {
        // 右端へラップアラウンドさせる
        nMenuIndex = nLastMenuIndex;
    }
}

これを size_t へ置き換えると、減算時に発生するオーバーフローによって境界判定が機能しなくなるので、減算前に判定を行う方式に作り替える必要がある。

// 左ボタンが押された
if ( IsLeftButtonPressed() )
{
    // 左端を超えない場合は左へ移動
    // 左端を越える場合は右端へラップアラウンドさせる
    uMenuIndex = (uMenuIndex > 0) ? uMenuIndex - 1 : uLastMenuIndex;
}

また、ダウンカウント方式のループも一時的に負の値をとる。

for ( int nIndex = nSize - 1; nIndex >= 0; nIndex-- )
{
    // 何らかの処理
}

これを size_t へ置き換えると、ループ終了直前にオーバーフローが発生して無限ループに突入してしまう。符号なし整数でダウンカウントを行う方法は存在するが、見た目が直感的ではない。

// 符号なし整数がオーバーフローしたら (-1 になったら) 停止
for ( std::size_t uIndex = uSize - 1; uIndex != -1; uIndex-- )
{
    // 何らかの処理
}

符号なし整数における -1 は最大値なので、インデックスとして使うことはできなくなるが、最大値が必要になるケースは稀なので実用上問題はない。

逆順の走査が頻繁に発生するのであれば予め要素を反転しておくのも一つの手だが、データ構造 (≒プログラムの仕様) が分かりづらくなる、という欠点がある。

size_t と ssize_t

以下のような検索関数を size_t に置き換える場合、

// haystack の中にある needle の位置を返す
int find( int haystack[], int needle, int size )
{
    for ( int index = 0; index < size; index++ )
    {
        if ( haystack[index] == needle )
        {
            // 見つかった
            return index;
        }
    }
    // 見つからなかった
    return -1;
}

インデックスと戻り値を size_t に変更することになるが、見つからなかったときは特別な値として -1 を返しているので、符号付きの size_t があると都合がよい。調べてみると ssize_t というものがあり、Linux の man page では以下のような説明になっていた。

Used for a count of bytes or an error indication.
It is a signed integer type capable of storing values at least in the range [-1, SSIZE_MAX].

バイト数またはエラー表示に使用されます。
これは、少なくとも [-1, SSIZE_MAX] の範囲の値を格納できる符号付き整数型です。

残念ながら Unix 処理系でのみ定義されている型で、Windows では使うことができない。ただ、同じ負の値を使って比較すれば判定することは可能なので、ssize_t を使うことにこだわる必要はない。

auto index = find( haystack, neelde, size );
if ( index == -1 )
{
    // 見つからなかった
}
size_t と printf

printf に size_t 型の値を渡すときは、長さ修飾子を付加する必要がある。

一部の型は 32 ビットと 64 ビットでサイズが異なります。例えば、size_t は x86 用にコンパイルされたコードでは 32 ビットですが、x64 用にコンパイルされたコードでは 64 ビットになります。可変幅の型に対応したプラットフォームに依存しない書式設定を作成するには長さ修飾子を使用します。Microsoft 固有の I (大文字の i) 長さ修飾子は可変幅の整数を処理しますが、移植性を考慮して型固有の j、t、z 修飾子の使用をお勧めします。

z が size_t に対応する修飾子となるが、Visual Studio Express 2013 では使うことができなかったので、Microsoft 固有の I を使って対処した。

time_t

time_t も 64 ビットに変更されるという話ではあったが、

time_t は、Visual Studio 2005 以前の 32 ビット Windows オペレーティングシステムでは 32 ビット値です。現在は、time_t は既定で 64 ビット整数です。

既に変更されていた。念のためプラットフォームを Win32 にして確認してみたが、説明通り sizeof で 8 が返ってくる。

time_t は乱数の初期化処理で unsigned int にキャストして使っていたが、そもそも srand の引数が unsigned int なので修正の余地はなかった。おそらく 2038 年以降は切り捨てが発生して 1970 年と同じ乱数シードが設定されることになるが、特に問題はないのでこのままにしておくことにした。これを修正するくらいなら乱数生成器自体を別のものに変えてしまった方が早い。