Win32 API でのファイルアクセス

今回は、 bload 命令や bsave 命令ではなく、純粋に Win32 API のみを使ってファイルの書き込みや読み取りをしてみたいと思います。「bloadbsave でできるのに、なぜそんな面倒っちいことを」などとも思うかもしれませんが、まあ、いずれ必要になることですので。


今回はいろいろと前知識が必要になります。まずはそれらから説明することにしましょう。

ファイルオブジェクトとハンドル

ファイル操作をする際にまずファイルを開く(オープンする)ということをしなくてはなりません。Windows上では、データアクセスするためにオープンされたファイルは、カーネルオブジェクトの1つである『ファイルオブジェクト』として扱われます。ファイルがオープンされると、メモリ上にファイルオブジェクトが作成され、そのオブジェクトを識別する値として、ファイルオブジェクトのハンドルが返されます。このハンドルはファイルハンドルとも呼ばれます。ファイル操作を行なうにはこのハンドルを用いて各種API関数を呼び出すことになります。

Windowsオブジェクトおよびハンドルについては、『共有メモリを使ってみる』の項を参照してください。

実際の読み書きの際には、ファイルオブジェクトがどのアクセスを持っているかということが重要になります。適切なアクセス指定によって作成されたファイルオブジェクトのハンドルを指定しないと、目的となるファイルアクセスを行なうことができなくなってしまいます。

例えば、ファイルからデータを読み取りたい場合には、ファイルオブジェクトが読み取りアクセスを持っていなければなりませんし、ファイルにデータを書き込みたい場合には読み取りアクセスを持っていなければなりません。読み取りアクセスを持たない場合には、たとえ書き込みアクセスを持っていてもデータを読み取ることができません。

ファイルポインタ

APIを使ってファイルの読み書きを行なう場合、ファイルポインタと呼ばれるものが使われます。ファイルポインタとは、現在読み書きしている位置をバイト単位で表したインデックスのことで、ファイルオブジェクトごとに内部で保持されています。通常、ファイルへの読み書きを行なうと、そのサイズぶんだけファイルポインタが自動的に移動して、次回読み書き時にはそのファイルポインタの位置から読み書きが行なわれます。

シーケンシャル処理とランダム処理

ファイル処理の方法にはシーケンシャルファイル処理(シーケンシャルアクセス)とランダムファイル処理(ランダムアクセス)の2種類の方法があります。

シーケンシャルファイル処理とは、ファイルの前回の読み書き部分に続いて次の読み書きを行なう、というものです。基本的には、ファイルの先頭からファイルの終わりまでを連続的に書き込みあるいは読み取りしていきます。例えば、ファイルに10バイトずつ並んでいるデータを順番に読み込んで処理する、といった場合の方法です。

ランダムファイル処理とは、ファイルを読み書きする際に、ファイルのどの位置を読み書きするかを指定するものです。すなわち、ファイルポインタの位置を指定してから読み書きを行なう、ということになります。HSPの bload 命令や bsave 命令で行なわれる処理はこのランダム処理になります。

実際の手順

さて、HSPの標準命令を使う場合には bload 命令や bsave 命令だけで済んでしまうことなのですが、これをAPIで実現するにはかなりの手間がかかります。基本的な手順としては以下のようになります。

  1. CreateFile 関数で既存のファイルをオープン(または新しいファイルを作成)し、そのファイルオブジェクトのハンドルを取得する。
  2. 必要ならば SetFilePointer 関数でファイルポインタを移動させる。
  3. WriteFile 関数でファイルに書き込む。または ReadFile 関数でファイルからデータを読み込む。
  4. CloseHandle 関数でファイルハンドルをクローズする。

ファイルのオープン

ファイルをオープンしたり、新しいファイルを作成するには CreateFile 関数を呼び出します。

HANDLE CreateFileA(
    PCTSTR pszFileName,          // ファイル名
    DWORD  dwAccess,             // アクセス指定
    DWORD  dwShare,              // 共有方法
    PSECURITY_ATTRIBUTES psa,    // セキュリティ属性
    DWORD  dwCreatDisposition,   // 動作指定
    DWORD  dwFlagsAndAttributes, // フラグと属性
    HANDLE hTemplate             // テンプレートファイル
);

pszFileName パラメータにはファイル名を表す文字列のアドレスを指定します。

