Win64 開発

はじめに

この記事では、 Windows プログラムを同じソースファイルから x86 (32 bit) 版および x64 (64 bit) 版の Windows 用にコンパイル (クロスコンパイル)できるようにするために、 私が学んだ事柄を紹介します。 なお、この記事では x86 版 Windows 用にコンパイルされたバイナリを「x86 バイナリ」、 x64 版 Windows 用にコンパイルされたバイナリを「x64 バイナリ」と呼ぶ事にします。

x64 版 Windows の上では、本来は x64 バイナリのプログラムしか動作しません。 しかしそれでは過去のアプリケーションが動作しなくなってしまい、 アプリケーションを x64 用に書き直すためにコストをかける必要が出てしまいます。 そこで x64 版 Windows は x86 アプリケーションを動作させるエミュレータ "WOW64" を搭載しました。 これによって過去のアプリケーションの多くは x64 版 Windows でそのまま動作する事となりました。 しかし WOW64 は良くできているとはいえエミュレータですから限界があり、 残念ながらすべてのアプリケーションが動作するとは限らないのが現状であり、 今後もそうでしょう。 以上のように、今後 Windows アプリケーションの開発においては実行環境をより意識する必要が出てきています。

Windows アプリケーション開発の方策

x64 版 Windows が現れてから少しは時間が経過しているものの、 まだ x64 版でのみ動作するアプリケーションを開発するのは現実的ではありません。 今後、当分の間は x86 版および x64 版 Windows の両方で動作するアプリケーションの開発が主流になるでしょう。 すると、考えられる開発モデルは次のうちどちらかになります。

  1. x86 バイナリと x64 バイナリの両方を作成し、 アプリケーションの x86 版と x64 版を提供する
  2. x86 バイナリのみを作成して x86 版アプリケーションを提供するが、 x64 版 Windows では WOW64 上での動作をサポートする

前者の開発モデルでは x86、x64 両環境でクロスコンパイルするための知識が必要です。 逆に後者の開発モデルでは WOW64 環境とネイティブな x86 環境との違いを理解する必要があります。 新規の開発であれば最初からクロスコンパイルできるようにするべきでしょう。 なぜなら今後は徐々に x64 環境へと移行していくからです。 逆に、クロスコンパイルをまったく考えていないような既存の x86 アプリケーションをメンテナンスするのであれば WOW64 上での動作を完全なものにするのが良い選択肢かもしれません。 いずれにしても開発するアプリケーションによって、 どちらの方策を採用するかは自然と決まってくるでしょう。

Win32 環境と Win64 環境の違い

クロスコンパイルを実現するには x86 環境と x64 環境の違いを理解する必要があります。 ここで、 便宜上 x86 環境用 API を Win32API、 x64 環境用 API を Win64API と仮に呼ぶ事とします。 Win64API は Win32API を置き換えるようなものではなく、 データモデルが変わっただけと捉えて問題ありません。 データモデルとは 「C 言語における、どの型がどれくらいのサイズか」 を決めるものと考えください。 Win32API のデータモデルは ILP32 と呼ばれ、 「int、long、pointer が 16 bit から新しく 32 bit になった」 モデルです。 Win64API のデータモデルは P64 (LLP64) と呼ばれ、 「pointer が 32 bit から新しく 64 bit になった」 モデルです。 なおポインタ以外にも size_t が拡張されています。 次の表に、各環境で採用されている型ごとのサイズを一覧します。


int long pointer long long size_t
Win32 (ILP32) 32 32 32 64 32
Win64 (P64) 32 32 64 64 64
Mac 64 (LP64) 32 64 64 64 64
Linux 64 (LP64) 32 64 64 64 64

クロスコンパイル可能なコードを書く

前述の通り Win32API と Win64API の間には API として極めて高い互換性があります。 そのためソースコードがクロスコンパイル可能かどうかは 「データモデルの変化が問題とならないよう記述してあるかどうか」 の一点でほぼ決まります。

データモデルの変化が問題とならないように記述するには、 要するに「型のサイズに依存しない」ように書けば良いだけです。 言語仕様上どの型もサイズが決められていない C/C++ の世界では 「型のサイズに依存しない」 という考えは原則に近い作法です (C99 で導入された固定サイズの型は例外)。 そのためこの作法を守って普段から開発していれば特に悩む事もありません。 しかし仮にコンパイラの警告レベルを最大にして型のサイズに関する警告が出ない状態になっても、 実は十分とは言い切れません。 Win64API ではサイズが異なるが Win32API 環境では「たまたま」同じサイズである型同士で変数の代入をしている場合、 これは危険であるにも関わらず警告が出ないのです。 この例の中でも特に重要なのは long 型の変数にポインタを代入するコードです。 この場合、 x86 環境ではポインタのサイズと long のサイズが 「たまたま同じ」 であるため x86 用にコンパイルする限りは「問題無い」と見なされて警告が出ません。 しかしこのようなコードを Win64API 環境でコンパイルして実行すれば、プログラムは確実にクラッシュします。

