Win16 から Win32 への移植

この文書では Win16 から Win32 へアプリケーションプログラムを移植する際の注意事項をまとめています。

データ型の違い

基本的には、ソースの修正は必要ない。

データ型Win16Win32
HANDLE16bit32bit
UINT16bit32bit
BOOL16bit32bit
WORD16bit16bit
DWORD32bit32bit

near アドレスと far アドレスについて

Win32 は、near アドレスと far アドレスとを区別しない。 大文字の NEAR 型と FAR 型は、WINDEF.H で定義されているために、インクルード ファイルによって自動的に処理され、Win32 では空文字列として再定義される。このような処理が行われるため、結果的に NEAR と FAR は無視されることになる。 小文字のnearとfarを用いている場合は、マクロ定義のFARとNEARに変更する。

アドレス修飾子による違い
修飾子Win16Win32
near 16ビットアドレス(セグメントアドレッシング) ×
far 32ビットアドレス(セグメントアドレッシング)
NEAR 16ビットアドレス(セグメントアドレッシング) 32ビットアドレス
(リニアアドレッシング)
FAR 32ビットアドレス(セグメントアドレッシング)
指定無し 16ビットアドレス(セグメントアドレッシング)

far ポインタ関数について

Windows 3.x には、far ポインタを使ってメモリやファイルを操作する _fxxxx 形式の名前を持つ関数がある。 Win32 では far ポインタがないため、Win32 では、これらの関数名から _f のプリフィックスを取り除いた xxxx の部分と同じ名前の関数に置き換えられている。 ただし、WINDOWSX.H ファイルには _fxxxx の関数名も定義されているため、Win32 でも _fxxxx という関数名は、サポートされている _f プリフィックスのない関数と同じ意味になる。つまり、WINDOWSX.H をインクルードすれば、これらの関数については書き直す必要がない。一部の定義を以下に示す。

#define _fmemcpy       memcpy
#define _fstrcpy       strcpy
#define _fstrcmp       strcmp
#define _fstrcat       strcat

Win16 APIでは、メモリ操作関数がnearポインタ用とfarポインタ用の2種類が用意されている。

Win32 Win16
nearポインタ farポインタ
memcpy memcpy _fmemcpy
strcpy strcpy _fstrcpy
strcmp strcmp _fstrcmp
strcat strcat _fstrcat

WinMain関数の引数の違いについて

Win16とWin32では、WinMain関数の引数に型に違いがある。これにともない、メッセージのハンドリングにも変更が必要である。

引数Win16Win32
第1引数HWND型(16ビット)HWND型(32ビット)
第2引数UINT型(16ビット)UINT型(32ビット)
第3引数WORD型(16ビット)UINT型(32ビット)
第4引数LONG型(32ビット)LONG型(32ビット)

ウィンドウクラスの登録について

Win32ではWinMain関数の第2引数は常に.0なので、すでに存在するインスタンスのハンドルが0のときにウィンドウクラスを登録するという手順になっているWin16のソースコードに手を加えなくてもよい。

Win16

WinMain関数の第2引数には、すでに存在するインスタンスのハンドルが入っている。すでに存在するインスタンスがあれば、RegisterClass関数を呼ばずにインスタンスを共有するようになっている。 RegisterClass()の返り値は、関数の実行が成功(True)したか失敗(False)したかを返す。

Win32

必ずRegisterClass()を呼ばなければならない。また、WinMain関数の第2引数は常に0である。RegisterClass()の返り値は、クラスの識別子を返し、失敗した場合は0を返す。

メッセージの違いについて

Win16 と Win32 では、WinMain 関数の引数に型に違いがあります。これにともない、メッセージのハンドリングにも変更が必要です。

Win16 WM_COMMANDの場合
引数意味
WPARAMウィンドウのID(16ビット)
LPARAM(HIGH)ウィンドウハンドル(16ビット)
LPARAM(LOW)通知コード(16ビット)
Win32 WM_COMMANDの場合
引数意味
WPARAM(HIGH) ウィンドウのID(16ビット)
WPARAM(LOW) 通知コード(16ビット)
LPARAM ウィンドウハンドル(32ビット)

メッセージのハンドリングについて

wParam/lParamのパッキングが変わったため、メッセージのハンドリングに注意する必要がある。 メッセージパッキングの問題を局所的に抑えるには、以下のようにローカル変数に取り出すか、関数化すればよい。

switch (msg) {
    case WM_COMMAND:
        id = LOWORD(wparam);
        hWnd = (HWND)(UINT)lParam;
        cmd = HIWORD(wParam)
}

また、Win16とWin32でコードを共有するには、以下の方法がある。

(1) メッセージクラッカを用いる。

switch (msg) {
    case WM_PAINT:
        return HANDLE_WM_PAINT(hwnd, wParam, lParam, MyWnd_OnPaint);
}

void MyWnd_OnPaint(HWND hwnd) {
    // WM_PAINTのハンドリング
}

(2) WINDOWSX.Hで定義されているマクロを使用する。

マクロWin16Win32
GET_WM_COMMAND_ID(wp, lp) (wp) LOWORD(wp)
GET_WM_COMMAND_HWND(wp, lp) (HWND)LOWORD(lp) (HWND)(lp)
GET_WM_COMMAND_CMD(wp, lp) HIWORD(lp) HIWORD(wp)

DilogBox関数について

Win16では第4引数を指定するにはMakeProcInstanceを用いなければならないが、Win32では直接ダイアログプロシージャ自身のポインタを指定することも可能である。

Win16

int DialogBox(HANDLE, LPCSTR, HWND, FARPROC);

Win32

int DialogBox(HANDLE, LPCSTR, HWND, DLGPROC);

メモリ環境の違いについて

Win32環境下では、GlobalAllocとLocalAllocは共にmallocと同じ動作をするようになった。また、ロック/アンロックの操作も必要なくなった。

メモリの確保と解放
目的Win16Win32
メモリの確保 GlobalAlloc() または LocalAlloc() malloc()
メモリの解放 GlobalFree() または LocalFree() free()

関数の型について

WinMain関数の型をWINAPI型に変更する。 ウィンドウプロシージャ関数をLRESULT CALLBACK型に変更する。また、_exportは省略できる。 Win32ではAPI関数の型はWINAPI型に統一された。

関数Win16Win32
WinMain FAR PASCAL型 WINAPI型 または APIENTRY型
ウィンドウプロシージャ FAR PASCAL型 LRESULT CALLBACK型

WindowsNTとWindows95との違いについて

上記のようにWindowsNTやWindows95には、それぞれ固有のAPIがあるが、WindowsNTまたはWindows95に依存したAPIは使用しない。

Win32APIの機能Windows95WindowsNT
32ビット拡張されたWin16相当のAPI
32ビットメモリ管理
ファイルマッピング
ネットワーキング(NetBios,Windows Sockets)
OLE2.0
メールスロットと名前付きパイプ
スレッド
拡張GDI(ベジェパス)
リモートプロセスコール
GDIトランスフォーム
イベントログ
セキュリティ
Unicode
プラグアンドプレイ

DLLの違いについて

Win16 では DLL の初期化時に呼ばれる関数が LibMain で、DLL の終了時に呼ばれる関数が WEP でした。

Win32 では DLL の初期化時と終了時に呼ばれる関数が DllMain という関数に集約されました。

DllMain(HANDLE hInst, ULONG ulReason, LPVOID lpvReserved)
hInst
モジュールハンドル(インスタンスハンドル)
ulReason
DllMain が呼び出された理由。
DllMain 関数の ulReason 引数
意味
DLL_PROCESS_ATTACH新しいプロセスが DLL へアクセスしようとしている。単一スレッドとみなす。
DLL_THREAD_ATTACH既存のプロセスの新しいスレッドが DLL にアクセスしようとしている。この呼び出しは、DLL にアタッチするプロセスの 2 番目のスレッドから行われる。
DLL_PROCESS_DETACH プロセスがDLL からデタッチする。
DLL_THREAD_DETACH プロセスの追加スレッドの 1 つ (最初のスレッド以外) がDLL からデタッチする。
lpvReserved
システム予約

DllMain関数には、以下のようなWin16のLibMain関数のような初期化パラメータはない。

