Using exception handling, Memory mapped files

Using exception handling

By Per B. Larsen

Structured exception handling (SEH) is one of the coolest features in modern Windows software development environments. It is, however, a significant paradigm shift that often causes a lot of fear, uncertainty and doubt - especially among programmers coming from other environments, like DOS. Here, we present a short primer and a few guidelines to help you make the most of what structured exception handling has to offer.

The concept

The idea behind SEH is that you should write your code as if it would never fail.

In traditional environments, you would attempt some "risky" action - like opening a file or allocating memory. You would then inspect a status code that the called function returned. If the call succeeded, you could continue with the next action, otherwise you would return the error condition to the caller. Here's an example of what that might look like:


{$i-} {this turns off exceptions for I/O errors}


function ReadSomeData1(FileName : string; var Buffer : array of char) : Boolean;
var
  F : File;
begin
  Result := False;
  AssignFile(F, FileName);
  Reset(F,1);
  if IoResult = 0 then begin
    BlockRead(F,Buffer,sizeof(Buffer));
    Result := IoResult = 0;
    CloseFile(F);
  end;
end;


      

The call site might look like this:

procedure ReadTest1;
var
  Buffer : array[0..1023] of char;
begin
  if ReadSomeData1('TestFile.Dat',Buffer) then begin
    ...

      

The same functionality coded with the use of exceptions looks like this:

{$i-} {this turns on exceptions for I/O errors}


procedure ReadSomeData2(FileName : string; var Buffer : array of char);
var
  F : File;
begin
  AssignFile(F, FileName);
  Reset(F,1);
  BlockRead(F,Buffer,sizeof(Buffer));
  CloseFile(F);
end;
      

Not only is this version easier both to write and to read it is also faster because there is no longer a need to test every function result. Note that the routine is now a procedure instead of a function - it does not return a status value.

The call site becomes simpler too:

procedure ReadTest2;
var
  Buffer : array[0..1023] of char;
begin
  ReadSomeData2('TestFile.Dat',Buffer);
  ...
      

In this second example, if an error occurs during any of the I/O functions, an exception is raised. What that means is that the low-level function that failed informs the run-time system that it could not complete the request. The run-time system will stop the program in its tracks and transfer control to a special handler located elsewhere in the program. The default exception handler will display an error message to the user - more on this later.

Control never returns to the instruction following the failing call. If Reset fails, control never reaches BlockRead. If BlockRead fails, control never reaches CloseFile, and so on. Indeed, in the case of an error, control doesn't even return to ReadTest2. This means that ReadTest2 can be coded as if the call to ReadSomeData2 will always succeed.

Protecting resources with the try..finally block.

There is one problem with the previous example. What happens if the Reset succeeds, but the BlockRead fails? Control goes to the exception handler (because of the exception raised in BlockRead), but what about the open file? The Reset did not fail, so the file was opened successfully. Since the BlockRead failed, however, CloseFile never got a chance to execute.

For this type of situation, Object Pascal has the try..finally..end construct. With a try..finally..end, the code between finally and end is guaranteed to execute even if the part between try and finally fails. In general, you should use a try..finally..end whenever you acquire some resource, perform an operation and then release the resource again:

..acquire resource
try
..work with resource
finally
  ..free resource
end;
     

Implementing exception handling, then, our modified example looks like this:

procedure ReadSomeData3(FileName : string; var Buffer : array of char);
var
  F : File;
begin
  AssignFile(F, FileName);
  Reset(F,1);
  try
    BlockRead(F,Buffer,sizeof(Buffer));
  finally
    CloseFile(F);
  end;
end;

      


This guarantees that if the
Reset succeeds, the CloseFile will execute even if BlockRead (or anything else within try..finally) fails.
 

Note that it is important to keep the resource acquisition outside the
try block. Some literature, like Jeffrey Richter's Advanced Windows, has code similar to this:  


      
procedure ReadSomeData4(FileName : string; var Buffer : array of char);
var
  F : File;
