[Win32 API] ウィンドウハンドルから実行ファイル名取得と、その注意点

WEB 上で調べると `EnumProcessModules()` 関数を使用した方法がよく出てきますが、実行ファイル名を取得するだけなら不要です。

他にも注意点が複数あるので、まとめました。

# 0. 要点

1. モジュールハンドル `hModule` は**不要**
2. ファイル名の長さは `MAX_PATH` や `_MAX_PATH` では**不十分**
3. 基本的には、タスクマネージャ等のシステムプロセスを扱えない
4. 自プロセスしか扱えないファイル名取得関数に注意

# 1. 全体の流れ

1. ウィンドウハンドルからプロセス ID を取得し、プロセスをオープン
2. プロセスから実行ファイル名取得
3. ファイル名を取得する際、バッファサイズが足りなかった可能性がある場合は、**バッファサイズを増やして再取得する** (`MAX_PATH` や `_MAX_PATH` を過信しない)
4. ファイル名の取得失敗にかかわらず、プロセスクローズ

# 2. コード (テスト用)

※ここでは文字列をすべて Unicode で扱うことにします。
※ここでは WSL 上の MinGW-w64 でコンパイルすることを想定しています。
※以下のコードでは、タスクマネージャ等のシステムプロセスは扱えません。

```C++:main.cpp
#include <windows.h>
#include <iostream>
#include <fcntl.h>

#if ( _WIN32_WINNT < 0x0600 )

// メモ: ここでは、WINVER と _WIN32_WINNT が同じ値に設定されている場合、
//       常に PSAPI_VERSION == 1 が選択されるが、ソースコードを変更した場合にバグが発生しないように記述。
#ifndef PSAPI_VERSION
#if ( WINVER < 0x0601 ) // メモ: 0x0600 は PSAPI_VERSION == 2 対応・非対応が混在している。
#define PSAPI_VERSION 1
#elif
#define PSAPI_VERSION 2
#endif
#endif

#include <psapi.h>

#endif

// 
bool printFileName(HWND hWnd);
bool printFileName(HANDLE hProcess);
bool printFileName(HANDLE hProcess, DWORD nSize);

// 
int wmain() {

	// 出力の文字コード指定
	_setmode(fileno(stdout), _O_U8TEXT);
	_setmode(fileno(stderr), _O_U8TEXT);

	// 
	const HWND hWnd = (HWND) /* 0x00000 */; // TODO: テスト用。手動入力

	if ( ! printFileName(hWnd) ) return -1;

	return 0;

}

bool printFileName(HWND hWnd) {

	// プロセス ID
	DWORD dwProcessId;
	GetWindowThreadProcessId(hWnd, &dwProcessId);

	// プロセスを開く
	// 
	// メモ: これだけでは、タスクマネージャ等のシステムプロセスは扱えない。
#if ( _WIN32_WINNT < 0x0600 )
	const HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, dwProcessId);
#else
	const HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, dwProcessId);
#endif

	if ( ! hProcess ) {
		std::wcerr << L"OpenProcess Error: " << GetLastError() << std::endl;
		return false;
	}

	// 
	printFileName(hProcess); // ここではエラーにかからわずスルーする (プロセスクローズに注意)

	// プロセスを閉じる
	CloseHandle(hProcess);

	return true;

}

bool printFileName(HANDLE hProcess) {

	// ファイルパス
	// 
	// メモ: 新しい Windows ではファイルパスの長さが _MAX_PATH を超える可能性があるため、
	//       バッファサイズを増やしながら再取得する。
	for (DWORD nSize = 256; nSize <= 32768; nSize <<= 1) {
		if ( printFileName(hProcess, nSize) ) return true;
	}

	std::wcerr << L"Error: printFileNameFrom()" << std::endl;

	return false;

}

bool printFileName(HANDLE hProcess, DWORD nSize) {

	// ファイルパス
	WCHAR lpszFileName[nSize];

#if ( _WIN32_WINNT < 0x0600 )

	const DWORD length = GetModuleFileNameExW(hProcess, NULL, lpszFileName, nSize); // hModule == NULL

    if ( 0 == length ) {
        std::wcerr << L"GetModuleFileNameExW Error: " << GetLastError() << std::endl;
        return true; // ループ終了
    } else if ( nSize - 1 <= length ) {
        return false; // ループ継続
    }

#else

	DWORD length = nSize;

	if ( ! QueryFullProcessImageNameW(hProcess, 0, lpszFileName, &length) ) {
		const DWORD lastError = GetLastError();
		if ( ERROR_INSUFFICIENT_BUFFER == lastError ) {
			return false; // ループ継続
		} else {
			std::wcerr << L"QueryFullProcessImageNameW Error: " << lastError << std::endl;
			return true; // ループ終了
		}
	}

#endif

	// 
	std::wcout << L"File Name: " << lpszFileName << std::endl;

	return true; // ループ終了

}
```