dwAccess パラメータには、アクセス指定子を指定します。データ書き込みを行なう場合には 0x40000000 (GENERIC_WRITE) を、データ読み取りを行なう場合には 0x80000000 (GENERIC_READ) を指定します。書き込みと読み取りの両方を行なうには、これら2つを組み合わせた値 (0xC0000000) 指定します。

dwShare パラメータには、他のプロセスとファイルを共有するかどうか、すなわち、自分のプロセスがファイルをオープンしている間に他のプロセスがこのファイルをオープンしようとしたときに、それを許可するかどうかを示す値を指定します。

psa パラメータにはハンドル継承に関する SECURITY_ATTRIBUTES 構造体のアドレスを指定しますが、通常は 0 (NULL) を指定すれば OK です。

dwCreationDisposition パラメータには、指定されたファイル名がすでに存在するとき、またはしないときのそれぞれの動作を指定します。例えば、新しいファイルを作成する(存在する場合には上書きする)には、 2 (CREATE_ALWAYS) を指定します。また、すでに存在するファイルをオープンしたい場合には 3 (OPEN_EXISTING) を指定します。

dwFlagsAndAttributes パラメータには、作成するファイルの属性およびフラグを指定します。ファイル属性として通常は 0x00000080 (FILE_ATTRIBUTE_NORMAL) を指定します。

hTemplateFile パラメータには 0 (NULL) を指定します。

CreateFile 関数は、指定されたファイルのファイルオブジェクトを作成し、戻り値としてファイルオブジェクトののハンドルを返します。また、エラーにより失敗した場合には -1 (INVALID_HANDLE_VALUE) が返ります。

ファイルがオープンされた時点ではファイルポインタはファイルの先頭にあるので、ファイルの先頭から読み書きする場合(シーケンシャルファイル処理の場合)はそのままファイル読み取り・書き込みを行なうことができます。

ファイルポインタの移動(ランダムアクセス時)

さて、ファイルの先頭から読み書きする場合(シーケンシャルファイル処理の場合)はそのままデータの読み取り・書込み操作を行なうことができますが、ファイル操作の開始位置を指定したい場合(ランダム処理の場合)は、どこから読み書きを始めるかを指定する必要があります。

これには、 SetFilePointer 関数を呼び出して、ファイルポインタを移動させます。

DWORD SetFilePointer(
    HANDLE hFile,          // ファイルハンドル
    LONG   lDistance,      // 下位オフセット
    PLONG  pDistanceHigh,  // 上位オフセットの変数
    DWORD  dwMoveMethod    // 移動開始点
);

hFile パラメータにはファイルハンドルを指定します。

lDistance パラメータには、移動するファイルポインタのオフセット(移動量)をバイト単位で指定します。指定するオフセットの絶対値が 0x7FFFFFFE (= 231 - 2) を超えるような場合には、符号付き64ビット数値で表された値の下位32ビットを指定します。

pDistanceHigh パラメータには、指定するオフセットの絶対値が 0x7FFFFFFE (= 231 - 2) を超えるような場合に、符号付き64ビット数値で表された値の上位32ビットを格納した変数のアドレスを指定します。オフセット値の絶対値が 0x7FFFFFFE を超えない場合は 0 (NULL) を指定することができます。サイズが 0x7FFFFFFE を超えるほどの大きさのファイル(2G バイト以上)を操作することはほとんどないと思うので、0 (NULL) を指定することになると思いますが。

dwMoveMethod パラメータには、移動の開始点を指定します。 0 (FILE_BEGIN) を指定すると、ファイルポインタがファイルの先頭からオフセットぶんだけ移動したところになります。 1 (FILE_CURRENT) を指定すると現在のファイルポインタの位置が、 2 (FILE_END) を指定するとファイルの終端がそれぞれ移動の開始点になります。

この関数は戻り値として新しいファイルポインタの下位32ビット値を返します。 pDistanceHigh パラメータに変数のアドレスを指定した場合には、この変数に上位32ビット値が格納されます。また、関数が失敗すると戻り値 0xFFFFFFFF を返します。ただ、 pDistanceHigh パラメータに変数のアドレスを指定した場合には、関数が成功しても 0xFFFFFFFF が返る可能性もあるので、そのあたりの配慮は必要です。 pDistanceHigh パラメータに 0 (NULL) を指定しているのならば問題はありませんが。

ファイルの読み取り

ファイルの読み取りは ReadFile 関数を使って行ないます。