begin
  AssignFile(F, FileName);
  try
    Reset(F,1); {BAD!}
    BlockRead(F,Buffer,sizeof(Buffer));
  finally
    CloseFile(F);
  end;
end;

      


This will still work properly if no exception is raised and even if the BlockRead fails. However, if the Reset fails, the call to CloseFile in the finally block will execute and the program may crash because the file is not open in that case.
 

If you allocate multiple resources, you can nest
try..finally..end blocks to any depth like this:

..allocate resource 1
try
  ..allocate resource 2
  try
    ..work with resources
  finally
    ..free resource 2
  end;
finally
  ..free resource 1
end;


      


There is a small amount of overhead associated with setting up each
try block. If the resource allocated is memory from the heap, it is often more efficient to initialize the pointers to nil and use only one try..finally..end block, as in this example:
 

o1 := nil;
o2 := nil;
o3 := nil;
try
  o1 := To1Object.Create;
  o2 := To2Object.Create;
  o3 := To3Object.Create;
  ..work with objects
finally
  o1.free;
  o2.free;
  o3.free;
end;

Here, we can get away with allocating the resources within the try..finally block because we initialize each pointer to nil. TObject.Free does nothing if the object passed in is initialized this way.  

Trapping exceptions

Sometimes it is not good enough just to have the default exception handler show an error message when an exception is raised. There are cases where you want to handle the error in code - perhaps to fix the problem that caused the exception to be raised and retry the operation that failed, or to do some cleanup work before letting the exception proceed. For those situations, there is the try..except..end construct.

If we want to change our previous ReadSomeData3 example so that it returns a buffer of zeroes instead of failing if the file does not exist, we can use a try..except..end block, like this:

procedure ReadSomeData5(FileName : string; var Buffer : array of char);
var
  F : File;
begin
  AssignFile(F, FileName);
  try
    Reset(F,1);
    try
      BlockRead(F,Buffer,sizeof(Buffer));
    finally
      CloseFile(F);
    end;
  except
    on E:EInOutError do
      if E.ErrorCode = 2 then
        fillchar(Buffer,sizeof(Buffer),0)
      else
        raise;
  end;
end;
      


Here, the Reset and the read logic are placed within the try..except section of a try..except..end block. If the file exists, Reset does not fail and flow continues with the BlockRead, then CloseFile. The except block is skipped and the procedure returns to the caller.
 

If
Reset does raise an exception, control is passed to the first instruction within the except..end block. Reset can fail for any number of reasons. The on E:EInOutError do construct indicates that we are only interested in handling the EInOutError exception type in this case. Other exception types are passed on as if there was no except..end block. The E: in front of the exception type lets us inspect the data of the exception. This is necessary in this case because the EInOutError exception type is a generic one used for all operating system level I/O errors. We are only interested in handling the "file not found" error, which has error code 2. If the exception was raised for another reason, we re-raise the exception using the raise keyword, so that it can proceed to the next handler.

The other common form of the try..except..end block is the unqualified exception trap. That is, a try..except..end block with no On <ExceptionType> do case. These are mostly used to release acquired resources when an error occurs. Try..finally blocks work well in cases where you allocate resources, use them, and free them again. However, in cases, where you allocate resources in one place, use them for a while, and then release them in a different place, we need a different kind of handling.

Say, in our example, we want to keep the file open so that we can read from it from a separate routine, we might have code like this:

var
  F : File;

procedure ReadSomeData6(var Buffer : array of char);
begin
  BlockRead(F,Buffer,sizeof(Buffer));
end;
      
procedure OpenDataFileAndReadSomeData1(FileName : string; var Buffer : array of char);
begin
  AssignFile(F, FileName);
  Reset(F,1);
  ReadSomeData6(Buffer);
end;

procedure CloseDataFile;
begin
  CloseFile(F);
end;


      

What if the Reset succeeds, but the ReadSomeData6 call fails? The file will be left open. We can't use a finally block to close the file, because that will cause the file to be closed even when the ReadSomData6 call succeeds. Instead we use an unqualified try..except..end block like this:



procedure OpenDataFileAndReadSomeData1(FileName : string; var Buffer : array of char);
begin
  AssignFile(F, FileName);
  Reset(F,1);
  try
    ReadSomeData6(Buffer);
  except
    CloseFile(F);
    raise;
  end;
end;     


This ensures that the file is closed in case of a read error. Note the raise instruction used to re-raise the exception after the error. This is important. If left out, the caller would not know that the read had failed.

In general, you should always re-raise exceptions that you do not completely handle.

Raising custom exceptions

So far, we have dealt with trapping exceptions. Most exceptions are raised in RTL or VCL code, but sometimes we need to raise our own custom exceptions. Say, for instance, that our example code should be used only to open a particular file type, which has a signature word at the beginning, we can use a custom exception to signal an error if the signature is not found:

type
  ESigException = class(Exception);

procedure ReadSomeData7(FileName : string; var Buffer : array of char);
const
  Signature = $1234;
var
  F : File;
  Sig : Word;
begin
  AssignFile(F, FileName);
  Reset(F,1); 
  try
    BlockRead(F,Sig,sizeof(Sig));
    if Sig <> Signature then
      raise ESigException.CreateFmt(
       'Bad signature in file %s',
           [FileName]);
    BlockRead(F,Buffer,sizeof(Buffer));
  finally
    CloseFile(F);
  end;
end;


      

If the correct signature is not present at the beginning of the file, the raise will cause control to jump to CloseFile (because of the try..finally..end block). After that, control continues to the calling scope, if there is a try block there, and then to the next scope, and so on. This continues until either the exception is handled by a try..except (not re-raised) or until the outermost try..except handler is reached, which will then call the global default handler.

Note, that it is not necessary to introduce a custom exception type, as I do in this example, if you are certain that there will never be a need to handle this specific exception in a try..except..end block. In that case, you can use the pre-defined Exception type.

Exceptions should only be raised when a truly exceptional condition occurs. Raising an exception is a relatively costly operation. A good rule of thumb is that if it becomes annoying to have the break-on-exception turned on during development, you are raising too many exceptions.

Custom exception filters - the OnException handler

When an exception is raised in a VCL based application, it will eventually reach the default exception handler, TApplication.HandleException. The standard behavior of HandleException is to show a dialog with the text of the exception. The global Application object has an event handler, OnException, which lets you change the default behavior. This is convenient if you want to change the way an exception is handled at the global level. Say, for instance, that you want to change the default exception error message for a particular exception, you can install a custom exception handler like this:

procedure TForm1.OnException(Sender: TObject; E: Exception);
{custom exception handler}
begin
  if E is EInOutError then
    ShowMessage('I/O error')
  else {call default}
    Application.OnException(Sender, E);
end;
      
procedure TForm1.Button1Click(Sender: TObject);
begin
  {install custom exception handler}
  Application.OnException := OnException;
end;
      

Custom exception handlers are particularly useful for doing special processing on exceptions that cannot be trapped with try..except blocks. This includes exceptions that are raised by low level code (e.g. the BDE) in response to a user activity, like clicking a button.

Hints and tips on writing exception handling logic

When writing try..except..end blocks, only trap errors that you explicitly handle. That is, never “eat” exceptions by using an empty except..end part. Likewise, never assume that only one particular set of exceptions can occur. For example, it would be tempting to write the ReadSomeData5 function like this: 

procedure ReadSomeData8(FileName : string; var Buffer : array of char);
var
  F : File;
begin
  AssignFile(F, FileName);
  try
    Reset(F,1);
    try
      BlockRead(F,Buffer,sizeof(Buffer));
    finally
      CloseFile(F);
    end;
  except {BAD!}
    fillchar(Buffer,sizeof(Buffer),0)
  end;
end;
     

This example will work like the ReadSomeData5 example in the case where an exception is being raised because the file could not be opened, but will “eat” much more serious conditions (like “Disk Read error”). These unexpected exceptions should be allowed to pass through the try..except block to one that specifically deals with them – or eventually to the default handler which will inform the user of the problem.

Don’t trap exceptions only to show a message, or to change the existing exception text. If the exception you wish to change is a system or VCL message, change the appropriate resource instead – or use a customized global exception handler as described in the previous section. If the exception is one of your own custom exceptions, change the string used to raise the exception to a more appropriate one. As a rule, when you raise custom exceptions, make sure your error message describes the problem adequately, since it will likely be displayed to the end-user.

Usually, you should not change the logical meaning of an error. There was an example posted on a Borland newsgroup recently where the programmer had code similar to this:

function Average(Sum,Count : double) : double;
begin
  try
    Result := Sum / Count;
  except 
    on EZeroDivide do 
      Result := 0;  
  end;  
end; 
      
      

This is a bad idea. Dividing by zero does not yield a zero result. This example might have made the reporting code where it was used more robust, but that does not make it correct. It would have been better to let the exception proceed to the next level where it could have been caught and turned into a string with the value “<overflow>” or “n/a”.

Raise exceptions when you want to communicate a truly exceptional condition to the caller – not to signal some common condition. Raising exceptions is relatively costly: First, an exception object needs to be created, then the operating system needs to be notified. The operating system notifies the run-time system of your application, which in turn looks for exception handlers (try..except and try..finally blocks) to handle the exception. Finally, when the exception is being handled, either by one of your custom try..except blocks, or by the outmost default try..except block (the one that calls ShowException), the exception object is disposed of. All of this takes time…

Another reason, besides performance considerations, not to raise exceptions too frequently is that it can become extremely annoying for users of your code (yourself included) to have to deal with all the breaks when debugging in the IDE.

Conclusion

Once you get used to the shift in paradigm from traditional test based error handling, structured exception handling is a great way to make your code more robust, simpler to maintain and more efficient.

 

-o-

 

Memory Mapped Files

by Per B. Larsen

The term memory mapped file refers to the operating systems ability to treat any file supplied by the user as an extension of virtual memory. The Windows operating system's support for memory mapped files appear to be somewhat under-appreciated by Delphi programmers - probably because the VCL offers no object oriented encapsulation of this functionality.

In this article, we will create some simple classes for managing file and memory mappings, and look at some of the things you can do with memory mapped files. For completeness, we will also discuss some of the issues involved when deciding whether or not to use memory mapped files in a given situation.

 

Virtual Memory

You are probably familiar with the basic principle behind virtual memory, but here is a quick refresher.

Windows lets you allocate large areas of memory (up to 2GB per process) even though you have much less real memory installed in your computer. This is possible because Windows does not actually reserve physical memory to match your request, but only the address interval where your memory should be.

Windows and the CPU together manage  memory in pages. On all Intel platforms, the page size is 4Kb. When memory is first allocated, Windows reserves the corresponding address space in its page table and marks all pages in the new address interval as 'not present'. The first time you actually try to access a certain section of this memory, the CPU will generate a 'page fault' exception. This exception is then trapped by Windows, which allocates a physical memory page, updates the page table with the new 'present' status, and restarts the instruction that caused the exception.

Of course, there might not be any free memory pages available that Windows can use to satisfy the request. In that case, Windows will select one of the memory pages that are already in use - usually one that hasn't been accessed for a while. Windows writes its contents to the swap file, and changes the page table to reflect the fact that the content of the old page is now in the swap file.

Later, if the address for  the data now in the swap file is referenced, Windows will know to restore the page's contents as part of the 'page fault' handling.

Mapping Files

For 32-bit applications, Windows extends its virtual memory system to allow mapping of files other than the swap file. This is useful because a lot of the things that are read into memory during program execution are already on disk.

To see why, consider an example where your application reads a large table from a disk file into a memory buffer. Then, when you switch to a different application, the memory used for the table is needed for other things and the table data is written to the swap file. Later, when you return to the first application, the table data is reloaded from the swap file.

Mapping the file with the table into memory saves the step of writing it to the swap file - it can be reloaded just as well from the original file.

In fact, Windows itself uses this mechanism to dynamically load pages from executable files into memory - and for many other internal tasks as well.

 

The Basics

The following is a bare-bones Delphi example of how to map a file into memory:

var
  FileHandle ,
  MappingHandle : THandle;
  FileSize : DWord;
  Data : Pointer;

procedure MapFile;
begin
  FileHandle := CreateFile(
      'sample.dat',
      GENERIC_READ,
      0, {exclusive}
      nil, {security}
      OPEN_EXISTING,
      FILE_ATTRIBUTE_NORMAL,
      0);

  FileSize := GetFileSize(FileHandle,nil);
  MappingHandle := CreateFileMapping(
          FileHandle,
          nil, {security}
          PAGE_READONLY,
          0, {High Dword of size}
          FileSize,
          NIL);

  Data := MapViewOfFile(
              MappingHandle,
              FILE_MAP_READ,
              0, {start of map}
              0, {low dword}
              FileSize); {Bytes to map}
end;
      

We open the file with the CreateFile API by passing the name of the file to map plus a set of arguments whose values are nearly always what is shown here. Check your on-line help for further documentation of the options. There are far too many to cover here.

The handle returned by CreateFile is passed to CreateFileMapping along with the size of the file and some default attributes. The last argument is an optional name of the file mapping object. This can be used to share mappings among processes. We'll get back to that later.

The  handle returned by CreateFileMapping is passed to the MapViewOfFile API.

At the end of this code, we have a pointer called Data that we can use to access the data in our 'sample.dat' file - just as if the data had been read into a memory buffer.

When we are done, we need to free the resources associated with the mapped file, like this:

procedure UnmapFile;
 begin
   UnmapViewOfFile(Data);
   CloseHandle(MappingHandle);
   CloseHandle(FileHandle);
 end;

The code in this paragraph illustrates the basic API calls needed for mapping files, but it lacks any kind of error handling and is not very flexible. It turns out that the process of mapping files is so similar between different applications that it makes sense to encapsulate the functionality in a simple class, and that is what we will do in the following section.
      

The TFileMapping class

A simple class encapsulation of the API for mapping files into memory could look like this:

unit FileMap;
interface
uses
  Windows,
  SysUtils;
type
  TFileMapping = class
  private
    FileHandle    : THandle;
    MappingHandle : THandle;
    FSize         : DWord;
    FData         : Pointer;
  public
    constructor Create(FileName : string);
    destructor Destroy; override;
    property Data : Pointer read FData;
    property Size : DWord read FSize;
  end;

      

We pass the name of the file we wish to map in Create, we access the data of the file via the Data property, the Size property tells us the size of the mapping, and Destroy closes the file mapping and releases all resources.

The code is fairly simple:

implementation
{ TFileMapping }
   
constructor TFileMapping.Create(FileName: string);
begin
  FileHandle := CreateFile(
    PChar(FileName),
    GENERIC_READ,
    0, {exclusive}
    nil, {security}
    OPEN_EXISTING,
    FILE_ATTRIBUTE_NORMAL,
    0);
  if FileHandle = INVALID_HANDLE_VALUE then
    raise Exception.Create('Unable to open file');
  FSize := GetFileSize(FileHandle,nil);
  MappingHandle := CreateFileMapping(
    FileHandle,
    nil, {security}
    PAGE_READONLY,
    0, {High Dword of size}
    FSize,
    nil);
  if MappingHandle = 0 then
    raise Exception.Create('Unable to create file mapping');
  FData := MapViewOfFile(
    MappingHandle,
    FILE_MAP_READ,
    0, {start of map}
    0, {low dword}
    FSize); {Bytes to map}
  if FData = nil then
    raise Exception.Create('Unable to map view of file');