DLLのデータセグメント Win32では必要ない。メモリ空間がフラットで、セグメント化されていないため。
DLLのローカルヒープのサイズ ローカルメモリ管理関数の呼び出しはすべてデフォルトのヒープで行われる。
コマンドラインへのポインタ コマンドラインは、API関数GetCommandLineを呼び出すことにより取得する。

必要であれば、DllMain関数を新たに追加する。簡単な移植方法を以下に示す。

#ifdef WIN32
BOOL WINAPI DllMain(HANDLE hInst, ULONG ulReason, LPVOID lpvReserved) {
    switch(ulReason) {
        case DLL_PROCESS_ATTACH:
            return LibMain(hInst, 0, 0, NULL);
        case DLL_PROCESS_DETACH:
            WEP(0);
            break;
    }
    return TRUE;
}
#endif

関数呼び出し規約のキーワードについて

_pascal 型、__pascal 型を用いているものは、WINAPI 型に修正する必要がある。 PASCAL 型、pascal 型を用いているものは、修正を施さなくてもコンパイル可能だが、WINAPI 型に修正することが望ましい。

Win16

__pascal
_pascal(旧式)
pascal(旧式)
PASCAL(マクロ)

Win32

pascal(マクロ)
PASCAL(マクロ)

レジストリの違いについて

WindowsNTとWindows95ではレジストリのキー項目に違いがある。 Windows95レジストリとWindowsNTレジストリでは、キーに関連するクラス情報の処理のしかたが異なる。 Windows95レジストリはすべてのクラスを同じように処理する。 RegQueryInfoKey に有効な lpszClass バッファポインタは渡すが、有効なlpcchClassポインタは渡さない場合(NULLを渡す場合)には、Windows95は関数呼び出しを続ける。 これに対して、WindowsNTレジストリはクラスを区別する。WidnowsNTでは、RegQueryInfoKeyに有効なlpszClassバッファポインタは渡すが、有効なlpcchClassポインタは渡さない場合(NULLを渡す場合)には、システムはERROR_INVALID_PARAMETERエラー値を返す。

Windows NT

HKEY_CURRENT_USER のサブキー

Windows 95

HKEY_CURRENT_USERのサブキー

アプリケーションの状態の記録について

Win16 では、アプリケーションの設定状態などは INI ファイルに記録していました。

Win32 では、アプリケーションの設定状態などはレジストリ(登録情報データベース)に記録するようにします。 アプリケーションの状態は、\HKEY_CURRENT_USER\Software\メーカ名\アプリケーション名\ の下に記録するのが慣例です。

Unicodeについて

文字列パラメータを引数とするWin32関数はUnicode文字列(ワイドキャラクタ)またはANSI文字列のいずれかを処理できるように設計されている。ただし、Windows95はほとんどのWin32関数のUnicodeバージョンを実装していない。若干の例外はあるもののこれらの関数はエラー値を返すスタブとして実装されている。Windows95では以下の関数のUnicodeを実装している。

さらに、Windows 95 は、Unicode との間で文字列を変換するための MultiByteToWideChar および WideCharToMultibyte 関数も実装している。

WINAPI (__stdcall) 型に関する注意

__stdcall キーワードを使うと、指定した関数の引数が右から左にプッシュされ、名前の前にアンダースコアが付き、後ろに @### が付加されます。 この ### は関数の引数のサイズのバイトサイズです。 前のバージョンとの互換性を保つために、 __stdcall の同義語 _stdcall も用意されています。 関数のプロトタイプがあり、引数の数が一定の場合は、呼び出された関数がスタックをポップします。 可変の場合は、呼び出し側でスタックをポップします。 __stdcall としてマークされ、可変個の引数を取る関数は、__cdecl としてインプリメントされます。 呼び出し規約をプロトタイプまたは宣言で指定すると、コマンド ライン オプションで指定したどの規約より優先されます。

__stdcall 呼び出し規約は、Win32 API 関数の呼び出しに使用します。 スタックは呼び出された側がクリアするので、vararg 関数は __cdecl になります。 この呼び出し規約を使う関数には、関数プロトタイプが必要になります。 次に、この呼び出し規約のインプリメントを示します。

引数は右から左の順番で渡されます。 また、引数は値で渡されます(ポインタ型と参照型を除く)。

