Al's consulting firm provides a wide array of development, education, and documentation services. He works regularly in C++, Delphi, and Visual Basic. You can find Al at http://ourworld.compuserve.com/ homepages/Al_Williams.
If there is one thing I've learned about answering questions, it is this: Don't answer them right away. Instead, find out what the person asking really wants to know. Often what someone asks and what they want to know are two different things.
Take memory, for instance. End users often confuse RAM and disk memory. One of my neighbors stopped me a few days after Windows 95 hit the stores. He asked me, "How can I get more memory to run Windows 95?" I launched into a discourse on 36-pin SIMMs versus 72-pin SIMMs, SIMM expanders, and related topics. His face fell. "I thought I could just delete some files," he said. As it turned out, he only had a few megabytes of hard-disk space left.
Even programmers might have trouble keeping memory and disk storage straight when it comes to Win32 shared memory. To share memory with Win32, you have to use mapped filesa special type of file that may or may not relate to a file on the disk.
Most of the hype surrounding Delphi centers on its rapid development of database programs. While this is certainly important, I think it is equally important that Delphi gives you full access to the Windows APIsomething not every visual language can say. If you can write it in C, you can write it in Delphi. There might be a few spots where it would be easier to write it in C, but you can bet there are many places where Delphi is easier, too.
In this column, I'll develop several cooperating Delphi programs that utilize a shared-memory component. This shared- memory component is simple to use and utilizes Win32 synchronization to keep the memory consistent. Using the component is simpleyou don't need to know anything about Win32 unless you want to explore deeper or make modifications to the component.
I originally wanted shared memory to support a simulation project I was writing. Each element of the simulation was a separate program. Under Windows 3.1 it was easy to cheat and pass around pointers to memory on the sly. Since all programs shared the same address space, you could get away with this. With Win32, this isn't the case. Each program runs in its own address spaceWin32 expressly forbids sharing memory.
Of course, Windows can't completely prohibit shared memory it is simply too handy. You can specifically request a shared-memory block and give it a name. All programs that use the same named block will share memory. If you look up shared memory in the Win32 documentation, you may be surprised to find that it falls into the category of a mapped file.
In the general case, Windows provides mapped files so that programs can treat a file as an array of bytes in memory. Windows swaps portions of the file in and out as you request access to different regions of it. If two programs request access to the same file-mapping object, they share the file, and Windows makes certain that each program sees the file in the same way.
This is handy in its own right, but does that mean that shared memory must use a file? No. When you create a file mapping, you pass it a file handle. If you pass THandle(-1), it will not use any file at all. This is how you create "pure" shared memory.
Of course, you might want to use a file for shared memory. Using a file makes your data persistent so that it will remain the same even after a reboot or if no programs are using it. By using a mapped file, you can have the convenience (and efficiency) of shared memory combined with the persistence of a file.
Here are the steps necessary to create a mapped file or shared-memory region:
1. If you are using an actual file, open it using CreateFile. You should open the file for exclusive access. You can request GENERIC_READ access, GENERIC_WRITE access, or both. If you can't open the file, it is probably because another program already opened it for exclusive access and already created the file-mapping object. In that case, go to step 3. If you successfully open the file, go to step 2.
When you no longer need the mapped file, you should take these steps:
1. Call UnMapViewOfFile.
2. Call CloseHandle to close the file-mapping handle (returned from OpenFileMapping or CreateFileMapping).
3. If you opened the file, close it by calling CloseHandle.
There is one more important consideration when dealing with shared resourcessynchronization. Imagine a scenario where program A writes a string into shared memory. From there, program B will write it to an error log file. Before program A finishes, program C begins writing into the same string. This will undoubtedly result in an entertaining error message (but not a useful one). As another example, consider a counter in shared memory. One program may retrieve this number, increment it, and replace it. What if another program retrieves the number at about the same time as the first program? The counter should increase by two, but will only increase by one.
You need a method to synchronize access to the memory. Win32 provides several types of synchronization primitives. In this case, the mutex (short for mutual exclusion) will fit the bill.
The key to synchronizing with a mutex is its state. Many Win32 objects may be in one of two states, signaled or unsignaled. This is true of a mutex. If you call WaitForSingleObject, Windows will wait for the object to be in the signaled state. When it is, the call will return. You can ask Windows to wait, or you can specify a timeout. If the timeout expires and the object remains unsignaled, the call returns an error.
When a mutex is signaled, any thread that calls WaitForSingleObject on it will succeed, and the calling thread will own the mutex. Then the mutex is unsignaled. Any other thread that tries to wait will either time out or block until you specifically call ReleaseMutex. There is one exception to this rule. The thread that owns the mutex will always succeed when calling WaitForSingleObject. In other words, you can wait on the same mutex more than once. No matter how many times you wait, you'll relinquish control the first time you call ReleaseMutex.
Why can't you regulate access with a shared-memory byte? The problem is the same one that affects the error-message string in the aforementioned example: One program tests the byte and, if it is clear, sets it. What if more than one program does this at the same time? There is a small chance that two programs will test the byte, notice it is clear, and then set it. Windows ensures that if multiple threads wait on a mutex, only one will own it. It can guarantee this because it can control the scheduling of threads.
Table 1 shows the interface I wanted for a Delphi shared-memory component. The design is simple. The name of the component becomes the name of the shared-memory block. The shared memory always stores strings. You specify the number of strings and their length. You can also specify a file name, which the component will create if it doesn't exist already. If it does create it, you can set the DeleteFlag property to force Windows to delete the file after it closes. Figure 1 is the component's design-time interface, while Listing One presents the code (SHDMEM.PAS).
At run time, you have several properties and methods you can use. The most basic are Sto and Rcl. As you might guess, Sto puts a string into a particular shared-memory slot (the first slot is zero), and Rcl retrieves it. These routines automatically lock a mutex to be certain that the entire operation occurs undisturbed. Without the mutex, one program might be reading a string at the same time another program is changing its value. Since a mutex and a file mapping can't have the same name, the component adds an X to the name to create the mutex name.
Suppose you plan to get a value from shared memory, change it, and put it
back. You can call the lock method to prevent other programs from using the shared-memory block. Be sure to call unlock as soon as possible, of course.
The other properties allow you to learn the state of the shared-memory block. If FirstUser is True, then the component created the block for you to use; otherwise, the component opened a block already in use. If FirstUser is True, you might want to call Clear to zero out the entire block. If you are using a file, you can examine the NewFile property. This property is True if the component created the file for you to use. If you are not using a file name, this property is the same as FirstUser.
Count, Size, and Name should not change at run time. You should only modify these properties at design time. If you wanted to make the component more robust, you could raise an exception when these properties change at run time. Alternately, you might rewrite the component so you could change these properties at run time.
Another design-time issue is creating the shared memory and mutex objects. The component creates these during the standard Loaded method. However, this method runs during design time, too. Thus, while debugging your program, you'd never find FirstUser or NewFile equal to True. To prevent this, the component checks to see if it is in the designer and doesn't initialize if it is. The Loaded method sets up the file mapping and creates the mutex only at run time.
There are a few things you should consider when using Win32 shared memory. First, there is no assurance that different processes will map a file to the same address. You can request a particular address by calling MapViewOfFileEx(), but that won't work if the address isn't available. The safest thing to do is to avoid storing pointers to shared memory in shared memory. Instead, consider storing an offset from the beginning of the shared-memory block and calculating the address as you need it.
Shared memory only works properly on a single computer. This thwarts any attempts to share memory using a network file between different machines. If you map a network file between multiple machines, you can't predict what will happen. This is true even if you try to flush the file mapping after each access.
The routines in the Delphi component don't use a time-out value for calls to WaitForSingleObject. If any program locks the mutex and fails to release it, all other programs will wait for the mutex indefinitely. Of course, if the offending program dies, the system automatically releases the mutex. If you are worried about errant programs locking things up, you might consider changing this. You could use a property to store a default time-out value, if you like.
When the shared-memory object starts, there are several things that may go wrong. The program may not have access to the file, for example. When any error occurs, the component sets the Valid property to False. You should check this property before using the shared memory.
Listing Two is a program that you might use to register guests at a meeting. This program uses shared memory to retrieve and update a key number. Other programs (available electronically) also share this key number. This makes certain that the key is unique. In addition, a monitor program tracks the total number of transactions.
Although these programs are simple, they serve as a useful test bed to experiment with shared memory. Try running multiple copies of all the programs. You can also experiment with using files to make the shared data persistent. You might also try modifying one of the programs so that it fails to unlock the shared memory and observe the results.
If you have access to a network, try pointing the shared memory to a file on the network and running the programs on several machines. You'll see it is hard to predict the exact behavior. I've included another application (SIMPDEMO, available electronically) that you can use for further experimentation. It simply provides access to an array of shared strings.
Shared memory has many uses. Simulations or other programs that easily divide into multiple processes are natural candidates for shared memory. Any set of cooperating processes can make use of shared memory. Imagine a set of voice, fax, and data-communications applications that share a phone book, for example.
Of course, you can use mapped files to simply read and write files. You don't have to share them with anybody. Instead of reading a text file, for example, you can map it using the same techniques and access it as a string in memory (of course, it might be a very long string). If you are not sharing the file mapping, you don't need to give it a name during CreateFileMapping.
New Win32 features allow you write distributed code that rivals UNIX-based software. Since Delphi allows you to access the API directly, you can use these features with no problem. Even better, you can encapsulate the arcane API calls into useful, easy-to-use, Delphi components.
Member Type Description
Sto Function Store a string in shared memory.
Rcl Function Recall a string from shared memory.
Lock Function Attempt to lock shared memory.
Unlock Procedure Unlock shared memory.
Clear Procedure Zero shared memory.
FirstUser* Property True if this program is the first user.
NewFile* Property True if the component created the file.
FileHandle* Property File handle (if any).
Valid* Property True if component loaded successfully.
Count** Property Number of shared-memory strings.
Size** Property Size of each string.
Filename** Property File name to use.
DeleteFlag** Property True if component should delete file when finished.
Figure 1: Shared-memory component's design interface.
Listing One
{ Shared memory component -- Williams }
unit shdmem;
interface
uses Windows, Messages, Classes, Controls,SysUtils, DsgnIntf,
Forms, Dialogs;
type
TShareMem=class(TComponent)
private
Ffilename : TFileName; { File name }
FDeleteFlag : Bool; { Delete on close? }
FFirstUser : Bool; { First user? }
FNewFile : Bool; { New file? }
fileh : THandle; { File handle }
fmap : THandle; { Handle to map }
addr : PChar; { Base address }
Fcount : Integer; { Number of strings }
FSize : Integer; { Size of each string }
Mutex : THandle; { Access Mutex }
FValid : Bool; { Good flag }
protected
{ no protected declarations }
public
constructor Create(obj : TComponent); override;
destructor Destroy; override;
procedure Loaded; override;
procedure UnLock;
procedure Clear;
function Rcl(n : integer;var s : String) : Bool;
function Sto(n : integer; s: String) : Bool;
function Lock(timeout : integer) : Bool;
Property FirstUser : Bool read FFirstUser;
Property NewFile: Bool read FNewFile;
Property FileHandle : THandle read fileh;
Property Valid : Bool read FValid;
published
property Count : Integer read FCount write FCount default 100;
property Size : Integer read FSize write FSize default 256;
property Filename : TFileName read FFilename write FFilename;
Property DeleteFlag : Bool read FDeleteFlag write FDeleteFlag;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Samples', [TShareMem]);
end;
constructor TShareMem.Create(obj : TComponent);
begin
inherited Create(obj);
{ Default setup }
FCount:=100;
FSize:=256;
Mutex:=0;
fileh:=-1;
FDeleteFlag:=False;
end;
destructor TShareMem.Destroy;
begin
{ Clear items }
if addr <> nil then
UnmapViewOfFile(addr);
if fmap <> 0 then
CloseHandle(fmap);
if fileh <> -1 then
CloseHandle(fileh);
if Mutex <> 0 then
CloseHandle(Mutex);
inherited Destroy;
end;
procedure TShareMem.Loaded;
var
delflag : Integer;
begin
inherited Loaded;
{ Only load if not designing }
if not (csDesigning in ComponentState) then
begin
{ Create OR open file mapping -- if map exists, this
just opens it }
FValid:=True; { Assume good things }
if (Fdeleteflag) then
delflag:=FILE_FLAG_DELETE_ON_CLOSE
else
delflag:=0;
if Ffilename <> '' then
fileh:=CreateFile(PChar(Ffilename),
GENERIC_READ or GENERIC_WRITE,0, nil,
OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL or delflag,0)
else
fileh:=THandle(-1);
if (fileh<>THandle(-1)) and
(GetLastError=Error_Already_Exists) then
FNewFile:=False
else
FNewFile:=True;
fmap:=CreateFileMapping(fileh,nil,PAGE_READWRITE,0,
FCount*FSize,PChar(Name));
if GetLastError=Error_Already_Exists then
FFirstUser:=False
else
FFirstUser:=True;
if fileh=THandle(-1) then
FNewFile:=FFirstUser;
if (fmap=THandle(0)) then FValid:=False;
addr:=MapViewOfFile(fmap,FILE_MAP_ALL_ACCESS,0,0,
FCount*FSize);
{ Create locking mutex }
Mutex:=CreateMutex(nil,FALSE,PChar(Name+'X'));
if Mutex=THandle(0) then FValid:=False;
end;
end;
function TShareMem.Rcl(n : integer;var s : String) : Bool;
var
ps:PChar;
begin
{ Lock, retrieve, and unlock }
Lock(INFINITE);
ps:=PChar(addr+(n*FSize));
s:=StrPas(ps);
Unlock;
result:=True;
end;
function TShareMem.Sto(n : integer; s: String) : Bool;
var
p: PChar;
begin
{ Lock, store, and unlock }
Lock(INFINITE);
p:=PChar(addr+(n*FSize));
StrPCopy(p,s);
Unlock;
result:=True;
end;
function TShareMem.Lock(timeout : integer) : Bool;
begin
result:=WaitForSingleObject(Mutex,timeout)<>0;
end;
procedure TShareMem.Unlock;
begin
ReleaseMutex(Mutex);
end;
procedure TShareMem.Clear;
begin
Lock(INFINITE);
FillChar(addr^,FCount*FSize,0);
Unlock;
end;
end.
Listing Two
{ Check in form }
unit vckinfrm;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls, shdmem;
type
TForm1 = class(TForm)
Label1: TLabel;
Name: TEdit;
Label2: TLabel;
Company: TEdit;
Label3: TLabel;
Visited: TEdit;
Label4: TLabel;
Timefield: TEdit;
Label5: TLabel;
Key: TEdit;
CheckIn: TButton;
Clear: TButton;
SharedMemory: TShareMem; { Shared Memory!}
procedure ClearClick(Sender: TObject);
procedure CheckInClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
function GetNewKey : String;
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
function TForm1.GetNewKey : String;
var
k : String;
keynum : Integer;
code : Integer;
begin
{ Lock shared memory }
SharedMemory.Lock(INFINITE);
{ Get next key }
SharedMemory.Rcl(0,k);
{ Convert to number }
Val(k,keynum,code);
{ Set return value from number (if string is empty
this ensures a zero return value) }
result:=IntToStr(keynum);
{ Increment next key value and put back }
keynum:=keynum+1;
k:=IntToStr(keynum);
SharedMemory.Sto(0,k);
SharedMemory.Unlock;
end;
procedure TForm1.CheckInClick(Sender: TObject);
begin
timefield.Text:=TimeToStr(Time);
key.Text:=GetNewKey;
{ commit to database here }
end;
procedure TForm1.ClearClick(Sender: TObject);
begin
timefield.Text:='';
key.Text:='';
name.Text:='';
company.Text:='';
visited.Text:='';
ActiveControl:=Name;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
if SharedMemory.NewFile then
{ clear memory }
SharedMemory.Clear;
end;
end.