Shared Memory Through Memory-Mapped Files


Memory-mapped files provide a way to look at a file as a chunk of memory. This feature is very useful in languages that support examining memory at ­arbitrary addresses. You map the file and get back a pointer to the mapped memory. You can simply read or write to memory from any location in the file mapping, just as you would from an array. When you’ve processed the file and closed the file mapping, the file is automatically updated. In other words, the operating system takes care of all the details of file I/O.


The API calls to create a file mapping are relatively simple, and you could ­easily call them from Visual Basic. There’s only one problem. See if you can spot it:

' Open file
hFile = CreateFile(sFileName, GENERIC_READ Or GENERIC_WRITE, 0, _
pNull, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, _
pNull)
' Open file mapping called MySharedMapping
hFileMap = CreateFileMapping(hFile, pNull, PAGE_READWRITE, 0, 0, _
"MySharedMapping")
' Get pointer to memory representing file
pFileMap = MapViewOfFile(hFileMap, FILE_MAP_WRITE, 0, 0, 0)

At this point, pFileMap is the address of a block of memory containing the file contents. Now, what can you do in Visual Basic with a pointer you receive from an API function? Repeat after me: “Pass it to another API function.” In other words, you’re stuck.


In C, you can treat a pointer like an array:

pFileMap[0] = 'A'
bTest = pFileMap[1]

But Visual Basic provides no similar capability. You can use CopyMemory to copy the file mapping to some other location in memory, but that usually ­defeats the purpose. The idea is to be able to use it in place. The fact is, memory-mapped files won’t be much use to Visual Basic programmers until they’re integrated into the language. Imagine this code:

Dim abFileMap() As Integer
hFileMap = FreeFile
Open sFileName For FileMap With abFileMap As #hFileMap
For i = 0 to UBound(abFileMap)
abFileMap(i) = CalculateMagicNumber(abFileMap(i))
Next
Close hFileMap

This might not be the best syntax. In fact, the next version of Visual Basic might implement file mapping behind the scenes with the existing syntax.


Although Visual Basic doesn’t support using memory-mapped files for file I/O, it doesn’t stand in your way if you want to use them for shared memory. You can create a file mapping that isn’t mapped to a file by passing a magic number (-1) instead of a handle to the CreateFileMapping function. This gives you a pointer to a named chunk of memory. Any program that knows the name can also get a pointer to the memory. You still can’t access the memory at that
location directly in Visual Basic, but any program can use CopyMemory to read or write to the memory.


The process is a little complicated, so I encapsulated it in the CSharedString class. You’ll need to run more than one instance of the test program shown in Figure 11-5 (TSHARE.VBP) to see the point. Change the text in one copy, and click the Set String button. Go to another instance of the program, and click the Get String button to read the current value.



Figure 11-5. Sharing strings.


Here’s the code to use a shared object:

Private ss As New CSharedString

Private Sub Form_Load()
ss.Create "MyShare"
If ss = sEmpty Then
ss = "Hello from the Creator"
End If
txtShare = ss
End Sub

Private Sub cmdSet_Click()
ss = txtShare
End Sub

Private Sub cmdGet_Click()
txtShare = ss
End Sub

Default members make using a shared string look a lot like using an ordinary string. Of course with normal strings you don’t have to call a Create method to assign a string name, which is different from the string value and different from the variable name. And you don’t have to remember that name any time you want to access that string from another program. But then you can’t access normal strings from another program anyway.


When the first test program creates a CSharedString object, the value is an empty string, so the program initializes to a string of its choice. When subsequent programs create objects, the value is whatever the previous program set. You can specifically destroy a shared string object by setting it to Nothing, but that’s usually unnecessary. The object is destroyed automatically when it goes out of scope during program destruction.


Most of the implementation work is done in the Create method, where the ­internal variables (h for handle and p for pointer) are initialized. The Class­_Terminate method undoes what Create did:

Private h As Long, p As Long

Sub Create(sName As String)
Dim e As Long
If sName = sEmpty Then ApiRaise ERROR_BAD_ARGUMENTS
' Try to create file mapping of 65535 (only used pages matter)
h = CreateFileMapping(-1, pNull, PAGE_READWRITE, 0, 65535, sName)
' Save "error" value which may not be an error value
e = Err.LastDllError
If h = hNull Then ApiRaise e

' Get pointer to mapping
p = MapViewOfFile(h, FILE_MAP_WRITE, 0, 0, 0)
If p = pNull Then
CloseHandle h ' Undo what we did
ApiRaise Err.LastDllError
End If
' Check cached value to see if new value
If e <> ERROR_ALREADY_EXISTS Then
' Set size of new file mapping by copying 0 to first 4 bytes
CopyMemory ByVal p, 0, 4
' Else
' Existing file mapping already initialized
End If
End Sub

Private Sub Class_Terminate()
UnmapViewOfFile p
CloseHandle h
End Sub

First the code calls CreateFileMapping to create a read-write memory mapping that is 65,535 bytes in length, with the name passed in the Create method. Wait a minute! You don’t want 65,535 bytes of memory just to share a 20-byte string. Don’t worry. Through the wonders of memory paging, you’ll use only the pages you touch. In other words, you’ll use one page of memory (4 KB) for that
20-byte string. That’s enough to make you a little careful about how many shared memory objects you create, but it’s not the same as throwing away 64 KB. Still, you’ll probably want to use CSharedString for large strings and create some other shared memory class (perhaps a CSharedStrings array) for sharing lots of little strings.


After you create a file mapping (memory mapping would be more accurate in this context), you need to call MapViewOfFile to get a pointer to it. You’ll get the same return (a pointer) whether you’re opening an existing mapping or creating a new one. You need to distinguish these two cases, and the only way to do so is to check the error value for ERROR_ALREADY_EXISTS, which isn’t really an error. If it’s a new mapping, you need to initialize the data. Here is my code to read and write the data:

' Default property
Property Get Item() As String
If h = hNull Then ErrRaise ERROR_INVALID_DATA
BugAssert p <> pNull
' Copy length out of first 4 bytes of data
Dim c As Long
CopyMemory c, ByVal p, 4
If c Then
' Copy the data
Item = String$(c, 0)
CopyMemoryToStr Item, ByVal (p + 4), c * 2
End If
End Property

Property Let Item(s As String)
If h = hNull Then ErrRaise ERROR_INVALID_DATA
BugAssert p <> pNull
Dim c As Long
c = Len(s)
' Copy length to first 4 bytes and string to remainder
CopyMemory ByVal p, c, 4
CopyMemoryStr ByVal (p + 4), s, c * 2
End Property

In your own shared memory classes, you can initialize and organize the data anyway you like as long as everyone who uses the data knows the convention. I save a shared string as a Long containing the string length, followed by the bytes of the string. I thought about various schemes for allowing multiple chunks of data and perhaps multiple data types—such as arrays. But I decided to leave you to design the CSharedCollection class.


When you test shared string objects, don’t terminate by using the End toolbar button, the End item on the Run menu, or the End statement. Instead, unload your forms. This is good practice in general, but it’s particularly important with the shared string class. Ending a program short-circuits the normal destruction mechanism, and you’ll “End” up with dangling copies of the shared memory. A shared memory mapping is destroyed only when all of its clients have unmapped it. If a program dies without unmapping, its mapping handle dies with it. The only way to get rid of the shared data is to log off.