呼び出された側の関数が引数をスタックからポップすることで、スタックのクリアを行います。

名前の修飾として、アンダースコア (_) が名前の頭に付けられます。 また、名前の後にはアット マーク (@) と引数リストのバイト数 (10 進) が続きます。 したがって、int func(int a, double b) と宣言された関数は修飾の結果 _func@12 となります。

大文字小文字の変換はありません。

/Gz コンパイラ オプションを指定すると、他の呼び出し規約で明示的に宣言されていないすべての関数が __stdcall となる。 __stdcall 修飾子とともに宣言された関数は、__cdecl で宣言された関数と同じ方法で値を返す。

DLLのメモリについて

Win16

DLLは自分のデータセグメントを持っている。このデータセグメントは、DLLが必要とするすべての静的変数、グローバル変数とDLL自身のプライベートローカルヒープを収めている。DLL関数/ファンクションがLocalAllocを呼び出すと、メモリはこのDLLデータセグメントから確保される。このセグメントのサイズは、他のセグメントと同様に64Kバイトに制限されている。

Win32

Win32ではヒープには1つのタイプしかない。1種類しかないので、「ローカル」とか「グローバル」といった特別な名前はない。 ヒープは常にプロセスにとってはローカルな存在である。プロセスのヒープの内容は他のプロセスのスレッドからはアクセスできない。 DLLはヒープを持たなくなった。DLLはプロセスのアドレス空間に含まれるヒープを利用する。 アプリケーションはプロセスのアドレス空間の中にデフォルトヒープ以外のヒープを作成できる。 DLLのファイルイメージが呼び出し側プロセスのアドレス空間にマッピングされる。DLLファイルイメージの複数マッピングのために静的変数、グローバル変数のインスタンスが別々に作成される。

複数起動プロセスのチェックについて

Win16

指定されたモジュールの参照カウンタをチェックすることにより可能

/* モジュール名よりモジュールハンドルを取得 */
MHndl = GetModuleHandle("OPE.EXE");

/* モジュールハンドルより参照カウンタを取得し
 * 2個以上より参照されている時はエラー
 */
if (GetModuleUsage(MHndl) > 1) {
    errproc("このアプリケーションは,一度に複数起動できません。" );
    exit(1);
}

Win32

Win32ではユーザがアプリケーションの複数のインスタンスを起動しているか否かを判定する為の簡単な方法は提供していないが、すべてのインスタンスに単一のグローバル変数を共有させることができれば、実行中のインスタンス数をグローバル変数を使用してアプリケーションが管理し、複数起動チェックを行うことが可能となる。共有化の方法としては以下がある

  1. DLL や EXE に Shared 属性のセクションを設けてデータを共有する
  2. メモリマップトファイルを使用して複数プロセス間でメモリ内のデータブロックを共有する

DLL や EXE に Shared 属性のセクションを設けてデータを共有する

Sharedという新セクションを追加し、変数を宣言する

#pragma data_seg("Shared")
DWORD abc = 0;
HWND  HwndA = NULL;
#pragma data_seg();

#pragma comment(lib,"msvcrt " "-section:Shared,rws)

リンカディレクティブを OBJ ファイルに埋め込む(プロジェクトのリンカスイッチでも指定可能)

メモリマップトファイルを使用して複数プロセス間でメモリ内のデータブロックを共有する

HANDLE hMap = CreateFileMapping((HANDLE) 0xFFFFFFFF,....,1024,"SharedData");
あるいは
HANDLE hMap = OpenFileMapping(...."SharedData");
if (hMap == NULL)
 return(GetLastError());
         .
         .
 LPVOID lpView = MapViewOfFile(hMap,.....);
         .
         .
         .
 UnmapViewOfFile((LPVOID) lpView);
         .
         .
 CloseHandle(hMap);

プロセスの作成について

