ファイルアクセス#

MGLはプラットフォームに依存しないファイルアクセス機能を備えています。また、必要に応じてファイルアクセスを特殊化する事も可能です。

本項では、基本的なファイルアクセスの方法についてを解説します。ファイルアクセスの特殊化や、標準で対応していないプラットフォームへの対応はファイルデリゲートの作成(準備中)を参照してください。

MGLにおけるファイルアクセスの仕組み#

MGLはプラットフォーム毎の差異や特殊化されたファイルアクセスを解決するために、ファイルマウントの仕組みを備えています。特定のディレクトリやファイルは、そこにアクセスするためのデリゲートクラスと共にマウント名と紐付けられ、アプリケーションからはそれらの差異を意識する事なく透過的にファイルアクセスが可能となります。

MGLのファイルシステムの概略図
../_images/filesystem_diagram.svg

標準構成においてはマウント処理は必須ではなく、MGLの初期化処理がいくつかのディレクトリをマウントした状態でアプリケーション側の処理を呼び出します。唯一意識すべき点は、パスの指定の際にマウント名を含める事のみです。

アプリケーションからはハンドルクラスとユーティリティクラスを利用してファイルアクセスを行います。本項では主にこれらの利用方法について解説します。

ファイルパスのフォーマット#

MGLが扱うファイルパスは、先頭に$ (0x24)を付与したマウント名で始まり、区切り文字に/ (0x2F)を使用します。また、パスはUTF-8エンコーディングである必要があります。

標準構成で最初からマウントされている、リソースディレクトリを表すマウント名resourceを利用した例を次に示します。

リソースディレクトリにあるファイルimage.pngへのパス
$resource/image.png

先頭の$から最初の区切り文字までの文字列がマウント名となります。リソースディレクトリの具体的な場所はプラットフォーム毎に異なりますが、アプリケーション側からはそれを意識する必要はありません。

標準構成におけるマウント済みのディレクトリの情報はプラットフォーム別情報を参照してください。

マウント名を省略した場合、指定したパスがそのままデフォルトのデリゲートに渡されます。そのパスをどのように解釈するかはデリゲートに依存するため、通常はアプリケーション側のプログラムでのマウント名の省略は推奨されません。各プラットフォーム固有の初期化処理において、マウント先を指定する場合にのみ使用してください。

ファイルパスクラス#

MGL向けのファイルパスを表現するために、MGL::File::PathクラスおよびMGL::File::PathViewクラスが用意されています。これらはSTLにおけるstd::filesystem::pathの代替となるものであり、マウント名の取得やパスの解析・結合などに対応しています。

MGL::File::Pathは自身でパス文字列を管理するファイルパスクラスです。与えられた文字列を内部にコピーするため、不正な参照のリスクが無く安全性に優れます。一方で、メモリリソースの確保と文字列のコピーを行うため、使用時に若干のオーバーヘッドが生じます。また、MGLのメモリアロケータを経由する関係上、MGLの初期化前には使用することができません。

const char *型と同じようにパスを扱いたい場合はMGL::File::PathViewが利用できます。このクラスは内部で文字列へのアドレスのみを保持し、参照だけを行う読み取り専用のファイルパスクラスです。使用の際にオーバーヘッドが生じず、定数として定義することも可能です。その反面、安全性もconst char *型と同等であり、使用中に参照先の内容が変化しないよう注意しなければなりません。

両者の使い分けとして、MGL::File::Pathはファイルパスの保持や編集する場合に向き、MGL::File::PathViewは関数の引数や定数宣言に向いていると言えます。MGLのAPIのインターフェースはMGL::File::PathViewを標準的に用いており、実装側で保持する必要がある場合のみMGL::File::Pathへとコピーしています。

より詳しい情報は両者のAPIリファレンスを参照してください。

ハンドルよるファイルアクセス#

ファイルの内容に対するアクセスはMGL::File::HandleまたはMGL::File::ThrowingHandleを利用します。両者の違いはエラー発生時のハンドリングの方法で、前者は手動で処理結果を取得する必要があり、後者は例外の送出を行います。

本項では処理の簡略化のために、例外を送出するMGL::File::ThrowingHandleを用いて解説します。MGL::File::Handleでも基本的な利用方法に違いはありません。

ハンドルクラスを利用するには、最初に対象ファイルをオープンします。ファイルのオープンは、コンストラクタまたはMGL::File::ThrowingHandle::Openの引数にパスを指定します。第2引数にはMGL::File::OpenModeReadまたはWriteを指定し、Readの場合は読み込みモードで、Writeの場合は書き込みモードでオープンします。オープンモードの指定は省略可能で、その場合はReadとして扱われます。