end;
       

destructor TFileMapping.Destroy;
begin
  if FData <> nil then
    UnmapViewOfFile(FData);
  if MappingHandle <> 0 then
    CloseHandle(MappingHandle);
  if FileHandle <> 0 then
    CloseHandle(FileHandle);
  inherited Destroy;
end;  
end.

 
Putting it to use

Let's use the unit with the file mapping logic presented in the previous section to build a simplistic hex file viewer.

1.       Create a new project with a blank form.

2.       Drop an OpenDialog on the form.

3.       Drop a panel on the form and align it to the top.

4.       Drop a button on the panel.

5.       Drop a label on the main form, set its AutoSize property to False, and align it to the client.

6.       Set the label's font property to Courier New or another fixed-pitch font.

7.       Double-click on the button and add the following code to the new event handler:


procedure TForm1.Button1Click(Sender: TObject);
var
  FileMapping : TFileMapping;
begin
  if OpenDialog1.Execute then begin
    FileMapping := TFileMapping.Create(OpenDialog1.FileName);
    try
      ShowPage(FileMapping);
    finally
      FileMapping.Free;
    end;
  end;
end;      

8.       Add FileMap, the name of our mapping unit, to the uses list.

9.       Add the following code to the unit:

 

procedure TForm1.ShowPage(FileMapping : TFileMapping);
var
  LineOffset,i,j : DWord;
  C : PChar;
  S : string;
  Ch : Char;
  LinesPerPage,CharsPerLine : DWord;
begin
  with Label1 do begin
    LinesPerPage := Height div Canvas.TextHeight('Wy') - 1;
    CharsPerLine := ((Width div Canvas.TextWidth('W')) - 3) div 3;
  end;
  S := '';
  {for each "line"}
  for i := 0 to pred(LinesPerPage) do begin
    LineOffset := i * CharsPerLine;
    C := PChar(DWord(FileMapping.Data) + LineOffset);
    {build line of hex values}
    for j := 0 to pred(CharsPerLine) do
      if LineOffset + j < FileMapping.Size then
        S := S + IntToHex(byte((C + j)^), 2)
      else
        S := S + '  ';
    S := S + '  ';
    {build line of printable characters}
    for j := 0 to pred(CharsPerLine) do begin
       if LineOffset + j < FileMapping.Size then
         Ch := char((C + j)^)
       else
         Ch := ' ';
      if Ch >= ' ' then
        S := S + Ch
      else
        S := S + '.';
    end;
    S := S + #13; {new line}
  end;
  Label1.Caption := S;
end;
      

10.    Copy the declaration for the ShowPage method header to the public section of the form.

11.    Compile and Run.

Note how the ShowPage method lacks any kind of file I/O code. It simply uses the Data property of the file mapping as a pointer directly to the data in the file, and lets the operating system handle the rest.

This example program only shows as much data from the file as will fit on the form, starting from the first byte of the file. Extending the programs capabilities to support multiple pages is pretty much a simple matter of adding support for managing a page offsets, but that is left as an exercise to the reader.

Using File Mappings To Share Data Between Processes

In addition to being a convenient mechanism for randomly accessing data in a disk file, memory mapped files have another very important use, namely sharing data between applications.

Previously, we touched upon the last parameter to the CreateFileMapping API function, which is lpName. In our code, so far, we've passed nil for the lpName argument, but passing a program defined string value instead lets other applications open a file mapping on the same section of memory as long as they know the name. This is in fact the Microsoft recommended method for sharing data between multiple processes. Indeed, several systems in Windows that must work across process boundaries, like SendMessage, DDE, and OLE use memory mapped files internally.