```bash:build.sh
#!/bin/bash

# g++ ver.5.1 以降を想定
x86_64-w64-mingw32-g++ -Wall -std=c++14 \
	-finput-charset=UTF-8 -fexec-charset=CP932 \
	-municode \
	-static-libgcc -static-libstdc++ \
	-DWINVER=0x602 \
	-D_WIN32_WINNT=0x602 \
	-o main.exe \
	main.cpp
```

`./build.sh` でビルドできます。

※ **PSAPI のバージョンおよびライブラリのリンクに関して後述**。
※コンパイルオプションは一例です。
※ここでは簡単のため、シェルスクリプトでビルドすることにします。
※規模が大きいプロジェクトの場合は、何らかのビルドツールを利用した方が良いです。

# 3. 注意点

## 3.1. プロセスから実行ファイル名を取得する関数

`GetModuleFileNameExW()` 関数は Windows XP から利用できますが、Windows Vista から `QueryFullProcessImageNameW()` 関数でも取得できるようになりました。
Microsoft Docs によると `QueryFullProcessImageNameW()` の方が「効率的で信頼性がある」と書かれていますが、具体的にどう違うのかは分かりませんでした…。
ですが、新しい Windows で動作させるなら `QueryFullProcessImageNameW()` の方が良いと思います。