この問題を人間がコードを読みながらチェックするのは大変です。 そこで、こういったコードを見つけやすくするために Microsoft のコンパイラには x64 移植で問題が起こりそうな点を警告する オプション(Wp64)が付いています。 これを利用すればこのようなコードにも警告を出す事ができます。 トリッキーなコードには不十分かもしれませんが、 このオプションを利用するメリットは十分にあります。 まずはこのオプションを有効にしてみましょう。 これだけで解決しない問題があるならば、 さらに高度なチェックを行う Viva64 [1] などのツールを試してみると良いかもしれません (私は使った事がないので何とも言えませんが)。

GetWindowLongPtr / SetWindowLongPtr

Wp64 コンパイラオプションを有効にし、 警告レベルを最大にしても型に関する警告が出なくなれば、 そのソースコードはまずクロスコンパイルできるでしょう。 しかし、困った事に Win64API のヘッダファイルにはミスがあるため正しく作法を守って書いたコードでも警告が出る場合があります。 その場合とは、 GetWindowLongPtr と SetWindowLongPtr を呼び出す場合です。 なお、以後は話の都合上 SetWindowLongPtr だけを扱いますが GetWindowLongPtr についても話は同じです。

警告が出る原因は、 SetWindowLongPtr というマクロが呼び出すマクロの定義にあります(ややこしい)。 具体的な原因はさておき、この問題への対策として次のものが考えられます。

一つ目は、SetWindowLongPtr の宣言を勝手に修正する方法です。 これは、とてもおすすめできません。 システムのヘッダファイルに手を加えると、 原因不明のバグで悩んだ時に 「もしかしてヘッダの修正が影響しているのかも」 といった余計な心配事を招きかねないからです。 次は、SetWindowLongPtr の呼び出し部分の警告が出ないようにする方法です。 これは pragma を使ってその警告だけを呼び出しの直前で無効に、 直後で有効にする事で影響範囲を最小限に抑えます。 つまり SetWindowLongPtr の呼び出しを pragma で挟む形になります。 しかし pragma はコンパイラ依存の命令(?)ですので可能な限り使うべきではないでしょう。 最後は、ミスの無い SetWindowLongPtr を独自に作る方法です。 これは要するに SetWindowLongPtr と同じ機能をもつマクロや関数を自分で定義して、 そちらを使う方法です。 個人的には一番素直な発想だと思います。 Win32API 環境で long とポインタを相互に代入する場合は明示的にキャストすれば警告が出ませんので、 たとえば次のようなマクロをプロジェクト共通のヘッダに定義する方法があります。

// warningless SetWindowLongPtrW
#ifdef _WIN64
#   define MySetWindowLongPtrW( w, i, l ) \
                SetWindowLongPtrW( w, i, l )
#else
#   define MySetWindowLongPtrW( w, i, l ) \
                SetWindowLongPtrW( w, i, (LONG)(l) )
#endif

これは Unicode 版 SetWindowLongPtrW の代替品です。 当然ですが TCHAR による char / wchar_t の自動変換を利用しているプロジェクトでは 残り 4 つのマクロも定義しなくてはなりません。

WOW64

WOW64 とは "Windows-on-Windows 64-bit" の略で、x64 環境で動作する x86 環境のエミュレータです。 x64 版 Windows が標準で搭載しており、 x86 アプリケーションはすべて WOW64 上で動作します。 エミュレータとは言うものの WOW64 は良くできており、 特殊な事をしている x86 アプリケーションでなければ まず動作するはずです。

WOW64 はエミュレータですから、 あたかも x86 環境で動作しているかのように x86 アプリケーションを騙します。 とはいえ何もかも面倒を見てくれるほど WOW64 も万能ではないので、次のような注意点があります。

  1. x64 アプリケーションは x86 DLL をロードできない(逆も不可)
  2. 一部のディレクトリやレジストリへのアクセスが自動的に別の場所にリダイレクトされる

一つ目のポイントは、 異なるフォーマットのバイナリをロードできないという話ですから当然の制約だと思います。 というより、納得が行かないとしても仕方がないので当然と思いましょう(苦笑)。 二つ目のポイントについては、 同一アプリケーションの x86 版と x64 版をインストールした際に衝突が起こらないようにする仕組みです。 たとえばデータモデルを意識せずにデータをシリアライズすると、 型のサイズ違いから x86 版と x64版とで書き出したバイナリに互換性が無くなる可能性があります。 レジストリについても同様の可能性がありますから、 これらのリダイレクトが自動的に行われるのだと思います。 なお、このリダイレクトはその x86 アプリケーション単独で考える限り、 問題になる事はまずありません。 しかし、もし同じシステムで動作している x64 アプリケーションとプロセス間通信を行う場合には注意が必要です。 そのような場合については、この記事で扱いません。 MSDN の該当記事 [2] を参照してください。