ファイルをオープンする例
try
{
    // コンストラクタでファイルをオープン
    MGL::File::ThrowingHandle handle("$user/test.data", MGL::File::OpenMode::Write);
}
catch(MGL::File::Result result)
{
    // エラーコードの表示
    MGL_TRACE("処理失敗: %d", result.GetErrorCode());
}

ここでは、ユーザーディレクトリにある"test.data"というファイルを書き込みモードでオープンしています。処理に失敗した場合、その理由を表すMGL::File::Resultが例外として送出されるため、それをハンドリングしてエラーコードを表示しています。エラーコードの種類についてはMGL::File::Errorを参照してください。

ファイルのクローズ処理は自動で行われます。何らかの理由により明示的に行いたい場合はMGL::File::ThrowingHandle::Closeを呼び出してください。

ファイルの内容へのアクセスは、読み込みにMGL::File::ThrowingHandle::Readを利用し、書き込みにはMGL::File::ThrowingHandle::Writeを利用します。まずは書き込みの例を次に示します。

ファイルに内容を書き込む例
try
{
    // 書き込むデータ(文字列)
    const MGL::STL::string kTestMessage("Hello, MGL");

    // ファイルをオープン
    MGL::File::ThrowingHandle handle("$user/test.data", MGL::File::OpenMode::Write);

    // 書き込み
    auto writeSize = handle.Write(kTestMessage.c_str(), kTestMessage.size() + 1);
    MGL_TRACE("%zuバイト書き込み成功。", writeSize);
}
catch(MGL::File::Result result)
{
    // エラーコードの表示
    MGL_TRACE("処理失敗: %d", result.GetErrorCode());
}
実行結果
11バイト書き込み成功。

この例では、"test.data"に対して文字列"Hello, MGL"を書き込んでいます。

MGL::File::ThrowingHandle::Writeの第1引数には書き込むデータが格納されているアドレスを、第2引数には書き込むサイズを指定します。ここでは、文字列に終端記号を含めるためにサイズ指定を1バイト多く指定しています。

戻り値には実際に書き込んだサイズが返されます。正常に処理されている場合は第2引数のサイズ指定と同じ値になります。

次に、上記で生成したファイルを読み込むMGL::File::ThrowingHandle::Readの利用例を示します。

ファイルの内容を読み込む例
try
{
    // 読み込み先バッファを準備
    constexpr const size_t kBufferSize = 64;
    char buffer[kBufferSize];

    // ファイルをオープン
    MGL::File::ThrowingHandle handle("$user/test.data");

    // 読み込み
    auto readSize = handle.Read(buffer, kBufferSize);
    MGL_TRACE("%zuバイト読み込み成功。", readSize);
    MGL_TRACE(buffer);
}
catch(MGL::File::Result result)
{
    // エラーコードの表示
    MGL_TRACE("処理失敗: %d", result.GetErrorCode());
}
実行結果
11バイト読み込み成功。
Hello, MGL

MGL::File::ThrowingHandle::Readの引数も書き込み時と同様に、第1引数に読み込んだデータの格納先アドレスを、第2引数に読み込むサイズを指定します。

戻り値には実際に読み込んだサイズが返ります。この値は読み込み時にファイルの終端に達していなければ第2引数と同じ値になりますが、途中で終端に達した場合は指定した値よりも小さくなる場合があります。上記の例では、11バイトの内容に対して64バイトを要求しているため、戻り値は11です。

もう少し複雑な読み込み処理の例を示します。次の例は、ファイルの先頭4バイトをスキップし、残りの全てのデータを1バイトづつ表示する例です。

ファイルの内容を複雑に読み込む例
try
{
    // ファイルをオープン
    MGL::File::ThrowingHandle handle("$user/test.data");

    // 最初の4バイトをスキップ
    handle.Skip(4);

    // 終端まで1バイトづつ読み込んで表示
    while (!handle.IsEOF())
    {
        char c;
        handle.Read(&c, 1);

        MGL_TRACE("%c(%02X)", c, c);
    }
}
catch(MGL::File::Result result)
{
    // エラーコードの表示
    MGL_TRACE("処理失敗: %d\n", result.GetErrorCode());
}
実行結果
o(6F)
,(2C)
 (20)
M(4D)
G(47)
L(4C)

MGL::File::ThrowingHandle::Skipはファイルのストリーム位置を指定したサイズ分進める関数です。ファイルの終端のチェックにはMGL::File::ThrowingHandle::IsEOFを使用し、戻り値がfalseでいる間だけループを回すようにしています。

ファイルユーティリティクラス#

ファイルユーティリティクラスはファイルシステムに対する操作を行うためのクラスです。MGL::File::UtilityまたはMGL::File::ThrowingUtilityで定義されており、両者の違いはハンドルクラスと同様に、エラーのハンドリング方法のみとなります。

