ホーム < ゲームつくろー! < クラス構築編 < ファイルをメモリのように「メモリマップドファイルクラス」

ファイルをメモリのように「メモリマップドファイルクラス」


 メモリ空間とファイル空間。Windowsの場合双方ともデータの置き方は一緒です(リトルエンディアン方式)。しかし双方には実質的な違いがあります。メモリはアドレスを持ち、ポインタを通してアクセスできます。しかしファイルはアドレスを持ちません。よってポインタもありません。同じようなデータ格納方法を持ちながら、メモリとファイルとはアクセス方法の互換性が無いんです。

 そこで登場するのが「メモリマップドファイル」です。これを用いると、何と何とファイルをメモリと全く同様な方法でアクセスできるようになります。この章では、まずこの非常に魅力的なメモリマップドファイルについて見ていき、もっともっと使いやすくするために「メモリマップドファイルクラス」としてまとめます。



@ メモリマップドファイルとは?

 まず、メモリマップドファイルについて説明します。これは端的に言いますと、ファイルの内容をメモリ上に反映させることで、ファイルに「「仮想アドレス」を与える技術です。もちろん、その作業自体はOSがやってくれますので、ユーザは用意されたAPIを用いるだけです。最終的には、ファイルの位置を表す仮想アドレスを得える事ができます。これはメモリのアドレスと全く同じなので、メモリとファイルとを区別することなく読み書きが可能となります。

 では、早速メモリマップドファイルを見てみましょう。

 一番最初にやる事は、CreateFile関数を用いてファイルをオープンする事です。

ファイルをオープン
HANDLE hFile;
char *filename = "TestFile.dat";
hFile = CreateFile(  filename, GENERIC_READ, 0, 0, OPEN_EXISTING, 0, 0 );
if( hFile == INVALID_HANDLE_VALUE )
   return false;

CreateFlie関数はファイルをオープンするAPI関数です。フラグの設定によって既存のファイルをオープンしたり、ファイルが無ければ新規作成したりなど、オープンしたファイルに対して色々な属性を設定する事が出来ます。上のプログラムの場合、第2引数に「GENERIC_READ(読み込み専用)」を指定していますので、ファイルへの書き込みはできません。また第5引数を「OPEN_EXISTING(ファイルが存在していたらオープン)」としていることから、ファイルが無ければ関数は失敗します。

 続いて、ファイルとメモリ空間を結びつける(マッピングする)ファイルマッピングオブジェクトを作成します。

ファイルマッピングオブジェクトを作成
HANDLE hMap;
char *mapname = "TestFile";
hMap = CreateFileMapping( hFile, 0, PAGE_READONLY, 0, 0, mapname );
if( hMap <= 0 )
   return false;

CreateFileMapping関数はファイルマッピングオブジェクトを作成します。第1引数にファイルハンドルを渡します。第3引数にはメモリアクセスの保護を指定します。上の例ですと読み込み専用になります。第6引数にはマッピングオブジェクトの名前を与えます。関数が成功すると、ファイルマッピングオブジェクトハンドルが帰ります。失敗した場合はゼロ以下となります。
 CreateFile関数で新規にファイルを生成した場合、ファイルサイズがゼロなのでマップするデータがありません。よって、新規作成ファイルに対してはファイルマッピングオブジェクトは失敗してしまいます。注意してください。

 ここまで来ると、ファイル位置をメモリアドレスとみなした「ポインタ」を取得する事が出来ます!ファイルの先頭ポインタを取得するにはMapViewOfFile関数を用います:

ファイルポインタを取得
char *pPointer;
pPointer = (char*)MapViewOfFile( hMap, FILE_MAP_READ, 0, 0, 0);

第1引数にファイルマッピングオブジェクト、第2引数にアクセスフラグを渡します。ポインタを一度取得しておけば、後はそれを基準にファイルにアクセスできます。これは非常に直接的なアクセスになりますので、メモリ同様の危険性がありますが、ポインタ同様のアクセスの自由度を得ることができます。

 ポインタをもう使用しない場合、然るべき方法でちゃんと閉じる必要があります。

各オブジェクトをクローズ
if( UnmapViewOfFile( pPointer ) )
   return false;   // クローズ失敗

CloseHandle( hMap )   // ファイルマッピングオブジェクトハンドルを閉じる;
CloseHandle( hFile );   // ファイルハンドルを閉じる


 UnmapViewOfFile関数にファイルポインタの先頭を与える事により、マップを解除します。これにより、ポインタの先はダングリングしてしまいますので、これ以上のアクセスをしてはいけません。後はハンドルを閉じておしまいです。以上がメモリマップドファイルの扱い方です。結構簡単ですよね。しかも、とても「クラス化しやすい」状態になっているのが嬉しいところです。

 「メモリにマップするというのはメモリに読み込むことなの?」と思われるかもしれませんが、実はそうではありません。これはあくまでもファイルに仮想アドレスを振り、ファイルをメモリのように見せているだけなので、数100MBというサイズのデータをマップしても、主記憶(メモリ)はちっとも消費されません(Releaseモードでタスクマネージャを見ながら実行してみるとわかります)。この性質は非常にありがたいんです。例えばテクスチャを数100枚まとめたでっかいファイルを作り、そこから必要なデータだけにポインタアクセスできたりします。これは、ファイルアーカイバの操作に繋がるわけです。もちろん、D3DXCreateTextureFromFileInMemory関数などの、メモリ内のデータからテクスチャを生成する関数にも使えます。テクスチャをメモリに読み込まなくてもこの関数を使えるわけです(と言いますか、きっとこの関数はメモリマップドファイルを想定しているんだろうと思います)。



A メモリマップドファイルクラス

 @で説明したハンドル扱いを全部隠蔽して、ファイルをメモリのように扱えてしまう「メモリマップドファイルクラス」を作成してみます。クラス化するからには、なるべく扱いやすくしたいわけで、そういう観点からメソッドを考えてみようと思います。

 まず、ファイルはオープンできないと話しになりませんね。オープンファイル名は指定する必要があります。またこの時、読み込み・書き込みの指定は出きるようにしたいところです。オープン後、ファイルポインタを取得するメソッドを用意します。オブジェクトのクローズはデストラクタの役目です。そう考えると、非常にシンプルなクラスになりそうですね。

 クラスの宣言部はこんな感じでしょうか。

メモリマップドファイルクラス(宣言部)
class CMemMapFile
{
protected:
   HANDLE m_hFile;
   HANDLE m_hMap;
   void* m_pPointer;

public:
   CMemMapFile();
   virtual ~CMemMapFile();

   // ファイルオープン
   virtual bool Open( char* filename, DWORD rwflag=GENERIC_READ | GENERIC_WRITE, DWORD openflag=OPEN_EXISTING);

   // ファイルポインタ取得
   virtual bool GetPtr( void** ptr , char* subfilename=NULL, DWORD *pfilesize=NULL );
};


ファイルオープン関数の実装部分です。

Openメソッド
// ファイルオープン
bool CMemMapFile::Open( char* filename, DWORD rwflag, DWORD openflag)
{
   // ファイルオープン
   m_hFile = CreateFile( filename, rwflag, 0, 0, openflag, 0, 0 );
   if( m_hFile == INVALID_HANDLE_VALUE )
      return false;

   // ファイルマッピングオブジェクトを作成
   DWORD mapflag = PAGE_READONLY;
   if( rwflag == (GENERIC_WRITE | GENERIC_READ) )
      mapflag = PAGE_READWRITE; // 読み書き属性
   m_hMap = CreateFileMapping( m_hFile, 0, mapflag, 0, 0, filename );
   if( m_hMap <= 0 ){
      CloseHandle( m_hFile );
      m_hFile = INVALID_HANDLE_VALUE;
      return false;
   }

   // ポインタを取得
   DWORD mapviewflag = FILE_MAP_READ;
   if( mapflag == PAGE_READWRITE )
      mapviewflag = FILE_MAP_WRITE;
   m_pPointer = (char*)MapViewOfFile( m_hMap, mapviewflag, 0, 0, 0);
   if( m_pPointer == NULL){
      CloseHandle( m_hMap );
      CloseHandle( m_hFile );
      m_hMap = 0;
      m_hFile = INVALID_HANDLE_VALUE;
      return false;
   }

   return true;
}


ポインタの取得を担うGetPtr関数にはサブファイル名を指定する引数がありますが、これはアーカイバの中から指定のファイル部分を読み込む時に使用することを見越しています。全ての関数は仮想関数として定義されていますので、派生クラスで独自のアーカイバの読み込みをさせる事が出来ます。


 メモリマップドファイルは比較的簡単に使える割には強力な機能を持っています。ファイルの読み書きに関しても自由ですし、ランダムアクセスだってポインタ演算を用いてあっさり出来ます。ただ、ファイルアクセスをしている事には変わりませんので、メモリより読み書きが遅いのは間違いありません。しかしながら、これとファイルアーカイバを合わせれば、リソースの扱いにちょっとした革命が起こりますので、活用してみて下さい(^-^)。

 クラスは実装と合わせて公開致します。またその簡単な使用例をサンプルプログラムとしてアップしましたので合わせてご覧下さい。