プロセスは、アプリケーションがCreateProcessを呼び出したときに作成される。 アプリケーション内のスレッドがCreateProcessを呼び出すと、オペレーティングシステムは参照カウンタの初期値として1がセットされたプロセスカーネルオブジェクトを作成する。このプロセスカーネルオブジェクトは、プロセス自体ではなく、オペレーティングシステムがプロセスの管理のために使う小さなデータ構造である。プロセスカーネルオブジェクトは、プロセスについての統計情報から構成される小さなデータ構造と考えることができる。システムは、次に、新プロセスのために4Gバイトの仮想アドレス空間を作成し、そこに実行可能ファイルのコード、データと必要なDLLをロードする。 次に、システムは、新プロセスの新スレッドのためにスレッドカーネルオブジェクトを作成する。スレッドカーネルオブジェクトは、プロセスカーネルオブジェクトと同様に、オペレーティングシステムがスレッド管理のために使う小さなデータ構造である。主スレッドは、Cランタイムのスタートアップルーチンから実行を開始し、このスタートアップルーチンがWinMain関数を呼び出す。システムが新プロセスと主スレッドの作成に成功すると、CreateProcessは、TRUEを返す。 CreateProcessのインタフェースを以下に示す。

BOOL CreateProcess(
LPCSTR lpszImageName,
LPCSTR lpszCommandLine,
LPSECURITY_ATTRIBUTES lpsaProcess,
LPSECURITY_ATTRIBUTES lpsaThread,
BOOL fInheritHandles,
DWORD fdwCreate,
LPVOID lpvEnvironment,
LPTSTR lpszCurdir,
LPSTARTUPINFO lpsiStartInfo,
LPPROCESS_INFORMATION lppiProcInfo);

CreateProcess の lppiProcInfo 引数は、PROCESS_INFORMATION 構造体へのポインタである。 この構造体は、呼び出し側アプリケーションで確保しておかなければならない。 CreateProcess は、制御を返す前にこの構造体を初期化する。構造体の定義は次の通りである。

typedef struct _PROCESS_INFORMATION {
  HANDLE hProcess;
  HANDLE hThread;
  DWORD  dwProcessId;
  DWORD  dwThreadId;
} PROCESS_INFORMATION;

プロセスを作成するとプロセスカーネルオブジェクトとスレッドカーネルオブジェクトが作成される。オブジェクトの作成時に、システムはその参照カウンタに1をセットする。そしてCreateProcessは、処理が終わる直前にこれらのオブジェクトをオープンし、PRCESS_INFORMATION構造体の hProcess、hThreadメンバにそのプロセスのみで有効なオブジェクトハンドルをセットする。この処理によって、2つのオブジェクトの参照カウンタはそれぞれインクリメントされて2になる。

このような設計になっているため、Win32システムでプロセスオブジェクトを解放するには、プロセスを終了させる(参照カウンタが1減る)だけでなく、親プロセスでCloseHandle を呼び出す(参照カウンタが1減る)必要がある。スレッドオブジェクトの場合も、親プロセスがハンドルをクローズしないと解放できない。

また、GetExitCodeProcessを使えば、プロセスハンドル(hProcess)に対応するプロセスが終了しているかどうか調べ、終了している場合にはその終了コードを取得することができる。

BOOL GetExitCodeProcess(HANDLE hProcess, LPDWORD lpdwExitCode);

終了コードは、lpdwExitCode 引数が指すDWORD 変数に格納される。プロセスが終了していない場合、このDWORD変数にはSTILL_ACTIVEが格納される。処理に成功すると、 GetExitCodeProcessは、TRUEを返す。

共有セクションについて

DLLの複数のマッピングの間で変数を共有したい場合は、変数を独自セクションに配置する。変数を独自セクションに配置するには、以下のように記述する。

#pragma data_seg("Shared")
int iShare;
#pragma data_seg()

そして、リンカに対して変数を共有するセクション名を指定する。これは、リンカで次のスイッチを指定すればよい。(Visual C Ver.4.0の場合、プロジェクト設定のC/C++カテゴリのプロジェクトオプションで指定する。)

-SECTION:<名前>,<属性>

指定できる属性にはR(READ),W(WRITE),S(SHARED),E(EXECUTE)がある。 たとえば、Sharedセクションを読み書き可能の共有セクションにしたければ、次のようにスイッチを指定すればよい。

-SECTION:Shared,RWS

また、複数のセクションの属性を変更したい場合には、-SECTIONスイッチを複数回指定する。