ファイルユーティリティクラスが持つ機能は様々です。ここでは一例として、「リソースディレクトリのimage.pngというファイルをテンポラリディレクトリにコピーする」例を示します。

リソースディレクトリのimage.pngをテンポラリディレクトリにコピーする例
try
{
    MGL::File::ThrowingUtility utility;

    // 既にコピー先に存在していたら処理をスキップ
    if (utility.Exists("$temp/image.png"))
    {
        MGL_TRACE("処理をスキップ");
    }
    else
    {
        // ファイルのコピー
        utility.Copy("$resource/image.png", "$temp/image.png");
        MGL_TRACE("コピー完了");
    }
}
catch(MGL::File::Result result)
{
    // エラーコードの表示
    MGL_TRACE("処理失敗: %d", result.GetErrorCode());
}

ここではMGL::File::ThrowingUtility::Existsを用いてコピー先に既にファイルが無いかをチェックし、存在していなければMGL::File::ThrowingUtility::Copyを用いてコピーを実行しています。

注釈

Windowsの標準構成ではリソースディレクトリとテンポラリディレクトリが同一であるため、このサンプルは常に処理をスキップします。

その他の機能と利用例については、各々のAPIリファレンス(MGL::File::UtilityまたはMGL::File::ThrowingUtility)を参照してください。

ディレクトリやファイルのマウント#

標準構成においてはMGLの初期化処理内でディレクトリへのマウントを行っていますが、アプリケーションの都合に応じて後から追加や変更も可能です。

マウントに関する機能はMGL::File::UtilityまたはMGL::File::ThrowingUtilityが備えています。例として、リソースディレクトリ内のモデルデータを格納した"model"というディレクトリを、同名のマウント名でアクセスする場合の例を示します。

$resource/model/ディレクトリを$model/でアクセスできるようにする例
try
{
    MGL::File::ThrowingUtility utility;

    // "$resource/model/" を "model" という名前でマウント
    utility.Mount("model", "$resource/model", MGL::File::MountAccessType::ReadOnly);
}
catch(MGL::File::Result result)
{
    // エラーコードの表示
    MGL_TRACE("処理失敗: %d", result.GetErrorCode());
}

MGL::File::ThrowingUtility::Mountの第1引数はマウント名、第2引数はマウント先のパスです。

第3引数のMGL::File::MountAccessTypeはそのマウント名を使用したアクセス権限の指定で、ここでReadOnlyが指定された場合は$model/以下への書き込みが制限されます。Writableを指定した場合、書き込みへの制限は適用されません。

ここでは省略していますが、第4引数には使用するデリゲートを指定します。省略した場合はデフォルトのデリゲートが利用されます。この引数は主にファイルアクセスを特殊化する場合に使用します。

上記の例でマウントした$modelのマウント解除は次の通りです。

$modelのマウントを解除
try
{
    MGL::File::ThrowingUtility utility;

    // $model のマウントを解除
    utility.Unmount("model");
}
catch(MGL::File::Result result)
{
    // エラーコードの表示
    MGL_TRACE("処理失敗: %d", result.GetErrorCode());
}

こちらは単にMGL::File::ThrowingUtility::Unmountを呼び出すだけとなっています。

マウント解除後、それまでオープンしていたファイルへのアクセスの可否はデリゲートに依存します。標準のファイルデリゲートは継続して利用可能です。

もう1つ、利用する機会の多い例を示します。次の例は、ユーザーディレクトリにユーザーIDを含んだサブディレクトリを生成し、そこを新たなユーザーディレクトリとする例です。このような処理は、Steamなどの複数ユーザーでアカウントを切り替えて実行できる環境で必要になります。

ユーザーIDでサブディレクトリを生成し、そこを新たなユーザーディレクトリにする例
if (auto *steamUser = SteamUser(); steamUser != nullptr)
{
    // Steamの64bitユーザーIDを取得
    // (これはMGLとは無関係だけど、例として適当だったため)
    auto userID = steamUser->GetSteamID().ConvertToUint64();

    try
    {
        MGL::File::ThrowingUtility utility;

        // 新たに設定するユーザーディレクトリのパスを生成
        auto newUserDirectory = MGL::Text::Format("$user/{}", {userID});

        // 新たなユーザーディレクトリを作成
        utility.MakeDirectory(newUserDirectory.c_str());

        // 新たなユーザーディレクトリのパスをフルパスに変換
        // (後のRemount()は $user を一旦解除してしまうため、フルパスで指定する必要がある)
        newUserDirectory = utility.GetSystemNativePath(newUserDirectory.c_str());

        // ユーザーディレクトリを再マウント
        utility.Remount(
                    "user",
                    newUserDirectory.c_str(),
                    MGL::File::MountAccessType::Writable);
    }
    catch(MGL::File::Result result)
    {
        // エラーコードの表示
        MGL_TRACE("処理失敗: %d", result.GetErrorCode());
    }
}

