2011年2月21日

TFileStreamのコンストラクタで指定するアクセスモードについて

Delphiでファイルを扱うときによく使うTFileStream (ja)のコンストラクタCreate (ja)の第2パラメータModeと、WindowsのAPIであるCreateFile (ja)の第2パラメータdwDesiredAccess、第3パラメータdwShareMode、第5パラメータdwCreationDispositionの関係を調べてみました。

まずModeに指定できる値としてfm...の定義値を調べてみます。fmCreateのみがClasses.pasに
fmCreate = $FFFF;
(Classes.pasの53行目)(Delphi 2007の場合、以下同じ)

と定義され、その他の値はSysUtils.pasに
fmOpenRead       = $0000;
  fmOpenWrite      = $0001;
  fmOpenReadWrite  = $0002;

  fmShareCompat    = $0000 platform; // DOS compatibility mode is not portable
  fmShareExclusive = $0010;
  fmShareDenyWrite = $0020;
  fmShareDenyRead  = $0030 platform; // write-only not supported on all platforms
  fmShareDenyNone  = $0040;
(SysUtils.pasの44行目付近)

と定義されています。…fmCreateが0xFFFFで他の定義値が16bitの定数ということは、ModeにfmCreateを指定した場合、その他の指定は一切意味を持たない、ということになりますね。実際にTFileStream.Createを見てみると、
constructor TFileStream.Create(const AFileName: string; Mode: Word; Rights: Cardinal);
  begin
  if Mode = fmCreate then
  begin
  ...
  end
  else
  begin
  ...
  end;
  FFileName := AFileName;
end;
(Classes.pasの5474行目付近)

と、確かにModeがfmCreateかそれ以外かで完全に処理が分けられてしまっています。ではfmCreateのケースをさらに追ってみます。
inherited Create(FileCreate(AFileName, Rights));
(Classes.pasの5478行目)

とSysUtils.pas上のFileCreate (ja)を呼び出しています(inherited CreateはTFileStreamの派生元であるTHandleStream (ja)のコンストラクタ呼び出しです)。FileCreateは
Result := Integer(CreateFile(PChar(FileName), GENERIC_READ or GENERIC_WRITE,
    0, nil, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0));
(SysUtils.pasの5259行目付近)

という実装であり、結果としてfmCreateの場合、dwDesiredAccessはGENERIC_READ or GENERIC_WRITE、dwShareModeは0(Prevents other processes from opening a file or device if they request delete, read, or write access.=他の処理からは削除、読み込み、書き込みアクセスできない)、dwCreationDispositionはCREATE_ALWAYS(新しいファイルを作成します。指定したファイルが既に存在している場合、そのファイルを上書きし、既存の属性を消去します。)と、これらの値が固定的に指定される、ということになります。

一方fmOpen...の場合、
inherited Create(FileOpen(AFileName, Mode));
(Classes.pasの5484行目)

とSysUtils.pas上のFileOpen (ja)を呼び出しています。FileOpenは
const
  AccessMode: array[0..2] of LongWord = (
    GENERIC_READ,
    GENERIC_WRITE,
    GENERIC_READ or GENERIC_WRITE);
  ShareMode: array[0..4] of LongWord = (
    0,
    0,
    FILE_SHARE_READ,
    FILE_SHARE_WRITE,
   FILE_SHARE_READ or FILE_SHARE_WRITE);
  ...
  Result := Integer(CreateFile(PChar(FileName), AccessMode[Mode and 3],
    ShareMode[(Mode and $F0) shr 4], nil, OPEN_EXISTING,
    FILE_ATTRIBUTE_NORMAL, 0));
(SysUtils.pasの5196行目付近)

という実装で、dwDesiredAccessはfmOpenRead→GENERIC_READ、fmOpenWrite→GENERIC_WRITE、fmOpenReadWrite→GENERIC_READ or GENERIC_WRITEとマップされ、dwShareModeはfmShareCompat/fmShareExclusive→0(他の処理からは削除、読み込み、書き込みアクセスできない)、fmShareDenyWrite→FILE_SHARE_READ(他の処理からは読み込みアクセスのみ許可)、fmShareDenyRead→FILE_SHARE_WRITE(他の処理からは書き込みアクセスのみ許可)、fmShareDenyNone→FILE_SHARE_READ or FILE_SHARE_WRITE(他の処理からは読み込み、書き込みアクセスとも許可)とマップされ、dwCreationDispositionはOPEN_EXISTING(ファイルを開きます。指定したファイルが存在していない場合、この関数は失敗します。)が固定的に指定される、ということになります。

これ以外の組み合わせ、たとえば書き込みのために新規にファイルを作成(存在していたら上書き)し、そのファイルに読み込みアクセスのみ許可する、というような場合(dwDesiredAccess = GENERIC_READ or GENERIC_WRITE、dwShareMode = FILE_SHARE_READ、dwCreationDisposition = CREATE_ALWAYS)はTFileStreamを使用するのではなく、直接Windows APIのCreateFileをこれらのパラメータで呼び出し、取得したファイルハンドルでTHandleStreamを作成する、という処理が必要になります。例えばこんな感じでしょうか。
procedure HandleStreamSample(const AFilename: String);
var
  FileHandle: Integer;
  Stream: THandleStream;
begin

  FileHandle := CreateFile(PChar(AFilename),GENERIC_READ or GENERIC_WRITE,
                           FILE_SHARE_READ,nil,CREATE_ALWAYS,
                           FILE_ATTRIBUTE_NORMAL,0);
  if FileHandle < 0 then
  begin
    raise EFCreateError.CreateResFmt(@SFCreateErrorEx,[ExpandFileName(AFileName),
                                     SysErrorMessage(GetLastError)]);
  end;

  Stream := THandleStream.Create(FileHandle);
  try
    { Streamに対する処理 }

  finally
    Stream.Free;
  end;

end;

あるいはCreateFileの他のパラメータ、例えば第6パラメータにFILE_FLAG_...を指定したいような場合などもこのようなコーディングが必要になります。

0 件のコメント: