Interprocess Communication
Quite often, two Windows CE processes need to communicate. The walls between processes that protect processes from one another prevent casual exchanging of data. The memory space of one process isn't exposed to another process. Handles to files or other objects can't be passed from one process to another. Windows CE doesn't support handle inheritance. Some of the other more common methods of interprocess communication, such as named pipes, are also not supported under Windows CE. However, you can choose from plenty of ways to enable two or more processes to exchange data.
Finding Other Processes
Before you can communicate with another process, you have to determine whether it's running on the system. Strategies for finding whether another process is running depend mainly on whether you have control of the other process. If the process to be found is a third-party application in which you have no control over the design of the other process, the best method might be to use the FindWindow function to locate the other process's main window. FindWindow can search either by window class or by window title. You can enumerate the top-level windows in the system using EnumWindows. You can also use the ToolHelp debugging functions to enumerate the processes running, but this works only when the ToolHelp DLL is loaded on the system, and unfortunately, it generally isn't included, by default, on most systems.
If you're writing both processes, however, it's much easier to enumerate them. In this case, the best methods include using the tools you'll later use in one process to communicate with the other process, such as named mutexes, events, or memory-mapped objects. When you create one of these objects, you can determine whether you're the first to create the object or you're simply opening another object by calling GetLastError after another call created the object. And the simplest method might be the best; call FindWindow.The classic case of using FindWindow on a Pocket PC occurs when an application must determine whether another copy of itself is already running. According to the Pocket PC and the earlier Palm-size PC guidelines, an application must allow only one copy of itself to run at a time. Following is a code fragment that all the examples in this book use for accomplishing this task.
// If Pocket PC, allow only one instance of the application.
HWND hWnd = FindWindow (szAppName, NULL);
if (hWnd) {
SetForegroundWindow ((HWND)(((DWORD)hWnd) | 0x01));
return -1;
}
The first statement uses FindWindow to find a window class of the same name as the class of the application's main window. Because this call is made before the main window is created in the application, the only way the window could have been found, assuming you're using a unique name for your window class, is for it to have already been created by another copy of your application. An advantage of this technique is that FindWindow returns the handle of the main window of the other instance. In the case of the Pocket PC, we want to set that instance in the foreground, which is what we do with the subsequent call to SetForegroundWindow. The ORing of the 1 to the window handle is a hack of Windows CE that causes the window being activated to be restored if it was in a minimized state.
WM_COPYDATA
After you find your target process, the talking can begin. If you're staying at the window level, you can simply send a WM_COPYDATA message. WM_COPYDATA is unique in that it's designed to send blocks of data from one process to another. You can't use a standard user-defined message to pass pointers to data from one process to another because a pointer isn't valid across processes. WM_COPYDATA gets around this problem by having the system translate the pointer to a block of data from one process's address space to another's. The recipient process is required to copy the data immediately into its own memory space, but this message does provide a quick-and-dirty method of sending blocks of data from one process to another.
Named Memory-Mapped Objects
The problem Chapter 8 in the discussion of memory-mapped files. Because no file is specified, you must specify the size of the memory-mapped region in the maximum size fields of CreateFileMapping. The following routine creates a 16-MB region by using a memory-mapped file:
// Create a 16-MB memory-mapped object.
hNFileMap = CreateFileMapping ((HANDLE)-1, NULL, PAGE_READWRITE,
0, 0x1000000, NULL);
if (hNFileMap)
// Map in the object.
pNFileMem = MapViewOfFile (hNFileMap,
FILE_MAP_WRITE, 0, 0, 0);
The memory object created by this code doesn't actually commit 16 MB of RAM. Instead, only the address space is reserved. Pages are autocommitted as they're accessed. This process allows an application to create a huge, sparse array of pages that takes up only as much physical RAM as is needed to hold the data. At some point, however, if you start reading or writing to a greater number of pages, you'll run out of memory. When this happens, the system generates an exception. I'll talk about how to deal with exceptions later in this chapter. The important thing to remember is that if you really need RAM to be committed to a memory-mapped object, you need to read each of the pages so that the system will commit physical RAM to that object. Of course, don't be too greedy with RAM; commit only the pages you absolutely require.
Naming a Memory-Mapped Object
A memory-mapped object can be named by passing a string to CreateFileMapping. This isn't the name of a file being mapped. Instead, the name identifies the mapping object being created. In the preceding example, the region was unnamed. The following code creates a memory-mapped object named Bob. This name is global so that if another process opens a mapping object with the same name, the two processes will share the same memory-mapped object.
// Create a 16-MB memory-mapped object.
hNFileMap = CreateFileMapping ((HANDLE)-1, NULL, PAGE_READWRITE,
0, 0x1000000, TEXT ("Bob"));
if (hNFileMap)
// Map in the object.
pNFileMem = MapViewOfFile (hNFileMap,
FILE_MAP_WRITE, 0, 0, 0);
The difference between named and unnamed file mapping objects is that a named object is allocated only once in the system. Subsequent calls to CreateFileMapping that attempt to create a region with the same name will succeed, but the function will return a handle to the original mapping object instead of creating a new one. For unnamed objects, the system creates a new object each time CreateFileMapping is called.
When you're using a memory-mapped object for interprocess communication, processes should create a named object and pass the name of the region to the second process rather than pass a pointer. While the first process can simply pass a pointer to the mapping region to the other process, this isn't advisable. If the first process frees the memory-mapped file region while the second process is still accessing the file, the operating system throws an exception. Instead, the second process should create a memory-mapped object with the same name as the initial process. Windows knows to pass a pointer to the same region that was opened by the first process. The system also increments a use count to track the number of opens. A named memory-mapped object won't be destroyed until all processes have closed the object. This system assures a process that the object will remain at least until it closes the object itself. The XTalk example, presented later in this chapter, provides an example of how to use a named memory-mapped object for interprocess communication.
Message Queues
Windows CE supports a method of interprocess communication called message queues. The Message Queue API, as the name suggests, provides data queues for sending data from one process to another.To communicate with a message queue, a process or pair of processes creates a message queue for reading and one for writing. A call to create or open a queue can specify only read or write access, not both read and write access. The queue is then opened again for the corresponding write or read access. "Messages" are then written to the queue by using the write handle to the queue. (In this context, a message is simply a block of data with a defined length.) The message can be read by using the read handle to the queue. If a series of messages is written to a queue, they are read in the order they were written, in classic first in, first out (FIFO) fashion. When a queue is created, the number and the maximum size of messages are defined for the queue. If the queue is full and a write occurs, the write function will either block (waiting for a free slot in the queue), fail and return immediately, or wait for a specific amount of time before failing and returning. Likewise, read functions can block until a message is in the queue to be read, or they can wait a specific period of time before returning.In addition, a message can be marked as an "alert" message. Alert messages are sent to the front of the queue so that the next read of the queue will read the alert message regardless of the number of messages that have been waiting to be read. Only one alert message can be in the queue at any one time. If a second alert message is written to the queue before the first one was read, the second alert message replaces the first and the first alert message is lost.To create a message queue, call this function:
HANDLE WINAPI CreateMsgQueue (LPCWSTR lpszName, LPMSGQUEUEOPTIONS lpOptions);
The first parameter is the name of the queue that will be either opened or created. The name is global to the entire system. That is, if one process opens a queue with a name and another process opens a queue with the same name, they open the same queue. The name can be up to MAX_PATH characters in length. The parameter can also be set to NULL to create an unnamed queue.The second parameter of CreateMsgQueue is a pointer to a MSGQUEUEOPTIONS structure defined as follows:
typedef MSGQUEUEOPTIONS_OS {
DWORD dwSize;
DWORD dwFlags;
DWORD dwMaxMessages;
DWORD cbMaxMessage;
BOOL bReadAccess
} MSGQUEUEOPTIONS;
The dwSize field must be filled in with the size of the structure. The dwFlags parameter describes how the queue should act. The flags supported are MSGQUEUE_NOPRECOMMIT, which tells Windows CE not to allocate the RAM necessary to support messages in the queue until the RAM is needed; and MSGQUEUE_ALLOW_BROKEN, which allows writes and reads to the queue to succeed even if another call hasn't been made to open the queue for the matching read or write of the message. The dwMaxMessages field should be set to the maximum number of messages that are expected to be in the queue at any one time. The cbMaxMessage field indicates the maximum size of any single message. Finally, the bReadAccess field should be set to TRUE if read access is desired for the queue and FALSE if write access is desired. A single call to CreateMsgQueue can only create the queue for either read or write access. To open a queue for both read and write access, CreateMsgQueue should be called twice, once for read access and once for write access.The function returns the handle to the queue if successful, or NULL if the function failed. The handle returned by CreateMsgQueue is an event handle that can be waited on with WaitForSingleObject and the other related Wait functions. The event is signaled when the state of the queue changes, either by a new message being placed in the queue or by an entry in the queue becoming available.CreateMsgQueue will succeed even if a queue of the same name already exists. GetLastError will return ERROR_ALREADY_EXISTS if the queue existed before the call to CreateMsgQueue.An unnamed message queue can be opened with this function:
HANDLE WINAPI OpenMsgQueue (HANDLE hSrcProc, HANDLE hMsgQ,
LPMSGQUEUEOPTIONS pOptions);
The parameters are the process handle of the process that originally opened the message queue, the handle returned by CreateMsgQueue, and a pointer to a MSGQUEUEOPTIONS structure. The only fields in the MSGQUEUEOPTIONS structure examined by the function are the dwSize field and the bReadAccess field.To write a message to the queue, the aptly named WriteMsgQueue function is used. It is prototyped as follows:
BOOL WINAPI WriteMsgQueue (HANDLE hMsgQ, LPVOID lpBuffer, DWORD cbDataSize,
DWORD dwTimeout, DWORD dwFlags);
The initial parameter is the write handle to the message queue. The lpBuffer parameter points to the buffer containing the message, whereas cbDataSize should be set to the size of the message. If cbDataSize is greater than the maximum message size set when the queue was created, the call will fail.
The dwTimeout parameter specifies the time, in milliseconds, that WriteMsgQueue should wait for a slot in the queue to become available before returning. If dwTimeout is set to 0, the call will fail and return immediately if the queue is currently full. If dwTimeout is set to INFINITE, the call will wait until a slot becomes free to write the message. The dwFlags parameter can be set to MSGQUEUE_MSGALERT to indicate that the message being written is an alert message.The return value from WriteMsgQueue is a Boolean, with TRUE indicating success. The function will fail if the queue has not been opened for read access and MSGQUEUE_ALLOW_BROKEN was not specified when the queue was created. To determine the reason for failure, call GetLastError.To read a message from the queue, the function ReadMsgQueue is used. It's prototyped as follows:
BOOL ReadMsgQueue (HANDLE hMsgQ, LPVOID lpBuffer, DWORD cbBufferSize,
LPDWORD lpNumberOfBytesRead, DWORD dwTimeout,
DWORD* pdwFlags);
As with WriteMsgQueue, the first two parameters are the handle to the message queue, the pointer to the buffer that, in this case, will receive the message. The cbBufferSize parameter should be set to the size of the buffer. If cbBufferSize is less than the size of the message at the head of the queue, the read will fail with ERROR_INSUFFICIENT_BUFFER returned by a call to GetLastError.The lpNumberOfBytesRead parameter should point to a DWORD that will receive the size of the message read. The dwTimeout parameter specifies how long the function should wait until a message is present in the queue to read. As with WriteMsgQueue, passing 0 in this parameter causes ReadMsgQueue to fail and return immediately if there is no message in the queue. Passing INFINITE in the dwTimeout parameter causes the call to wait until there is a message in the queue before returning. The pdwFlags parameter should point to a DWORD that will receive the flags associated with the message read. The only flag currently defined is MSGQUEUE_MSGALERT, which indicates that the message just read was an alert message.You can query the configuration of a message queue with this function:
BOOL GetMsgQueueInfo (HANDLE hMsgQ, LPMSGQUEUEINFO lpInfo);
The parameters are the handle to the message queue and a pointer to a MSGQUEUEINFO structure defined as follows:
typedef MSGQUEUEINFO {
DWORD dwSize;
DWORD dwFlags;
DWORD dwMaxMessages;
DWORD cbMaxMessage;
DWORD dwCurrentMessages;
DWORD dwMaxQueueMessages;
WORD wNumReaders;
WORD wNumWriters
} MSGQUEUEINFO;
The first few fields in this structure match the MSGQUEUEOPTIONS structure used in creating and opening queues. The field dwSize should be set to the size of the structure before the call to GetMsgQueueInfo is made. The remaining fields are filled in by a successful call to GetMsgQueueInfo.The dwFlags field will be set to the queue flags, which are MSGQUEUE_NOPRECOMMIT and MSGQUEUE_ALLOW_BROKEN. The dwMaxMessages field contains the maximum number of messages the queue can contain, while cbMaxMessage contains the maximum size of any single message.The dwCurrentMessages field is set to the number of messages currently in the queue waiting to be read. The dwMaxQueueMessages field is set to the maximum number of messages that were ever in the queue. The wNumReaders field is set to the number of handles opened for read access for the queue, while wNumWriters is set to the number of handles opened for write access.To close a message queue, call this function:
BOOL WINAPI CloseMsgQueue (HANDLE hMsgQ);
The single parameter is the handle to the queue. Because queues must be opened at least twice, once for reading and once for writing, this call must be made at least twice per queue.Message queues are great for interprocess communication because they are fast and they are thread safe. Messages can be almost any size, although for long queues with really huge buffers it might be best to allocate data buffers dynamically by using memory-mapped objects and by using message queues to pass pointers to the large data buffers.
Communicating with Files and Databases
A more basic method of interprocess communication is the use of files or a custom database. These methods provide a robust, if slower, communication path. Slow is relative. Files and databases in the Windows CE object store are slow in the sense that the system calls to access these objects must find the data in the object store, uncompress the data, and deliver it to the process. However, since the object store is based in RAM, you see none of the extreme slowness of a mechanical hard disk that you'd see under the desktop versions of Windows. To improve performance with files in the object store, the FILE_FLAG_RANDOM_ACCESS flag should be used.