処理内容を大まかに説明すると、MGL::File::ThrowingUtility::MakeDirectoryでディレクトリを生成し、そのパスでMGL::File::ThrowingUtility::Remountを呼び出して$userのマウント先を変更しています。ただし、再マウントの際に$userが一時的に利用不可能となるため、MGL::File::ThrowingUtility::GetSystemNativePathにてその環境でのフルパスへの変換を挟んでいます。

新たなディレクトリパスの生成に利用しているMGL::Text::Formatについては、テキスト整形を参照してください。

マウント先はディレクトリだけでなく、通常ファイルに対しても行えます。この動作はアーカイブファイルの内容を直接扱いたい場合に有用です。ただし、標準のファイルデリゲートでは対応しておらず、専用のデリゲートを実装して登録する必要があります。ファイルデリゲートの実装についてはファイルデリゲートの作成(準備中)を参照してください。

プラットフォーム別情報#

MGLの標準構成におけるプラットフォーム別の情報です。

Windows#

マウント先一覧

マウント名

アクセスタイプ

場所

resource

ReadOnly

カレントディレクトリ

user

Writable

カレントディレクトリ

exec

Writable

実行ファイルのあるディレクトリ

temp

Writable

カレントディレクトリ

cwd

Writable

カレントディレクトリ

Windows向けの標準ファイルデリゲートはドライブレターから始まるパスを受け付けますが、区切り記号は\ (0x5C)ではなく/ (0x2F) を使用します。また、パスの文字コードは入力・出力共にUTF-8エンコーディングを使用します。MGL::File::Utility::GetSystemNativePathなどの戻り値をWin32 APIに渡す場合には文字コードの変換が必要となります。

Visual Studioからデバッグを開始して実行した場合、実行ファイルのあるディレクトリはビルド構成によって、カレントディレクトリはプロジェクトのプロパティの設定によってそれぞれ変化します。カレントディレクトリの設定は作業ディレクトリの設定を参照してください。

macOS#

マウント先一覧

マウント名

アクセスタイプ

場所

resource

ReadOnly

(アプリケーションバンドル)/Contents/Resources/

user

Writable

~/Library/Application Support/(バンドルID)/ (※1:補足参照)

exec

Writable

(アプリケーションバンドル)/Contents/MacOS/

temp

Writable

システムが自動生成 (※2:補足参照)

cwd

Writable

カレントディレクトリ

補足
  • ※1: Appサンドボックスを有効にしている場合、ホームディレクトリが次の場所へと変更されます

    • ~/Library/Containers/(バンドルID)/Data/

  • ※2: 永続性が保証されておらず、システムの都合により削除される場合があります

macOS向けの標準ファイルデリゲートはPOSIX準拠のCインターフェースのAPIで作成された汎用的なものが使用されています。/から始まるルートディレクトリからのアクセスや、~から始まるホームディレクトリへのアクセスも可能です。

Appサンドボックスを有効にしている場合、アクセス可能なディレクトリに強い制限が課せられます。デフォルトのマウント先とそのサブディレクトリには自由にアクセス可能ですが、それ以外の場所に対しては適切な権限の設定が必要になります。

iOS/iPadOS#

マウント先一覧

マウント名

アクセスタイプ

場所

resource

ReadOnly

アプリケーションバンドル内のリソースディレクトリ

user

Writable

ドキュメントディレクトリ

temp

Writable

システムが自動生成 (※1:補足参照)

補足
  • ※1: 永続性が保証されておらず、システムの都合により削除される場合があります

iOS/iPadOS向けの標準ファイルデリゲートはAppleプラットフォーム向けに実装されたものが使用されています。これはPOSIX準拠のデリゲートの一部をNSFileManagerによる実装に置き換えたもので、AppStore向けアプリケーションへの規約に適合するための対応となっています。

iOS/iPadOSのディレクトリ構成は環境によって変化するため、ルートディレクトリやホームディレクトリ、相対パスなどの利用は推奨されません。必ずマウント名を使用するか、APIから取得したパスをそのまま使用してください。

tvOS#

マウント先一覧

マウント名

アクセスタイプ

場所

resource

ReadOnly

アプリケーションバンドル内のリソースディレクトリ

temp

Writable

システムが自動生成 (※1:補足参照)

補足
  • ※1: 永続性が保証されておらず、システムの都合により削除される場合があります

tvOSの性質はほぼiOS/iPadOSと同じですが、永続的に書き込み可能な領域が存在しないことに注意してください。製品の性質上、セーブデータなどのユーザーデータはiCloudなどのクラウドストレージに保存する必要があります。