So far, in our examples, we've been mapping a physical file on disk into memory. However, for applications that share a buffer between processes, it usually doesn't make sense to associate the buffer with a particular disk file. Fortunately,  the CreateFileMapping function lets us pass the constant $FFFFFFFF instead of a valid file handle, which will cause the mapping to be associated with a section of the swap file. We must also supply a size argument to let Windows know how big we want the buffer to be.

Here is the straight API code for creating a named memory buffer:

var
  Data : Pointer;
  FileMappingHandle : THandle;

procedure CreateSharedMap(ShareName : string; BufferSize : DWord);
begin
  FileMappingHandle := CreateFileMapping($FFFFFFFF, nil, PAGE_READWRITE, 0,
    BufferSize, PChar(ShareName));
  Data := MapViewOfFile(FileMappingHandle, FILE_MAP_ALL_ACCESS, 0, 0, 0);
end;

The arguments to CreateSharedMap are ShareName, which is the name of the mapping object, and BufferSize, which determines the size of the buffer to be created.

When we are done using the buffer, we must release its associated resources:

procedure CloseSharedMap;
begin
  UnmapViewOfFile(Data);
  CloseHandle(FileMappingHandle);
end;

      

Once a process has created the named buffer, any other process can create its own pointer to the same memory buffer using the following code:

procedure OpenSharedMap(ShareName : string);
begin
  FileMappingHandle := OpenFileMapping(FILE_MAP_ALL_ACCESS, False, PChar(ShareName));
  Data := MapViewOfFile(FileMappingHandle, FILE_MAP_ALL_ACCESS, 0, 0, 0);
end;       
      
      

Like the process that creates the file mapping, other processes that open their own view to an existing file mapping must release their resources by calling CloseSharedMap. The shared buffer remains allocated until the last process closes its mapping and handle - even if the process that first creates the buffer is closed first.

We can encapsulate the buffer sharing logic in a simple Delphi class:


unit ShareMap;
interface
uses
  Windows,
  SysUtils;
type
  TShareMapping = class
  private
    MappingHandle : THandle;
    FSize         : DWord;
    FData         : Pointer;
  public
    constructor Create(ShareName : string; BufferSize : DWord);
    destructor Destroy; override;
    property Data : Pointer read FData;
    property Size : DWord read FSize;
  end;


      

This class uses the same interface for the process that creates the buffer and other processes. Create will attempt to open an existing file mapping with the name passed in ShareName. If the attempt fails, a new file mapping is created.

implementation

      
{ TShareMapping }
constructor TShareMapping.Create(ShareName: string; BufferSize: DWord);
begin
  MappingHandle := OpenFileMapping(FILE_MAP_ALL_ACCESS, False, PChar(ShareName));
  if MappingHandle = INVALID_HANDLE_VALUE then begin
    MappingHandle := CreateFileMapping($FFFFFFFF, nil, PAGE_READWRITE, 0,
      BufferSize, PChar(ShareName));
    if MappingHandle = 0 then
      raise Exception.Create('Unable to create file mapping');
  end;
  FData := MapViewOfFile(MappingHandle, FILE_MAP_ALL_ACCESS, 0, 0, 0);
  if FData = nil then
    raise Exception.Create('Unable to map view of buffer');
  FSize := BufferSize;
end;
       


destructor TShareMapping.Destroy;
begin
  if FData <> nil then
    UnmapViewOfFile(FData);
  if MappingHandle <> 0 then
    CloseHandle(MappingHandle);
end;
end.
      

Simplifying Data Access

The two classes presented so far provide access to the data in the file mapping by way of a public Data property, which uses the generic Pointer type. This means that whenever you need to access data in the buffer, the Data property must be cast to a typed pointer  appropriate for the actual data you are working with.

If your file mapping is an array of a specific type, you can simplify the use of the mapping object by creating a custom descendant which provides a typed, indexed property of the actual data type you are using. Say we are working with a file of records where each record has the following layout:


type       
  TMyRecord = packed record 
    Name : string[80]; 
    Address : string[80]; 
    City : string[50]; 
    State : string[2]; 
    Zip : string[8]; 
    Country : string[30]; 
  end;      

We can then create a custom descendent of TFileMapping which looks like this:


type
  PMyRecord = ^TMyRecord;
  TMyRecordMapping = class(TFileMapping)
  private
    function GetMyRecord(Index: Integer): PMyRecord;
    function GetRecordCount: DWord;
  public
    property RecordCount : DWord read GetRecordCount;
    property MyRecord[Index : Integer] : PMyRecord read GetMyRecord;
  end;
      

RecordCount returns the number of records in the file, MyRecord returns a pointer to a particular record in the file.

This is what the two get methods look like:


{ TMyRecordMapping }
function TMyRecordMapping.GetMyRecord(Index: Integer): PMyRecord;
begin
  Result := Data;
  inc(Result, Index);
end;
      
function TMyRecordMapping.GetRecordCount: DWord;
begin
  Result := Size div sizeof(TMyRecord);
end;
      
      

This custom file mapping class lets us write nice, clean code like the following example, which reads all records in the file and prints out the name field for each one: 

procedure TForm1.Button1Click(Sender: TObject);
var
  MyRecordMapping : TMyRecordMapping;
  i : Integer;
begin
  MyRecordMapping := TMyRecordMapping.Create('MyRecords.dat');
  try
    for i := 0 to pred(MyRecordMapping.RecordCount) do
      writeln(MyRecordMapping.MyRecord[i].Name);
  finally
    MyRecordMapping.Free;
  end;
end; 
    

Performance Issues

Mapping files into memory can greatly simplify the design of applications that deal with files. The technique is especially handy when you need to access the contents of a file in a fairly random fashion. However, because memory mapped files are implemented as an extension of the operating system's page fault handling, there are situations where you should not use this technique.

Consider the case where you want to sort a file of records. An obvious solution using memory mapped files would be to map the file into memory and use QuickSort. This solution is likely to be very slow, however. The reason is that the QuickSort algorithm tries to minimize the number of exchanges, not the number of times each record is accessed. Each page of the file will be accessed many times during the sort process, and unless the entire file fits into free memory, all the pages of the file will wind up being read several times from disk. There are many sorting algorithms available that perform much better for this kind of thing.

In general, memory mapped files work well for applications where each section of the file is read only a few times - or where only a minor portion of the file needs to be read into memory.

Things To Be Aware Of When Sharing Buffers

When you share memory mapped files or buffers between applications, you should realize that though different processes can open a handle to and create a pointer to a shared buffer, the address of the buffer will typically be different in each process. In other words, the pointer is only valid in the process that creates it. What this means is that you can't store pointers that are offsets into the buffer in one process and use those pointers in other processes.

Note: Windows 95/98 peculiarity

It turns out that this does in fact work under Windows 95 and Windows 98. For performance reasons, these operating systems map memory files at the same address in all processes, but that is undocumented behavior that can change at any  time in the future, according to Microsoft.

Anther thing you should be aware of is that you can't store huge strings or objects in a shared buffer. The reason is that both huge string and object variables are really pointers to memory allocated on the heap. Normally,  the Delphi run-time library handles this transparently, but if you store a pointer variable in a shared buffer and try to access it from another process, the address of the object will be either invalid or point to a completely different area of memory.

Conclusion

Memory mappings can greatly simplify many kinds of applications that deal with files, and it is the method of choice for sharing data buffers among applications in Windows. This article does not discuss every aspect of their use, but the code presented does cover the basics and should provide a good starting point for expansion. Hopefully, it will inspire to more widespread use of this technique among Delphi programmers.

 

-o-

These articles first appeared in the now defunct Delphi Developer's Journal.

Graphics design copyright © 2002-2005 Emely Bolette Lys.

Other content copyright © 1998, 2005 Per B. Larsen