BOOL ReadFile(
    HANDLE   hFile,                // ファイルハンドル
    LPCVOID  pBuffer,              // バッファアドレス
    DWORD    nNumberOfBytesToRead, // サイズ
    LPDWORD  pNumberOfBytesRead,   // 実際のサイズを格納する変数
    LPOVERLAPPED  pOverlapped      // OVERLAPPED構造体
);

hFile パラメータには、操作するファイルのハンドルを指定します。このハンドルは、読み取りアクセス(GENERIC_READ アクセス)を持ったものでなければなりません。

pBuffer パラメータには、読み取ったデータを格納するバッファのアドレスを指定します。

nNumberOfBytesToRead パラメータには、読み取るデータのサイズをバイト単位で指定します。

pNumberOfBytesRead パラメータには、実際に読み取られたデータのサイズを格納するための変数のアドレスを指定します。関数呼出し後に、この変数にデータサイズが格納されます。この値が 0 になった場合は、ファイルポインタがファイルの終端を超えたことを示します。

pOverlapped パラメータには 0 (NULL) を指定します。

この関数は、現在のファイルポインタの位置から、指定されたサイズ分だけのデータを読み取り、バッファに格納します。

ファイルの書き込み

ファイルの書き込みには WriteFile 関数を使います。

BOOL WriteFile(
    HANDLE   hFile,                  // ファイルハンドル
    LPCVOID  pBuffer,                // バッファアドレス
    DWORD    nNumberOfBytesToWrite,  // サイズ
    LPDWORD  pNumberOfBytesWritten,  // 実際のサイズを格納する変数
    LPOVERLAPPED  pOverlapped        // OVERLAPPED構造体
);

hFile パラメータには、操作するファイルのハンドルを指定します。このハンドルは、書き込みアクセス(GENERIC_WRITE アクセス)を持ったものでなければなりません。

pBuffer パラメータには、書き込むデータが格納されているバッファのアドレスを指定します。

nNumberOfBytesToWrite パラメータには、書き込むデータのサイズをバイト単位で指定します。

pNumberOfBytesWritten パラメータには、実際に書き込まれたデータのサイズを格納するための変数のアドレスを指定します。関数呼出し後に、この変数には実際に書き込まれたデータサイズが格納されます。

pOverlapped パラメータには 0 (NULL) を指定します。

ファイルサイズ

ファイルサイズの取得

ファイル全体を一度に読み取る場合には、あらかじめファイルサイズを取得しておく必要がありますね。ファイルサイズを取得するには、 GetFileSize 関数を呼び出します。

DWORD GetFileSize(
    HANDLE hFile,          // ファイルハンドル
    PDWORD pFileSizeHigh   // 上位32ビットを格納する変数
);

hFile パラメータには、操作するファイルのハンドルを指定します。このハンドルは、書き込みアクセス(GENERIC_WRITE アクセス)または読み取りアクセス(GENERIC_READ アクセス)を持ったものでなければなりません。

pFileSizeHigh パラメータには、ファイルサイズが 4G バイト以上だったときに、サイズの上位32ビットを格納するための変数のアドレスを指定します。ファイルサイズが 4G バイト未満であると分かっているなら、このパラメータに 0 (NULL) を指定することもできます。

この関数は、戻り値としてファイルサイズ(サイズが 4G バイト以上のときはその下位32ビット)を返します。

ファイルサイズの設定

WriteFile 関数を使ってファイルにデータを書き込んでいく場合には、以前のファイルサイズを越えて書き込まれた場合にファイルサイズは自動的に拡張されていくのですが、例えば、ファイルの後ろの部分を削除したい場合、すなわち、はじめの部分のファイル内容はそのままにしてファイルサイズを減らしたいという場合がありますね。このようなときには、新しくファイル終端(end-of-file : EOF)の位置を設定する必要があります。

新しくファイル終端位置を設定するには、まず SetFilePointer 関数を呼び出してファイル終端を設定したい位置にファイルポインタを移動した後で、 SetEndOfFile 関数を呼び出します。

BOOL SetEndOfFile(
    HANDLE hFile     // ファイルハンドル
);

この関数は、現在のファイルポインタがある位置にファイル終端を設定します。 hFile パラメータには、ファイル終端位置を設定したいファイルのハンドルを指定します。

サンプルスクリプト

さっそくスクリプトを書いてみましょう。テキストファイルの読み込みと保存を行なっています。

サンプルスクリプト sample1.as

    #include "llmod.as"

    #define MAX_BUF_SIZE 32768  ; テキストの最大サイズは32Kバイト

    objsize winx/2, 24
    pos 0,0       : button "開く", *lb_open
    pos winx/2, 0 : button "保存", *lb_save

    sdim filename, 260
    sdim textbuf, MAX_BUF_SIZE

    pos 0, 24 : mesbox textbuf, winx, winy-24, 1
    stop

*lb_open
    dialog "txt", 16
    if stat == 0 : stop
    filename = refstr

    ; ファイルをオープンする
    getptr pm.0, filename       ; ファイル名文字列のアドレス
    pm.1 = $80000000            ; GENERIC_READ (読み取りアクセス)
    pm.2 = 0                    ; 排他モード
    pm.3 = 0                    ; NULL (デフォルトセキュリティ属性)
    pm.4 = 3                    ; OPEN_EXISTING
    pm.5 = 0                    ; 既存ファイルでは属性指定は無意味
    pm.6 = 0                    ; NULL
    dllproc "CreateFileA", pm, 7, D_KERNEL
    hFile = stat                ; ファイルハンドル
    if hFile == -1 {            ; 戻り値 INVALID_HANDLE_VALUE のとき
        dialog "ファイルのオープンに失敗しました。", 1
        stop
    }

    ; ファイルサイズを取得する
    pm.0 = hFile                ; ファイルハンドル
    getptr pm.1, sizehigh       ; サイズの上位32ビットを入れる変数のアドレス
    dllproc "GetFileSize", pm, 2, D_KERNEL
    sizelow = stat              ; ファイルサイズ下位32ビット
    ; 戻り値は符号なし整数なので、負数の場合でも実際は大きいサイズを表している
    if (sizehigh != 0) | (sizelow >= MAX_BUF_SIZE) | (sizelow < 0) {
        dialog "ファイルサイズが32KBを超えています。\n始めの32KBを読み込みます。"
    }

    ; ファイルからテキストデータを読み込む
    pm.0 = hFile                ; ファイルハンドル
    getptr pm.1, textbuf        ; データバッファのアドレス
    pm.2 = MAX_BUF_SIZE - 1     ; 読み取りサイズ(終端ヌル文字を考慮して -1)
    getptr pm.3, readsize       ; 実際に読み取られたサイズを入れる変数のアドレス
    pm.4 = 0                    ; NULL
    dllproc "ReadFile", pm, 5, D_KERNEL
    if stat == 0 {
        dialog "読み取りに失敗しました", 1
    } else {
        ; 終端ヌル文字を付加
        poke textbuf, readsize, 0
        ; エディットボックスに表示
        objprm 2, textbuf
    }

    ; ファイルハンドルのクローズ
    dllproc "CloseHandle", hFile, 1, D_KERNEL
    hFile = 0
    stop

*lb_save
    dialog "txt", 17
    if stat == 0 : stop
    filename = refstr

    ; ファイルをオープン(作成)する
    getptr pm.0, filename       ; ファイル名文字列のアドレス
    pm.1 = $40000000            ; GENERIC_WRITE (書き込みアクセス)
    pm.2 = 0                    ; 排他モード
    pm.3 = 0                    ; NULL (デフォルトセキュリティ属性)
    pm.4 = 2                    ; CREATE_ALWAYS
    pm.5 = $80                  ; FILE_ATTRIBUTE_NORMAL
    pm.6 = 0                    ; NULL
    dllproc "CreateFileA", pm, 7, D_KERNEL
    hFile = stat                ; ファイルハンドル
    if hFile == -1 {            ; 戻り値 INVALID_HANDLE_VALUE のとき
        dialog "ファイルのオープンに失敗しました。", 1
        stop
    }

    ; ファイルにテキストデータを書き込む
    pm.0 = hFile                ; ファイルハンドル
    getptr pm.1, textbuf        ; データバッファのアドレス
    strlen pm.2, textbuf        ; 書き込みサイズ
    getptr pm.3, writtensize    ; 実際に書き込まれたサイズを入れる変数のアドレス
    pm.4 = 0                    ; NULL
    dllproc "WriteFile", pm, 5, D_KERNEL
    if stat == 0 {
        dialog "書き込みに失敗しました", 1
    }

    ; ファイルハンドルのクローズ
    dllproc "CloseHandle", hFile, 1, D_KERNEL
    hFile = 0
    stop