システム DLL のファイル名

kernel32.dll をはじめとするシステム DLL の名前は、Win32 時代のそれと変化していません。 つまり x64 版の Windows には 64 bit バイナリの kernel32.dll が含まれています。 これに加え、x86 版の kernel32.dll が WOW64 環境用に含まれています。

Win16 から Win32 へ移行した時には、 両仕様のアプリを共存可能にするために Win16 用の kernel.dll を残した上で kernel32.dll が追加されました。 しかし Win64 へ移行するにあたって kernel64.dll というライブラリは追加されていません。 これは、2 点ほど当時と状況が違っているためだと思われます。 まず 1 点目は、Win64API が Win32API と非常に高い互換性を持っている事です。 2 点目は、 x86 環境のエミュレータを走らせても劇的なパフォーマンス低下が起こらない程に、 コンピュータの性能が高まっている事です。 もしシステム DLL の名前を変えたならば、 システム DLL に動的リンクするアプリケーションは x86 用にビルドする場合と x64 用にビルドする場合とでリンクするライブラリ名を切り替える必要が出てきます。 一方、もし同じファイル名であればそのような修正は不要です。 私見ではありますが、おそらくクロスコンパイルをより簡単にするためにシステム DLL の名前は変更されなかったのだと思います。

何にせよ、システム DLL は kernel64.dll といった名前になっておらず、 x86 版 Windows と同じ名前になっています。 システム DLL を動的ロードするアプリケーションでは システム DLL の名前が重要ですが、 「変わっていない」という事を覚えておけば良いでしょう。

自分が何 bit のプロセスか判定する

Windows 用のソースコードと一口に言っても、 今やそれは x86 アプリ、WOW64 上の x86 アプリ、 x64 アプリとして動作する可能性があります。 場合によっては、 どの環境で動作しているのか実行時に調べたい事もあるでしょう。 ここでは動作している環境を判定する方法をまとめます。

OS の判定

アプリケーションが動作している OS の種類を判定するには GetSystemInfo API を用いてきました。 しかし当然ながら x86 エミュレータである WOW64 上でこの API を呼ぶと x86 Windows だという答えが返ってきます。 そこで、アプリケーションの 実行環境の情報ではなく OS の情報を取得する新 API GetNativeSystemInfo が Windows XP 以降に導入されました。 この API を使えば WOW64 上でも OS の種類を取得できます。 次に簡単な例を示します。

SYSTEM_INFO sysInfo

// OS がどの CPU アーキテクチャ用なのか判定
GetNativeSystemInfo( &sysInfo );
switch( sysInfo.wProcessorArchitecture )
{
    case PROCESSOR_ARCHITECTURE_AMD64:
        printf( "x64\n" );
        break;

    case PROCESSOR_ARCHITECTURE_IA64:
        printf( "IA64\n" );
        break;

    case PROCESSOR_ARCHITECTURE_INTEL:
        printf( "x86\n" );
        break;

    case PROCESSOR_ARCHITECTURE_UNKNOWN:
    default:
        printf( "???\n" );
        break;
}

ただ、GetNativeSystemInfo は新しい API であるため、 利用できる事を前提とした実装をしないよう推奨されています。 具体的には、実行時に kernel32.dll を LoadLibrary した上で GetProcAddress で同 API を取得、 その関数ポインタを通して使うよう推奨されています。 もし GetProcAddress に失敗した場合は少なくとも WOW64 が導入された世代の OS ではありません。 その場合は従来どおり GetSystemInfo で判定しましょう。

WOW64 かどうかの判定

WOW64 も完璧ではないので、 アプリケーションによってはネイティブの x86 環境と WOW64 環境で動作を切り替える必要があるかもしれません。 そのためには WOW64 上で動いているのかどうかを判定する必要があります。

WOW64 の導入とともに、 次に挙げる新しく二つの API が導入されました。

IsWow64Message
呼び出したスレッドのメッセージキューから最後に読み出したウィンドウメッセージが WOW64 プロセスから送られたものか判定
IsWow64Process
ハンドルで指定したプロセスが WOW64 上で動作しているか判定

