Seaside Laboratory

Posts

実行ファイルのパスを取得する方法

ファイルへのアクセスが必要なプログラムを作るときは基準となるディレクトリを正しく把握しておく必要がある。多くの場合、実行ファイルが置かれているディレクトリが基準となるが、このディレクトリの取得や扱いには注意しなければいけない点が多い。

パスの取得方法

実行ファイルのパスは様々な方法で取得することができる。

現在のディレクトリ

通常、作業ディレクトリは実行ファイルのディレクトリと同じになるので、GetCurrentDirectory で現在のディレクトリを取得する。

DWORD GetCurrentDirectory(
  DWORD  nBufferLength, // バッファの長さ
  LPTSTR lpBuffer       // ディレクトリ文字列を受け取るバッファへのポインタ
);

一番簡単な方法ではあるが、実行ファイルへファイルをドラッグ&ドロップしたり、コマンドプロンプトからファイル名を指定してプログラムを起動すると、作業ディレクトリが実行ファイルとは別の場所に変わってしまう問題がある。

モジュールのファイルパス

実行ファイルが置かれているパスは起動方法によって変化するものではないので、GetModuleFileName で実行ファイルのフルパスを取得する。

DWORD GetModuleFileName(
  HMODULE hModule,      // モジュールへのハンドル
  LPSTR   lpFilename,   // 完全修飾パスを受け取るバッファへのポインター
  DWORD   nSize         // バッファのサイズ
);

フルパスにはファイル名も含まれているので、ファイル名を削り落としてディレクトリパスに変換する必要がある。

コマンドライン引数

コマンドライン引数の第 1 引数には実行ファイルのパスが格納されているので、そこから取得することもできる。コマンドライン引数自体は WinMain の第 3 引数か GetCommandLine で取得することになるが、WinMain の方は実行ファイルのパスが含まれていないので GetCommandLine を使うことになる。

LPSTR GetCommandLine();

main 関数の argv とは違い、未処理のコマンドライン文字列がそのまま渡ってくるので、パース処理を自分で行わなければならないのと、

The name of the executable in the command line that the operating system provides to a process is not necessarily identical to that in the command line that the calling process gives to the CreateProcess function.The operating system may prepend a fully qualified path to an executable name that is provided without a fully qualified path.

オペレーティングシステムがプロセスに提供するコマンドラインの実行ファイル名は、呼び出し元のプロセスが CreateProcess 関数に提供するコマンドラインの実行ファイル名と必ずしも一致しません。オペレーティングシステムは、完全修飾パスなしで提供された実行ファイル名の前に完全修飾パスを付加する場合があります。

フルパスが保証されるのかあやしいので避けておくのが無難か。

Visual Studio からの起動

Visual Studio でビルドした際に生成される実行ファイルは、デバッグ時は Debug、リリース時は Relese ディレクトリに出力されるが、実行するとプロジェクトディレクトリ上にあるかのように振る舞ってくれるので、構成の設定に関わらずプロジェクト配下にあるファイルに問題なくアクセスすることができる。

これは作業ディレクトリをプロジェクトディレクトリに設定することで実現されているが、逆に言えば作業ディレクトリを無視するプログラムはこの機能がうまく動作しない。つまり、GetModuleFileName を使ったプログラムは Visual Studio から起動すると正常に動作しなくなる。

出力先を変更する

そもそも実行ファイルのパスと作業ディレクトリが違うのが問題なので、作業ディレクトリ (プロジェクトディレクトリ) に実行ファイルを出力してやるという方法。

プロジェクトのプロパティにある「リンカー」の「出力ファイル」を、

$(OutDir)$(TargetName)$(TargetExt)

から

$(ProjectDir)$(TargetName)$(TargetExt)

に変更してやればプロジェクトディレクトリに出力することができる。

ただ、下記のような問題が発生するのでプロジェクトの管理に影響が出てしまう。

  • プロジェクトを作る度に「出力ファイル」を設定し直す必要がある。
  • プロジェクト配下にあるソースコード群に実行ファイルが混ざってしまう。
  • Debug 版と Release 版の実行ファイルはデフォルト設定だと同じファイル名なのでビルドする度に上書きされてしまう。
親プロセスで切り分ける

Visual Studio から起動されたときだけ GetCurrentDirectory を使うという方法。起動元の検出は親プロセスが Visual Studio かどうかで判断する。

親プロセスの情報は Process32First と Process32Next を使って全プロセスを列挙し、その中から親プロセスを探し出すという面倒なコードを書かなければ取得できず、しかも開発時にしか使わないコードが残り続けるので、起動の度に不必要な判定が行われることになる。

構成で切り分ける

基本的に Visual Studio から起動するのは Debug 版なので、Debug 時だけ GetCurrentDirectory を使うという方法。構成の違いは NDEBUG マクロとプリプロセッサを使って切り分けるので、両方のコードが残ってバイナリサイズが大きくなる、といった心配はない。

完全な対処方法ではないので、Debug 版はドラッグ&ドロップすると正常に動作しないし、Release 版はコードが正常に動作するのか確認したくてもデバッグ情報がないのでトレースできないという欠点はある。