参考「[GetModuleFileNameExW function (psapi.h) - Win32 apps | Microsoft Docs](https://docs.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-getmodulefilenameexw)」
参考「[QueryFullProcessImageNameW function (winbase.h) - Win32 apps | Microsoft Docs](https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-queryfullprocessimagenamew)」

## 3.2．モジュールハンドル `hModule` は不要

`GetModuleFileNameExW()` 関数に関して、Microsoft Docs に「モジュールハンドルが `NULL` の場合は `hProcess` の実行ファイルのパスを返す」とあるので、別途モジュールハンドルの取得は不要です。

参考「[GetModuleFileNameExW function (psapi.h) - Win32 apps | Microsoft Docs](https://docs.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-getmodulefilenameexw)」

`QueryFullProcessImageNameW()` には元々モジュールハンドルの引数はありません。

## 3.3. ファイル名の長さについて

最近の Windows ではファイル名の長さが `MAX_PATH` や `_MAX_PATH` の値を超えることができるため、バッファサイズを固定するのは危ないです。
また、ファイル名の長さを 1 回で確実に取得する方法はありません (バッファのポインタを `NULL` にしても不可) 。

`GetModuleFileNameExW()` 関数の場合は、バッファサイズが足りないだけではは**エラーを返さない**ので、文字数が「バッファサイズ - 1」より大きいかどうかを確認します。
その場合には、バッファサイズが足りていない可能性が考えられますので、**バッファサイズを増やして再取得**します。

`QueryFullProcessImageNameW()` 関数の場合は、バッファサイズが足りないときはエラーが発生し、`ERROR_INSUFFICIENT_BUFFER == GetLastError()` になります。そのときも同様に**バッファサイズを増やして再取得**します。

参考「[windows - How can I calculate the complete buffer size for GetModuleFileName? - Stack Overflow](https://stackoverflow.com/questions/805814/how-can-i-calculate-the-complete-buffer-size-for-getmodulefilename)」
参考「[System Error Codes - Win32 apps | Microsoft Docs](https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes)」

## 3.4. タスクマネージャ等のシステムプロセスについて

上記のコードのままでは、タスクマネージャ等のシステムプロセスに関してオープン時にエラーが発生し、扱うことができません。

`SeDebugPrivilege` (SE デバッグ特権) を有効にすることで取得できるようになりますが、この特権はもともとデバッグ用のものであり、一般公開するソフトウェアでは使わない方が良いと思います。

どうしてもシステムプロセスを扱いたい場合のみ `SeDebugPrivilege` (SE デバッグ特権) を有効にして下さい。

参考「[OpenProcess function (processthreadsapi.h) - Win32 apps | Microsoft Docs](https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocess)」
参考「[Changing Privileges in a Token - Win32 apps | Microsoft Docs](https://docs.microsoft.com/en-us/windows/win32/secbp/changing-privileges-in-a-token)」

## 3.5. 自プロセスしか扱えないファイル名取得関数に注意

`GetWindowModuleFileNameW()` 関数や `GetModuleFileNameW()` 関数 (`Ex` なし) は、自プロセス以外では実行ファイル名を取得できません。

自プロセスで生成したウィンドウのハンドルから実行ファイル名を取得するのはあまりしない思うので、多くの場合は `GetModuleFileNameExW()` 関数や `QueryFullProcessImageNameW()` 関数等を使用することになると思います。

参考「[GetWindowModuleFileNameW function (winuser.h) - Win32 apps | Microsoft Docs](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowmodulefilenamew)」
参考「[GetModuleFileNameW function (libloaderapi.h) - Win32 apps | Microsoft Docs](https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulefilenamew)」

## 3.6. PSAPI のバージョンによるコンパイルオプションやライブラリのリンクの違いについて

Windows 7 以降で PSAPI のバージョンが増え、バージョン 2 でライブラリ psapi の機能が kernel32 に吸収されたため、古い Windows で動作させる必要がない場合は `PSAPI_VERSION=2` のようにします。

kernel32 のライブラリは Windows 向けのコンパイラ (MinGW-w64 等も含む) ではデフォルトでリンクされるため、オプションでのライブラリのリンク指定は不要です。

古い Windows で動かす必要がある場合は `PSAPI_VERSION=1` とし、`psapi.lib` をリンクします。

参考「[GetModuleFileNameExW function (psapi.h) - Win32 apps | Microsoft Docs](https://docs.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-getmodulefilenameexw)」

※本記事のコードでは Windows Vista 以降で PSAPI を使用しないため、実質 `PSAPI_VERSION=2`を使用しないようになっています。

## 3.7. プロセスのアクセス権限について

Windows Vista 以降で `PROCESS_QUERY_INFORMATION` 権限より制限が大きい `PROCESS_QUERY_LIMITED_INFORMATION` が利用できます。
今回のプログラムではどちらを利用しても動作は変わりませんが、権限は本来最低限のもののみ許可されるべきなので、利用できる環境なら `PROCESS_QUERY_LIMITED_INFORMATION` を利用した方が良いと思います。

Microsoft Docs によると、`GetModuleFileNameExW()` 関数については追加で `PROCESS_VM_READ` 権限も必要と書かれています。
Windows 10 で動作確認すると、モジュールハンドルが `NULL` の場合に (？) `PROCESS_VM_READ` 権限なしでも実行ファイル名を取得できますが、古い Windows でも同様に処理されるか分からないため、念のため付けておいた方が安全かと思います。

参考「[Process Security and Access Rights - Win32 apps | Microsoft Docs](https://docs.microsoft.com/en-us/windows/win32/procthread/process-security-and-access-rights)」
参考「[GetModuleFileNameExW function (psapi.h) - Win32 apps | Microsoft Docs](https://docs.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-getmodulefilenameexw)」
参考「[QueryFullProcessImageNameW function (winbase.h) - Win32 apps | Microsoft Docs](https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-queryfullprocessimagenamew)」

## 3.8. プロセスクローズを忘れないように注意

プログラミング全般で言えることですが、オープン後に別のエラーが発生した場合、クローズを忘れないように注意が必要です。