ところで、ウィンドウメッセージは簡単なプロセス間通信に利用できます。 WPARAM と LPARAM の 2 パラメータはポインタ型ではありますが、 数値としても扱えるため別プロセスのウィンドウにパラメータをつけてメッセージ送信すれば小さなデータを送る事ができます。 逆にメッセージを受け取った別プロセスのウィンドウプロシージャで、 やはり返値としてポインタではなく数値を返せば送信者側にその値が届きます。 こうした簡単なプロセス間通信を WOW64 アプリと x64 ネイティブアプリとで安全に行うために、 これらの API は利用できます。

IsWow64Message は呼び出したスレッドのメッセージキューから最後に読み出したウィンドウメッセージが WOW64 プロセスから送られたものかどうかを判定します。 たとえば、WOW64 で動作する x86 アプリから何かの数値を要求するようなメッセージが送られてきたとします。 そこで x64 アプリが 32 bit で表せない大きな数値を返してしまうと、 x86 アプリが受け取る頃には WOW64 によって データが 32 bit に削られるため壊れています。 このような場合、メッセージを解釈して値を返す前に IsWow64Message API を呼んで送信元が WOW64 上のアプリかどうか確認すれば適切に処理可能になります。

IsWow64Process は指定したプロセスが WOW64 上で動作しているかどうか判定します。 ここで IsWow64Message の場合と同じ例を考えますが、 今度は送信側が x64 アプリで受信側が WOW64 上 x86 アプリとします。 出てくる問題は IsWow64Message の例と同じで、 大きすぎる数値を WPARAM あるいは LPARAM として送信すると WOW64 上の x86 アプリは壊れた値を受け取ってしまいます。 このような場合、メッセージを送信するウィンドウを生成したプロセスのハンドルを IsWow64Process API で判定すれば大きい数値を送れない事が分かり、 適切に処理できます (なおウィンドウハンドルからその所有プロセスのハンドルは GetWindowThreadProcessId、OpenProcess で取得可能か?未確認)。

.NET アプリケーションでの注意点

純粋な .NET アプリケーションを開発する場合、 x86 と x64 の違いを意識する必要はありません。 なぜなら、それらは対象プラットフォームに AnyCPU を指定してビルドすると x86 OS では x86 アプリケーションとして、 x64 OS では x64 アプリケーションとして動作するからです。 しかし、ネイティブコードのライブラリを P/Invoke するアプリケーションでは話が違ってきます。 .NET アプリケーションが環境に応じて x86 バイナリと x64 バイナリのどちらに JIT コンパイルするかを自動で選択する一方、 呼び出されるネイティブコードのライブラリはすでに x86 バイナリか x64 バイナリか決まっているからです。 つまり、 環境によってバイナリの互換性が崩れてしまう問題があるのです。

この問題を解決する方法は二通り考えられます。 一つ目は、.NET アプリケーションの対象プラットフォームを ネイティブコードのライブラリに合わせる方法です。 普通は x86 環境に統一する事になるでしょう。 二つ目は、次のような方法です。

この方法では、x86 環境でも x64 環境でもアプリケーション全体がネイティブに動作するため、 もっとも望ましい形だと言えます。 .NET アプリケーションが実行時に動作中のプラットフォームを判定する方法については、 手抜きですがポインタのサイズを調べる方法があります。 たとえば C# では次のようになります。

// ポインタのサイズを実行時に判定
int ptrSize = Marshal.SizeOf( IntPtr.Zero );
if( ptrSize == 8 )
{
    Console.WriteLine( "64 bit environment." );
}
else
{
    Console.WriteLine( "32 bit environment." );
}

手抜きというのは、 ポインタサイズから判定できるのはプラットフォームではなくデータモデルだからです。 たとえば P64 データモデルだと判定できても、 それが x64 なのか IA64 なのかは判定できません。 「子供だまし」なテクニックですが手軽なので、 x86 と x64 だけを対象とした趣味レベルの開発では使えるかもしれません。 ちゃんとしたものを開発する必要があるならば GetNativeSystemInfo を P/Invoke して確認するべきでしょう (動作は未確認)。

最後に

元々この記事は自作アプリケーションを Windows Vista x64 Edition で動作させるのに調べた内容をまとめるために書き始めたものでした。 そのため書いた内容の一部が商用品質に耐えないレベルになっています。 なお .NET アプリケーションでの注意点について扱っている記事はあまり無いと思います。 そんな中、何故この記事でそんなネタを扱ったか、という話のタネ明かしは、 自作アプリケーションが .NET + ネイティブ DLL なアプリだったからです。 むしろメインの話だったわけです。 単純明快。

参照

  1. Andrey Karpov: 64-bits for C++ Developers: from /Wp64 to Viva64; Viva64.com, 2007
  2. MSDN: Running 32-bit Applications; Platform SDK: 64-bit Windows Programming