Working with Processes and Threads

All Windows CE–based applications consist of a process and one or more threads. A process is a single instance of a running application. A thread is the basic unit to which the Windows CE operating system (OS) allocates processor time. A thread can execute any part of the process code, including parts currently being executed by another thread.

Processes enable users to open and work in several applications at the same time. For example, a user can edit a file in a word processing application while another application is recalculating a spreadsheet. Threads enable an application to perform more than one task at a time even though applications cannot execute more than one thread at a time; however, Windows CE supports preemptive multitasking, which creates the effect of simultaneously executing multiple threads. When a process has more than one thread running, the OS rapidly switches from one thread to another so that the threads appear to run simultaneously.


Processes

Windows CE can run up to 32 processes at one time. Each process starts with a single thread—often called the “primary thread”—which provides the resources that are required to run an application. Windows CE creates a primary thread when the loader calls the WinMain function. A process can also create additional threads when needed. The number of additional threads a process can create is limited only by the amount of random access memory (RAM) available on the device.

When the Windows CE OS initializes, it creates a single 4-gigabyte (GB) virtual address space. The address space is divided into 33 “slots,” and each slot is 32 megabytes (MBs). The address space is shared by all processes. When a process initializes, Windows CE selects an open slot for the process in the system’s address space. Slot zero is reserved for the currently running process.

In addition to assigning a slot for each process, Windows CE creates a stack for the thread and a heap for the process. Each stack has an initial size of at least 1 kilobyte (KB), which is committed on demand. The amount of stack space that is reserved by the system is specified in the (/STACK) option for the linker. Because the stack size is CPU-dependent, on some devices the system allocates 4 KB for each stack . The maximum number of threads is dependent upon the amount of available memory. You can allocate additional memory, outside of the 32 MB that is assigned to each slot, by using memory-mapped files or by calling the VirtualAlloc function.

The following illustration shows how memory is allocated in the Windows CE address space.

When a process initializes, the OS stores in the slot that is assigned to the process all of the dynamic-link libraries (DLLs), the stack, the heap, the application code, and the data section for each process. DLLs are loaded at the top of the slot, followed by the stack, the heap, and the executable file (.exe). The bottom 64 KB is always left free. DLLs are controlled by the loader, which loads all DLLs at the same address for each process.


Creating a Process

To start a process from within another process, call the CreateProcess function, which loads a new application into memory and creates a new process with at least one new thread.

The following code example shows the CreateProcess function prototype.

BOOL CreateProcess(LPCTSTR lpApplicationName,
LPTSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment,
LPCTSTR lpCurrentDirectory, LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation );

Because Windows CE does not support security or current directories and does not handle inheritance, the majority of the parameters must be set to NULL or 0. The following code example shows how the function prototype would look when all nonsupported features are taken into consideration.

BOOL CreateProcess(LPCTSTR lpApplicationName,
LPTSTR lpCommandLine, NULL, NULL, FALSE,
DWORD dwCreationFlags, NULL, NULL, NULL,
LPPROCESS_INFORMATION lpProcessInformation );

The first parameter, lpApplicationName, must contain a pointer to the name of the application to start. Windows CE does not support passing NULL for lpApplicationName and looks for the application in the following directories, in the following order:

  1. The path specified in lpApplicationName, if one is listed.
  2. An OEM-specified search path.
  3. The Windows directory (\Windows).
  4. The root directory in the object store (\).

The lpCommandLine parameter specifies the command line to pass to the new process. The command line must be passed as a Unicode string. The dwCreationFlags parameter specifies the initial state of the process after loading. The following table describes all of the supported flags.

Flag
Description
0 | Creates a standard process.
CREATE_SUSPENDED | Creates a process with a suspended primary thread.
DEBUG_PROCESS | Creates a process to be debugged by the calling process.
DEBUG_ONLY_THIS_PROCESS | Creates a process to be debugged by the calling process, but doesn’t debug any child processes that are launched by the process being debugged. This flag must be used in conjunction with DEBUG_PROCESS.
CREATE_NEW_CONSOLE | Creates a new console.

The last parameter used by CreateProcess is lpProcessInformation. This parameter points to the PROCESS_INFORMATION structure, which contains data about the new process. The parameter can also be set to NULL.

If the process cannot run, CreateProcess returns FALSE. For more information about the failure, call the GetLastError function.


Terminating a Process

The most common way to terminate a process is to have it return from a WinMain function call. You can also terminate a process by having the primary thread of the process call the ExitThread function. A Windows CE process automatically terminates if its primary thread is terminated, even if there are other active threads in existence for the process. ExitThread returns the exit code of the process. You can determine the exit code of a process by calling the GetExitCodeProcess function. Specify the handle to the process, which you can obtain by calling the CreateProcess or OpenProcess function; the function returns the exit code. If the process is still running, the function returns the STILL_ACTIVE termination status.

There are also other, less common, ways of terminating a process:

  • Use interprocess synchronization to instruct the process to terminate itself.

    For more information on using interprocess communication to terminate a process, see “Interprocess Synchronization.”

  • If the process has a message queue, send a WM_CLOSE message to the main window of the process. An application might not close if it does not receive this message and might display a message box.
  • Use the TerminateProcess function, which does not notify any attached DLLs that the process is terminating. This method should be used as a last resort.

Note A process immediately terminates if a related secondary thread generates an unhandled exception. This is a change in behavior from Windows CE version 2.10 or earlier.


Threads

A thread describes a path of execution within a process. Every time the OS creates a new process, it also creates at least one thread. The purpose of creating a thread is to make use of as much of the CPU’s time as possible. For example, in many applications, it is useful to create a separate thread to handle printing tasks so that the user can continue to use the application while it is printing.

Each thread shares all of the process’s resources, including its address space. Additionally, each thread has a stack, where the linker sets the stack size for all of the threads that are created in a process (/STACK). A thread also contains the state of the CPU registers, known as the “context,” and an entry in the execution list of the system scheduler. You can use the GetThreadContext function to retrieve the context of the specified thread and the SetThreadContext function to set the context of the specified thread.

Each thread in a process operates independently. Unless you make the threads visible to each other, they execute individually and are unaware of the other threads in a process. Threads sharing common resources, however, must coordinate their work by using a method of synchronization.

An application starts when the system scheduler gives one of its threads execution control. The system scheduler determines which threads should run and when they should run. Threads of lower priority may have to wait while higher priority threads complete their tasks.

Threads can be in one of the following states: running, suspended, sleeping, blocked, or terminated. When all threads are in the blocked state, Windows CE enters the suspended mode, which stops the CPU from executing instructions and consuming power. To conserve power, be sure to use synchronization objects to block threads that are waiting, instead of creating a thread that polls for status, such as the PeekMessage function.


Creating and Terminating a Thread

To create a thread, call the CreateThread function. The following code example shows the CreateThread function prototype.

HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes,
DWORD dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId );

Because Windows CE does not support the lpThreadAttributes and dwStackSize parameters, you must set them to NULL or 0. The following table describes the remaining CreateThread parameters.

Parameter
Description
lpStartAddress | Points to the start of the thread routine
lpParameter | Specifies an application-defined value that is passed to the thread routine
dwCreationFlags | Set to 0 or CREATE_SUSPENDED
lpThreadId | Points to a DWORD that receives the new thread’s identifier

If CreateThread is successful, it returns the handle to the new thread and the thread identifier. You can also retrieve the thread identifier by calling the GetCurrentThreadId function from within the thread. In Windows CE, the value returned in GetCurrentThreadId is the actual thread handle. You can also retrieve a handle to the thread by calling the GetCurrentThread function. This function returns a pseudo-handle to the thread that is valid only while in the thread. If you specify CREATE_SUSPENDED in the dwCreationFlags parameter, the thread is created in a suspended state and must be resumed with a call to the ResumeThread function.

You can terminate a thread by calling ExitThread, which frees the resources that are used by a thread when they are no longer needed. Calling ExitThread for an application’s primary thread causes the application to terminate.


Scheduling a Thread

Windows CE uses a priority-based time-slice algorithm to schedule the execution of threads. Because Windows CE does not have priority classes, the process in which the thread runs does not influence thread priorities. All the priorities can be used in the same process. A thread can have one of the eight priorities. The following table describes these priorities.

Priority
Description
THREAD_PRIORITY_TIME_CRITICAL | Indicates 3 points above normal priority
THREAD_PRIORITY_HIGHEST | Indicates 2 points above normal priority
THREAD_PRIORITY_ABOVE_NORMAL | Indicates 1 point above normal priority
THREAD_PRIORITY_NORMAL | Indicates normal priority
THREAD_PRIORITY_BELOW_NORMAL | Indicates 1 point below normal priority
THREAD_PRIORITY_LOWEST | Indicates 2 points below normal priority
THREAD_PRIORITY_ABOVE_IDLE | Indicates 3 points below normal priority
THREAD_PRIORITY_IDLE | Indicates 4 points below normal priority

Threads with a higher priority run first. Threads with the same priority run in a round-robin fashion—when a thread has stopped running, all other threads of the same priority run before the original thread can continue. Threads at a lower priority do not run until all threads with a higher priority have either finished or have been blocked. If one thread is running and a thread of higher priority is unblocked, the lower-priority thread is immediately suspended and the higher-priority thread is scheduled.

Threads run for a specific slice of time—called a quantum—which has a default value of 25 milliseconds. An OEM can specify a different quantum. If, after the quantum has elapsed, the thread has not relinquished its time slice and is not time-critical, it is suspended and another thread is scheduled to run. Threads having a priority level of THREAD_PRIORITY_TIME_CRITICAL cannot be preempted except by an interrupt service routine (ISR).

For the most part, thread priorities are fixed and do not change. However, there is one exception, called priority inversion. If a low-priority thread is using a resource that a high-priority thread is waiting to use, the kernel temporarily boosts the priority of the low-priority thread until it releases the resource that is required by the higher-priority thread.

To query the priority level of a thread, call the GetThreadPriority function. To change the priority level of a thread, call the SetThreadPriority function.


Suspending a Thread

You can suspend a thread at any time by using the SuspendThread function. You can also perform additional tasks on a thread. The following table describes these tasks.

To
Call
Resume running a thread | ResumeThread
Suspend a thread for a specified number of milliseconds | Sleep
Profile the performance of a thread and return how much time the thread has run | GetThreadTimes

When suspending a thread more than once, you must match multiple calls to SuspendThread with the same number of calls to ResumeThread.


Using Thread Local Storage

Thread local storage (TLS) is the method by which each thread in a multithreaded process allocates a location in which to store thread-specific data. There are several situations in which you may want a thread to access unique data. One example might include a spreadsheet application that creates a new instance of the same thread each time the user opens a new spreadsheet. The DLL that provides the functions for various spreadsheet operations can use TLS to save data about the current state of each spreadsheet.

TLS uses a TLS array to save thread-specific data. When a process is created, Windows CE allocates a 64-slot array for each running process. When a DLL attaches to a process, the DLL calls the TlsAlloc function, which looks through the array to find a free slot. The function then marks the slot “in use” and returns an index value to the newly assigned slot. If no slots are available, the function returns –1. Individual threads cannot call TlsAlloc. Only a process or DLL can call the function and it must do so before creating the threads that will use the TLS slot.

Once a slot has been assigned, each thread can access its unique data by calling the TlsSetValue function to store data in the TLS slot, or the TlsGetValue function to retrieve data from the slot.

The following table describes the TLS functions that are supported by Windows CE.

Function
Description
TlsAlloc | Allocates a TLS index. The index is available to any thread in the process for storing and retrieving thread-specific values. You must store this index in global memory, where all threads can retrieve its value.
TlsFree | Releases the TLS index, making it available for reuse.
TlsGetValue | Retrieves the value that is pointed to by the TLS index.
TlsSetValue | Stores a value in the slot that is pointed to by the TLS index.


Synchronizing Processes and Threads

In an OS where several threads run concurrently, it is important to be able to synchronize the activities of various threads. Windows CE provides several synchronization objects that enable you to synchronize a thread’s actions with those of another thread. These objects include: critical sections, mutexes, and events. Additionally, you can use interlocked functions to synchronize a thread.

Regardless of the synchronization method that is used, a thread synchronizes itself with another thread by releasing a synchronization object and then entering a wait state. The synchronization object tells the OS what special event has to occur before the thread can resume execution. When the event occurs, the thread is again eligible to be scheduled for CPU time. Once it is scheduled, the thread continues executing. The thread has now synchronized its execution with the occurrence of the event.


Critical Section Objects

When multiple threads have shared access to the same data, the threads can interfere with one another. A critical section object protects a section of code from being accessed by more than one thread. A critical section is limited, however, to only one process or DLL and cannot be shared with other processes.

Critical sections work by having a thread call the EnterCriticalSection function to indicate that it has entered a critical section of code. If another thread calls EnterCriticalSection and references the same critical section object, it is blocked until the first thread calls the LeaveCriticalSection function. A critical section can protect more than one section of code as long as each section of code is protected by the same critical section object.

To use a critical section, you must first declare a CRITICAL_SECTION structure. Because other critical section functions require a pointer to this structure, be sure to allocate it within the scope of all functions that are using the critical section. Then, create a handle to the critical section object by calling the InitializeCriticalSection function.

To request ownership of a critical section, call EnterCriticalSection; to release ownership, call LeaveCriticalSection. When you are finished with a critical section, call the DeleteCriticalSection function to release the system resources that were allocated when you initialized the critical section.

The following code example shows the prototype for the critical section functions. Notice that they all require a pointer to the CRITICAL_SECTION structure.

void InitializeCriticalSection (LPCRITICAL_SECTION lpCriticalSection);
void EnterCriticalSection (LPCRITICAL_SECTION lpCriticalSection);
void LeaveCriticalSection (LPCRITICAL_SECTION lpCriticalSection);
void DeleteCriticalSection (LPCRITICAL_SECTION lpCriticalSection);

The following code example shows how a thread initializes, enters, and leaves a critical section. This example uses the try-finally structured exception-handling syntax to ensure that the thread calls LeaveCriticalSection to release the critical section object.

void CriticalSectionExample (void)
{
  CRITICAL_SECTION csMyCriticalSection;

  InitializeCriticalSection (&csMyCriticalSection);

  __try
  {
    EnterCriticalSection (&csMyCriticalSection);

    // Your code to access the shared resource goes here.
  }
  __finally
  {
    // Release ownership of the critical section
    LeaveCriticalSection (&csMyCriticalSection);
  }
} // End of CriticalSectionExample code


Mutex Objects

A mutex object is a synchronization object whose state is set to signaled when it is not owned by any thread and non-signaled when it is owned. Its name comes from its usefulness in coordinating mutually exclusive access to a shared resource. Only one thread at a time can own a mutex object.

A thread calls the CreateMutex function to create a mutex object. The creating thread can request immediate ownership of the mutex object as well as specify a name for the mutex object. Threads in other processes can open a handle to an existing mutex object by specifying the object’s name in a call to CreateMutex. If the mutex object already exists, GetLastError returns ERROR_ALREADY_EXISTS. For more information about names for mutex objects and event objects, see “Interprocess Synchronization.”

Any thread with a handle to a mutex object can use a wait function to request ownership of the mutex object. If the mutex object is owned by another thread, the wait function blocks the requesting thread until the owning thread releases the mutex object by calling the ReleaseMutex function. The return value of the wait function indicates whether the function returned for some reason other than that the state of the mutex is set to signaled. For more information about wait functions, see “Wait Functions.”

Once a thread owns a mutex object, it can specify the same mutex object in repeated calls to one of the wait functions without blocking its execution. This prevents a thread from deadlocking itself while waiting for a mutex object that it already owns. To release its ownership under such circumstances, the thread must call ReleaseMutex once for each time that the mutex object satisfied the conditions of a wait function.

If a thread terminates without releasing its ownership of a mutex object, the mutex object is considered to be abandoned. A waiting thread can acquire ownership of an abandoned mutex object, but the wait function’s return value indicates that the mutex object is abandoned. To be safe, you should assume that an abandoned mutex object indicates that an error has occurred and that any shared resource being protected by the mutex object is in an undefined state. If the thread proceeds as though the mutex object had not been abandoned, the object’s abandoned flag is cleared when the thread releases its ownership. This restores typical behavior, if a handle to the mutex object is subsequently specified in a wait function.

The following code example shows how to call CreateMutex to create a named mutex object.

void NamedMutexExample (void)
{
  HANDLE  hMutex;
  TCHAR szMsg[100];

  hMutex = CreateMutex (
                NULL,                       // No security descriptor
                FALSE,                      // Mutex object not owned
                TEXT("NameOfMutexObject")); // Object name

  if (NULL == hMutex)
  {
    // Your code to deal with the error goes here.

    // Here is one example of what might be done.
    wsprintf (szMsg, TEXT("CreateMutex error: %d."), GetLastError ());
    MessageBox (NULL, szMsg, TEXT("Error"), MB_OK);
  }
  else
  {
    // Not an error -- deal with success
    if ( ERROR_ALREADY_EXISTS == GetLastError () )
      MessageBox (NULL, TEXT("CreateMutex opened existing mutex."),
                  TEXT("Results"), MB_OK);
    else
      MessageBox (NULL, TEXT("CreateMutex created new mutex."),
                  TEXT("Results"), MB_OK);
  }
} // End of NamedMutexExample code

The following code example opens a handle of an existing mutex object. Additionally, it uses the try-finally structured exception-handling syntax to ensure that the thread properly releases the mutex object. To prevent the mutex object from being abandoned inadvertently, the finally block of code executes no matter how the try block terminates, unless the try block includes a call to the TerminateThread function.

BOOL WriteToDatabase (HANDLE hMutex)
{
  DWORD dwWaitResult;

  dwWaitResult = WaitForSingleObject (hMutex,   // Handle of mutex object
                                      5000L);   // Five-second time-out
  switch (dwWaitResult)
  {
    case WAIT_OBJECT_0:
      __try
      {
        // Your code to write to the database goes here.
      }
      __finally
      {
        // Your code to clean up the database operations goes here.

        if (! ReleaseMutex (hMutex))
        {
          // Your code to deal with the error goes here.
        }
      }
      break;

    // Cannot get mutex object ownership due to time-out
    case WAIT_TIMEOUT:
      return FALSE;

    // Got ownership of an abandoned mutex object
    case WAIT_ABANDONED:
      return FALSE;
  }

  return TRUE;
} // End of WriteToDatabase example code


Event Objects

Windows CE uses event objects to notify a thread when to perform its task or to indicate that a particular event has occurred. For example, a thread that writes to a buffer resets the event object to signaled when it has finished writing. By using an event object to notify the thread that its task is finished, the thread can immediately start performing other tasks.

To create an event object, call the CreateEvent function. The following code example shows the CreateEvent function prototype.

HANDLE CreateEvent (NULL, BOOL bManualReset, BOOL bInitialState,
                    LPTSTR lpName);

Use the lpName parameter to name the event object or to leave the object unnamed. The bManualReset parameter enables you to specify whether the event object automatically resets itself from a signaled state to a nonsignaled state or whether it will require a manual reset. Use the bInitialState parameter to specify whether the event object is created in the signaled or nonsignaled state.

Named events can be shared with other processes. Threads in other processes can open a handle to an existing event object by specifying its name in a call to CreateEvent. All named synchronization objects are stored in the same queue. To determine whether a CreateEvent function created a new object or opened an existing object, you can call the GetLastError function immediately after calling CreateEvent. If GetLastError returns ERROR_ALREADY_EXISTS, the call opened an existing event. In other words, if the name specified in a call to CreateEvent matches the name of an existing event object, the function returns the handle of the existing object. When you use this technique for event objects, none of the calling processes should request immediate ownership of the event, because it is uncertain which process actually gets ownership.

To signal an event, use the SetEvent or PulseEvent function. SetEvent does not automatically reset the event object to a nonsignaled state. PulseEvent signals the event, and then it resets the event. For manual events, PulseEvent unblocks all threads waiting on the event. For automatic events, PulseEvent unblocks only one thread.

If an event object can reset itself, you need only use SetEvent to signal. The event object is then automatically reset to the nonsignaled state when one thread is unblocked after waiting on that event. To manually reset an event, use the ResetEvent function.

To close an event object, call the CloseHandle function. If the event object is named, Windows CE maintains a use count on the object, and you must make one call to CloseHandle for each call to CreateEvent.

A single thread can specify different event objects in several simultaneous overlapped operations. If this is the case, use one of the multiple-object wait functions to wait for the state of any one of the event objects to be signaled. You can also use event objects in a number of situations to notify a waiting thread of the occurrence of an event. For example, communications devices use an event object to signal their completion.

The following code example shows an application that uses event objects to prevent several threads from reading from a shared memory buffer while a master thread is writing to that buffer. The master thread uses the CreateEvent function to create a manual-reset event object. The master thread sets the event object to nonsignaled when it is writing to the buffer, and it then resets the object to signaled when it has finished writing. The master thread then creates several reader threads and an auto-reset event object for each thread. Each reader thread sets its event object to signaled when it is not reading from the buffer.

VOID ReadThreadFunction (LPVOID lpParam);  // Forward declaration

#define NUMTHREADS 4

HANDLE  hGlobalWriteEvent;
HANDLE  hReadEvents[NUMTHREADS];

void CreateEventsAndThreads (void)
{
  HANDLE hThread;   // Newlycreated thread handle
  DWORD dwThreadID; // Newlycreated thread ID value
  int i;            // For-loop counter

  hGlobalWriteEvent = CreateEvent (NULL,  // No security attributes
                                   TRUE,  // Manual-reset event
                                   TRUE,  // Initial state is signaled
                                   TEXT("WriteEvent"));
                                          // Object name
  if (hGlobalWriteEvent == NULL)
  {
    // Your code to deal with the error goes here.
  }

  for (i = 0; i < NUMTHREADS; i++)
  {
    hReadEvents[i] = CreateEvent (NULL,   // No security attributes
                                  FALSE,  // Auto-reset event
                                  TRUE,   // Initial state is signaled
                                  NULL);  // Object not named
    if (hReadEvents[i] == NULL)
    {
      // Your code to deal with the error goes here.
    }

    hThread = CreateThread (
                  NULL,               // No security attributes
                                      // in Windows CE
                  0,                  // Must be 0 under Windows CE
                  (LPTHREAD_START_ROUTINE) ReadThreadFunction
                  &hReadEvents[i],    // Pass new thread's event handle
                  0,                  // Thread runs immediately
                  &dwThreadID);       // Returned ID value (ignored)

    if (hThread == NULL)
    {
      // Your code to deal with the error goes here.
    }
  }
} // End of CreateEventsAndThreads example code

In the following code example, before the master thread writes to the shared buffer, it uses ResetEvent to set the state of hGlobalWriteEvent, which is an application-defined global variable, to nonsignaled. This blocks the reader threads from starting a read operation. The master thread then uses the WaitForSingleObject function to wait for all reader threads to finish any current read operations. When the loop of calls to WaitForSingleObject is completed, the master thread can safely write to the buffer. After it has finished writing, it sets hGlobalWriteEvent and all the reader-thread events to signaled, which enables the reader threads to resume their read operations. The example does not use the WaitForMultipleObjects function because in Windows CE the fWaitAll flag must be set to FALSE. For more information about wait functions, see “Wait Functions.”

int WriteToBuffer (void)
{
  DWORD dwWaitResult;
  int i;

  // Block all read threads from starting any new operations.
  if (!ResetEvent (hGlobalWriteEvent))
  {
    // Your code to deal with the error goes here.
  }

  // Wait for all of the read events to be signaled (threads done).
  for (i = 0; i < NUMTHREADS; i++)
  {
    dwWaitResult = WaitForSingleObject (hReadEvents[i], INFINITE);
    if (WAIT_OBJECT_0 != dwWaitResult)
    {
      // Your code to deal with the error goes here.
    }
  }

  // Now it is okay to write to the shared buffer, so that code
  // goes here.

  // Put all the events back to signaled (so read threads can run).
  for(i = 0; i < NUMTHREADS; i++)
  {
    if (!SetEvent (hReadEvents[i]))
    {
      // Your code to deal with the error goes here.
    }
  }

  if (!SetEvent (hGlobalWriteEvent))
  {
    // Your code to deal with the error goes here.
  }

  return 1;
} // End of WriteToBuffer example code

In the following code example, before starting a read operation, each reader thread uses the WaitForSingleObject function to wait for the application-defined global variable, hGlobalWriteEvent, and then uses it again to wait for its own read event to be signaled. When the loop of calls to WaitForSingleObject is completed, the reader thread’s auto-reset event has been reset to nonsignaled. This blocks the master thread from writing to the buffer until the reader thread uses the SetEvent function to set the event’s state back to signaled.

VOID ReadThreadFunction (LPVOID lpParam)
{
  DWORD dwWaitResult;
  HANDLE hEvent;
  BOOL bStayInLoop;

  hEvent = * (HANDLE *) lpParam;   // This thread's read event

  bStayInLoop = TRUE;

  while (bStayInLoop)
  {
    // First, wait for the master thread's event.
    WaitForSingleObject (hGlobalWriteEvent, INFINITE);
    if (WAIT_OBJECT_0 != dwWaitResult)
    {
      // Your code to deal with the error goes here.
    }

    // Now, wait for this thread’s event object.
    WaitForSingleObject (hEvent, INFINITE);
    if (WAIT_OBJECT_0 != dwWaitResult)
    {
      // Your code to deal with the error goes here.
    }

    // Now it is okay to read the shared buffer -- the code to do
    // that goes here.  When it is time for the thread to quit, set
    // the bStayInLoop variable to FALSE.

    // Now, signal that this thread is done with the buffer.
    if (!SetEvent (hEvent))
    {
      // Your code to deal with the error goes here.
    }
  }
} // End of ReadThreadFunction example code


Wait Functions

Windows CE supports single-object and multiple-object wait functions that block or unblock a thread based on the signaled or nonsignaled state of the object. The single object function is WaitForSingleObject. The multiple object functions are WaitForMultipleObjects and MsgWaitForMultipleObjects.

The WaitForSingleObject function enables a thread wait on a single object. The object can be a synchronization object, such as an event or mutex, or the object can be a handle to a process and thread. Handles are signaled when their processes or threads terminate. Thus, a process can monitor another process or thread and perform some action based on the status of the process or thread.

The WaitForMultipleObjects and MsgWaitForMultipleObjects functions enable the calling thread to specify an array containing one or more synchronization object handles. These functions return when one of the following events occurs:

  • The state of any of the specified objects is set to signaled or the states of all objects have been set to signaled.
  • The time-out interval elapses. You can set the time-out interval to INFINITE to specify that the wait will not time out.

Windows CE does not support waiting for all objects to be signaled.

The following code example shows how to use CreateEvent to create two event objects. It then uses the two created objects as parameters in the function call to WaitForMultipleObjects. WaitForMultipleObjects does not return until one of the objects is set to signaled.

int EventsExample (void)
{
  HANDLE hEvents[2];
  DWORD dwEvent;
  int i;

  for (i = 0; i < 2; i++)
  {
    hEvents[i] = CreateEvent (NULL,   // No security attributes
                              FALSE,  // Auto-reset event object
                              FALSE,  // Initial state is nonsignaled
                              NULL);  // Unnamed object

    if (hEvents[i] == NULL)
    {
      // Your error handling code goes here.
      MessageBox (NULL, TEXT("CreateEvent error!"),
                  TEXT("Error"), MB_OK);

      // You can use GetLastError to obtain more information.

      return 0;
    }
  }

  // Put code that uses the events here; that is, startup
  // of threads, which will set the events when completed.
  // . . .

  dwEvent = WaitForMultipleObjects (
                      2,            // Number of objects in an array
                      hEvents,      // Array of objects
                      FALSE,        // Wait for any (required
                                    // under Windows CE)
                      INFINITE);    // Indefinite wait

  switch (dwEvent)
  {
    case WAIT_OBJECT_0 + 0:         // First event was signaled
    case WAIT_OBJECT_0 + 1:         // Second event was signaled
      break;

    default:
      // Your error handling code goes here.
      MessageBox (NULL, TEXT("Wait error!"),
                  TEXT("Error"), MB_OK);

      // You can use GetLastError to obtain more information.

      return 0;
  }

  return 1;
} // End of EventsExample code

MsgWaitForMultipleObjects is similar to WaitForMultipleObjects, except that it enables you to specify input event objects in the object handle array. You select the type of input event to wait for in the dwWakeMask parameter. MsgWaitForMultipleObjects does not return if there is unread input of the specified type in the queue. It returns only when new input arrives.

For example, a thread can use MsgWaitForMultipleObjects with its dwWakeMask parameter set to QS_KEY. This blocks its execution until the state of a specified object has been set to signaled and there is keyboard input available in the thread’s input queue. The thread can use the GetMessage or PeekMessage function to retrieve the input.

The following code example shows how to use MsgWaitForMultipleObjects in a message loop. The loop continues until a WM_QUIT message appears in the queue. The dwWakeMask parameter is set to QS_ALLINPUT so that all messages are checked.

int MessageLoop (
      HANDLE* lphObjects,   // Handles that need to be waited on
      int     iObjCount )   // Number of handles to wait on
{
  while (TRUE)
  {
    // Block-local variables
    DWORD dwResult;
    MSG msgCurrent;

    while (PeekMessage (&msgCurrent, NULL, 0, 0, PM_REMOVE))
    {
      if (WM_QUIT == msgCurrent.message)
        return 1;

      DispatchMessage (&msgCurrent);
    }

    dwResult = MsgWaitForMultipleObjects (iObjCount, lphObjects,
                              FALSE, INFINITE, QS_ALLINPUT);

    if (dwResult == DWORD(WAIT_OBJECT_0 + iObjCount))
    {
      // Some input was received -- go around loop again
      continue;
    }
    else
    {
      // Check for errors and handle them here. If not an error,
      // write code which processes the result here.  Be sure to
      // create code that provides some way to get out of the loop!

      // The index for the signaled is
      // (dwResult - WAIT_OBJECT_0).
    }
  }
} // End of MessageLoop example code

Note Be careful when using the wait functions and code that directly or indirectly creates windows. If a thread creates any windows, it must process messages. Message broadcasts are sent to all the windows in the system. If you have a thread that uses a wait function with no time-out interval, the system will deadlock. One example of code that indirectly creates a window is the Component Object Model (COM) CoInitialize function. If you have a thread that creates windows, use MsgWaitForMultipleObjects rather than the other wait functions.


Interlocked Functions

Interlocked functions synchronize access to a variable that is shared by multiple threads. Their purpose is to prevent a thread from being preempted when it is in the middle of incrementing or checking a variable. The threads of different processes can use these functions as long as their variables share memory. Windows CE supports three interlocked functions: InterlockedIncrement, InterlockedDecrement, and InterlockedExchange. The following table describes the tasks that you can perform with each function.

To
Call
Increment a shared variable and check the resulting value | InterlockedIncrement
Decrement a shared variable and check the resulting value | InterlockedDecrement
Exchange the values of specified variables | InterlockedExchange
Exchange the values of specified variables only if one of the variables is equal to a specified value | InterlockedTestExchange


Interprocess Synchronization

All processes protect against the casual exchange of data; however, occasionally two processes may need to communicate with each other. One method that enables one process to communicate with another is called interprocess synchronization.

Because multiple processes can have handles to the same event or mutex object, these objects can be used to accomplish interprocess synchronization. The process that creates an object can use the handle returned by the CreateEvent or CreateMutex function. Other processes can open a handle to the object by using its name in another call to the CreateEvent or CreateMutex function.

Named objects provide a way for processes to share object handles. The name specified by the creating process is limited to the number of characters that are defined by MAX_PATH. It can include any character except the backslash (\) path-separator character. Once a process has created a named event or mutex object, other processes can use the name to call the appropriate function, either CreateEvent or CreateMutex, to open a handle to the object. Name comparison is case-sensitive.

The names of event and mutex objects share the same name space. If you specify a name that is in use by an object of another type when you create an object, the function succeeds, but GetLastError returns ERROR_ALREADY_EXISTS. To avoid this error, use unique names and be sure to check function return values for duplicate-name errors.

The following code example shows how to use object names by creating and opening named objects. The first process uses CreateMutex to create the mutex object. Note that the function succeeds even if there is an existing object with the same name.

HANDLE MakeMyMutex (void)
{
  HANDLE hMutex;

  hMutex = CreateMutex (
            NULL,                             // No security attributes
            FALSE,                            // Initially not owned
            TEXT("MutexToProtectDatabase"));  // Name of mutex object

  if (NULL == hMutex)
  {
    // Your code to deal with the error goes here.
  }

  return hMutex;
} // End of MakeMyMutex example code


Synchronization and Device I/O

Windows CE enables you to synchronously perform the WriteFile, ReadFile, and WaitCommEvent functions.

Windows CE does not support the overlapped I/O features of Windows NT. The lpOverlapped parameter to ReadFile or WriteFile must be NULL. Therefore, Windows CE cannot signal the event that is passed in when the I/O operation is completed. However, Windows CE does support simultaneous synchronous or asynchronous calls to ReadFile or WriteFile made by separate threads that are overlapped in time; this is not supported in Windows NT.


Managing Program Memory

One of the main sections of random access memory (RAM) in a Windows CE–based device is called the program memory. With program memory, an application dynamically allocates memory for strings, structures, and other data. These allocations are for the lifetime of the application and are deallocated while or after the application completes its task. As such, program memory fills the same function that RAM does in a desktop computer.

Windows CE uses a virtual memory system to manage and allocate program memory. Virtual memory is a style of memory management that separates how an application requests memory from the way that Windows CE provides memory. The application requests a block of virtual memory, and Windows CE maps that virtual memory address to a physical address. Subsequent memory allocations are not necessarily mapped to subsequent physical memory locations. Rather, Windows CE maps to whatever physical memory is available, regardless of the location of the memory on the device. The following illustration shows how virtual memory maps to physical memory.

Virtual memory keeps the application from having to manage memory allocation. Because the application uses virtual memory addresses instead of physical memory addresses, the application sees contiguous memory. Windows CE has a total of 4 gigabytes (GB) of virtual address space. Of the 4-GB virtual address space, Windows CE reserves 32 slots of 32 megabytes (MB) to run processes. Each process receives one 32-MB slot for dynamic memory requirements, including process code segments, loaded dynamic-link libraries (DLLs), thread stacks, and created heaps. The rest of the virtual address space is reserved for the Windows CE operating system (OS). Windows CE uses these memory addresses for tasks such as memory mapped files and large virtual allocations, neither of which come out of the process slots.

Windows CE allocates virtual memory in pages. A page is a unit of memory that is either 1,024 or 4,096 bytes long. Windows CE marks each page in memory as free, reserved, or committed:

  • A free page can be allocated to any application at any time.
  • A reserved page is held for an application, but has not yet been mapped to physical memory.
  • A committed page has been allocated to physical memory and is currently in use by an application.

Windows CE allocates pages along 64-kilobyte (KB) regions. Any function that reserves virtual memory pages automatically rounds up to the nearest 64-KB region. You can allocate memory more efficiently if you reserve virtual memory blocks in 64-KB regions. After you reserve the region, you can then go back and commit pages within the region, as needed.

Before allocating memory, use the GetSystemInfo and GlobalMemoryStatus functions to return information about your Windows CE–based device. GetSystemInfo returns information such as which microprocessor a device has, the memory page size, and the virtual addresses that are available to your application. GlobalMemoryStatus returns general information about memory allocation on the device. You can also use the GetStoreInformation function to determine how much memory is allocated to the system volume.

You can use the virtual memory application programming interface (API) to directly allocate virtual memory. The functions in the virtual memory API include LocalAlloc, LocalFree, LocalReAlloc, VirtualFree, VirtualProtect, and VirtualQuery. In addition, Windows CE uses the virtual memory in other memory allocations. Both the heap and the stack indirectly use virtual memory in their API sets. The fourth type of memory, the static data block, does not have a direct virtual memory address; rather, you place information in the static data block before compiling your application.


Using Virtual Memory

The virtual memory API directly allocates virtual memory. Windows CE also uses the virtual memory API to allocate memory for the heap and the stack. Use the virtual memory API when you need to reserve and allocate large blocks of memory for an application. The advantage of using virtual memory is that virtual memory does not fragment: Windows CE always allocates an integral number of pages. However, because Windows CE manages virtual memory in 64-KB units, you must ensure that you use all of the memory efficiently. Typically, an application wastes half of a virtual memory page per allocation. Also, Windows CE requires a slight overhead for managing the memory mapping. Finally, you must remember the reserved and committed status of each page. Therefore, if you do not think that you can use most of the 64-KB unit in your virtual memory allocation, use a heap instead.

To allocate and deallocate virtual memory

  1. Call the VirtualAlloc function to reserve and commit virtual memory.

    You can also use VirtualAlloc to reserve virtual memory and then have Windows CE map the memory address directly to physical memory when the pages are actually used by the application.

  2. Use the virtual memory that is allocated to your application.
  3. If necessary, call the VirtualQuery function to determine the read/write status of a virtual memory page.
  4. If necessary, call the VirtualProtect function to alter the access rights on committed pages.
  5. Call the VirtualFree function to decommit or free virtual memory and return the memory to Windows CE.

    Decomitting a page unmaps the page from physical memory but keeps the virtual addresses reserved for the application.

The following code example shows how to simultaneously reserve and commit virtual memory.

#define THIRTY_K  (30 * 1024) // Definition example

LPVOID lpMemory = NULL;       // Pointer to a region of memory
LPVOID lpPage = NULL;         // Pointer to a page of memory

lpMemory = VirtualAlloc (NULL, THIRTY_K,
                         MEM_RESERVE | MEM_COMMIT,
                         PAGE_READWRITE);

if (NULL == lpMemory)
{
  // Your error-handling code goes here. You can use the
  // GetLastError function to obtain more information.
}

// Your code that uses the just-allocated memory goes here.
// For example:     LPBYTE MyPointer = (LPBYTE)lpMemory;
//                  memset (MyPointer, 0, THIRTY_K);
//                  return MyPointer;
// . . .

// When you are finished with the memory, you must release it.
if (!VirtualFree (lpMemory, 0, MEM_RELEASE))
{
  // Your error-handling code goes here. You can use the
  // GetLastError function to obtain more information.
}

// Remember to clear the pointer to prevent re-use.
lpMemory = NULL;

The following code example shows how to reserve a bundle of memory and then commit that memory, as needed.

lpMemory = VirtualAlloc (NULL, THIRTY_K,
                         MEM_RESERVE, PAGE_READWRITE);

if (NULL == lpMemory)
{
  // Your error-handling code goes here. You can use the
  // GetLastError function to obtain more information.
}

// Determine the page size on this system.
SYSTEM_INFO siSystemInfo;

GetSystemInfo (&siSystemInfo);

// Commit the first page of the just-reserved block.
// (To commit more than one page, multiply the dwPageSize
// parameter used below by the number of pages to commit.)
lpPage = VirtualAlloc (lpMemory,        // Base for this commit
                       siSystemInfo.dwPageSize,
                                        // number of bytes
                                        // to commit
                       MEM_COMMIT,      // Selects the operation
                       PAGE_READWRITE); // Selects permissions

if (NULL == lpPage)
{
  // Your error-handling code goes here. You can use the
  // GetLastError function to obtain more information.
}

// To commit additional pages, add the appropriate
// offset to 'lpMemory' and call VirtualAlloc again.
// For example, to commit the tenth page:
// lpPage = VirtualAlloc ((LPBYTE)lpMemory + 9 * dwPageSize,
// and so on.
// . . .

The following code example shows how to use the VirtualQuery function to examine the characteristics of a page of virtual memory that you previously allocated.

DWORD dwResult;
MEMORY_BASIC_INFORMATION mbiMemory;

// Clear the results structure.
memset (&mbiMemory, 0, sizeof(MEMORY_BASIC_INFORMATION));

dwResult = VirtualQuery (lpPage,        // Page to examine
                         &mbiMemory,    // Structure for results
                         sizeof(MEMORY_BASIC_INFORMATION));

if (sizeof(MEMORY_BASIC_INFORMATION) != dwResult)
{
  // Your error-handling code goes here.
}

// Here, write code to use the contents of the mbiMemory
// structure that was filled in by the above call.
// For example: if (mbiMemory.Protect != PAGE_READWRITE)
// and so on.
// . . .

The following code example shows how to use VirtualProtect to change the characteristics of a page of virtual memory that you previously allocated.

DWORD dwOldProtect;
DWORD dwSize = 2048;  // Or, set this to 'siSystemInfo.dwPageSize'
                      // (See the above code for how to get this.)

if (!VirtualProtect (lpPage,        // Beginning of the set of
                                    // pages to change
                     dwSize,        // Length, in bytes, of the
                                    // set of pages to change
                     PAGE_READONLY, // What to change it to
                     &dwOldProtect  // Place to store the old setting
                     ))
{
  // Your error-handling code goes here. You can use the
  // GetLastError function to obtain more information.
}

The following code example shows how to decommit a page by using VirtualFree. Presumably, you could recommit the page later with different attributes.

if (!VirtualFree (lpPage, 0, MEM_DECOMMIT))
{
  // Your error-handling code goes here. You can use the
  // GetLastError function to obtain more information.
}


Using the Local Heap

A heap is a region of reserved virtual memory space that Windows CE manages for your application. The original heap is called the local heap. Unlike VirtualAlloc, you can allocate memory on a heap in 4-byte or 8-byte units, depending on your CPU type. In addition to being more efficient for small sizes, the heap can be used to avoid having to deal with the different sizes of memory pages that different microprocessors support. The heap also has no set limit. However, any heap allocations greater than 192 KB are fulfilled with a call to VirtualAlloc. If you attempt to allocate more memory than Windows CE originally set aside for a heap, the system attempts to find unreserved memory. Like virtual memory mapping, this memory might not be physically located next to the original heap. Also, because Windows CE allocates memory in fixed blocks, the heap might become fragmented over time.

Use the local heap when you need to allocate specific sizes of memory on a regular basis. Because Windows CE reclaims a memory page only if that page is totally free, be sure that you deallocate memory blocks correctly when you are using the heap. This becomes an issue with applications that run for a long time, such as applications that are designed for the Palm-size PC device category.

To allocate and deallocate memory using the local heap

  1. Call the LocalAlloc function with the size of the memory block passed in the uBytes parameter.

    LocalAlloc returns a handle to the virtual memory block that is allocated to your application. Windows CE also maps the virtual memory block to a physical memory block at this time.

  2. Use the memory allocated to your application.
  3. If necessary, call the LocalSize and LocalReAlloc functions to reallocate the local heap memory.

    If you need to increase the size of a block, call LocalSize to determine that the block contains enough space. Then, call LocalReAlloc to either add memory to the top of the allocation or to move the block to a larger area.

  4. Call the LocalFree function to return the memory to Windows CE.

Note The HeapAlloc function can also allocate memory outside of the local heap by using the handle that is returned by the GetProcessHeap function.

The following code example shows how to allocate a set number of bytes from the local heap.

LPBYTE pbMyPointer = NULL;
HLOCAL hlHeapMemory = NULL;
HLOCAL hlHeapMem2 = NULL;
UINT uiSize;

hlHeapMemory = LocalAlloc (LPTR,    // FIXED and ZEROINIT
                           42);     // Number of bytes

if (hlHeapMemory == NULL)
{
  // Your error-handling code goes here. You can use
  // the GetLastError function to obtain more information.
}

// The return value is actually the address of the memory.
pbMyPointer = (LPBYTE)hlHeapMemory;

// Your code to use the memory goes here.
// SomeFunctionCall (pbMyPointer, and so on)
// . . .

The following code example shows how to check the size of a piece of the local heap by using LocalSize.

uiSize = LocalSize (hlHeapMemory);
if (uiSize == 0)
{
  // Your error-handling code goes here. You can use
  // the GetLastError function to obtain more information.
}

The following code example shows how to change the size of a piece of local heap by using LocalReAlloc.

hlHeapMem2 = LocalReAlloc (hlHeapMemory,  // Existing handle
                           100,           // New size, in bytes
                           0);            // Options (none)

if (hlHeapMem2 == NULL)
{
  // Your error-handling code goes here. You can use
  // the GetLastError function to obtain more information.
}

// The return value is actually the address of the memory.
pbMyPointer = (LPBYTE)hlHeapMem2;

The following code example shows how to free memory from the local heap.

hlHeapMem2 = LocalFree (hlHeapMem2);

// The pointer returned should be NULL.
if (hlHeapMem2 != NULL)
{
  // Your error-handling code goes here. You can use
  // the GetLastError function to obtain more information.
}


Using a Separate Heap

Instead of adding more size to the local heap, you might need to create a series of separate heaps. For example, a word processor might have a separate heap associated with each text file. Because you can delete separate heaps, fragmentation is not as much of an issue.

To allocate and deallocate memory on a separate heap

  1. Call the HeapCreate function.

    HeapCreate returns a handle. Use this handle to reference which heap you are using in the following steps.

  2. Call HeapAlloc to allocate memory from the heap to your application.

    Windows CE does not place any limits on the size of a separate heap. As long as you have virtual and physical memory addresses available, you can allocate memory from the separate heap.

  3. Use the memory that is allocated to your application.
  4. If necessary, call HeapSize and HeapReAlloc to reallocate the separate heap memory.

    If you need to increase the size of a block, call HeapSize to determine if the block contains enough space. Then, call HeapReAlloc to either add memory to the top of the allocation or to move the block to a larger area.

  5. Once you are done, call the HeapFree function to deallocate the memory back to Windows CE.

    Like the local heap, Windows CE deallocates a page only if all of the blocks within that page are also deallocated. This may result in fragmentation within the separate heap.

  6. Once you are done with the heap, call the HeapDestroy function to return all of the heap memory to Windows CE.

    You do not have to call HeapFree on all blocks within the separate heap before you call HeapDestroy.

The following code example shows how to create a new heap.

LPBYTE pbMyPointer = NULL;
HANDLE hMyNewHeap = NULL;
LPVOID lpHeapMemory = NULL;
LPVOID lpHeapMem2 = NULL;
UINT uiSize;
BOOL bResult;

hMyNewHeap = HeapCreate (0,     // *do* serialize
                         35000, // 35,000 bytes wanted
                         0);    // Unsupported in Windows CE

The following code example shows how to allocate memory from the heap even when the heap is very small, by using HeapAlloc.

lpHeapMemory = HeapAlloc (hMyNewHeap,       // Specify which heap
                          HEAP_ZERO_MEMORY, // Zero the memory
                          42);              // Number of bytes
if (lpHeapMemory == NULL)
{
  // Your error-handling code goes here. You can use
  // the GetLastError function to obtain more information.
}

// The return value is actually the address of the memory.
pbMyPointer = (LPBYTE)lpHeapMemory;

// Your code to use the memory goes here.
// SomeFunctionCall (pbMyPointer, and so on)
// . . .

The following code example shows how to check the size of part of the heap by using HeapSize.

uiSize = HeapSize (hMyNewHeap,    // Specify the heap
                   0,             // No flags
                   lpHeapMemory); // Specify which memory
if (uiSize == 0)
{
  // Your error-handling code goes here. You can use
  // the GetLastError function to obtain more information.
}

The following code example shows how to change the size of part of the heap by using HeapReAlloc.

lpHeapMem2 = HeapReAlloc (hMyNewHeap,   // Specify which heap
                          0,            // Options (none)
                          lpHeapMemory, // Existing memory
                          100);         // New size, in bytes
if (lpHeapMem2 == NULL)
{
  // Your error-handling code goes here. You can use
  // the GetLastError function to obtain more information.
}

// The return value is actually the address of the memory.
pbMyPointer = (LPBYTE)lpHeapMem2;

The following code example shows how to free memory from the heap by using HeapFree.

bResult = HeapFree (hMyNewHeap,       // Specify which heap
                    0,                // Options (none)
                    lpHeapMem2);      // Specify what to free
if (!bResult)
{
  // Your error-handling code goes here. You can use
  // the GetLastError function to obtain more information.
}

// Always zero the caller’s pointer after freeing memory.
lpHeapMem2 = NULL;

The following code example shows how to destroy the heap by using HeapDestroy.

bResult = HeapDestroy (hMyNewHeap);

if (!bResult)
{
  // Your error-handling code goes here. You can use
  // the GetLastError function to obtain more information.
}


Using the Stack

The stack is the storage area for variables that are referenced in a function. Windows CE allocates memory for a variable from the stack and deallocates the memory after the function call is complete. When a thread or process begins, Windows CE allocates one page of memory to the stack for that thread. Each thread has a stack, and each stack contains up to 60 KB of data: Windows CE reserves 58 KB for the stack, and reserves the final 2 KB for stack overflow control.

Note Exceeding the 60-KB limit for a stack causes a system-access violation that shuts down your application.

By default, you call on the stack each time that you declare a variable. You can also allocate memory from the stack by using the HeapAlloc function.


Using the Static Data Block

The static data block is a block of memory that Windows CE loads with an application. This block contains strings, buffers, and other static values that the application references throughout the life of the application. Windows CE allocates two sections for static data: one for read/write data and one for read-only data. Because Windows CE allocates the static data blocks one page at a time, you can often find unused memory between the end of the static data and the end of the memory page. Use a utility such as DumpBin.exe, or the Remote Memory viewer, to determine the size of your application’s static data block. Use this information to arrange your declared data as efficiently as possible.

One way to use the unused memory in the static data blocks is to assign the unused memory to a buffer. Use this buffer in your application instead of creating a buffer dynamically. When assigning extra memory to a buffer, be sure to leave at least 64 KB free for the loader. Windows CE allocates another page for the loader if it cannot find enough space in the current static data page.

Another technique is to place a note in your code to remind you to look at your memory use each time that you add more data. For example, Windows CE might assign an entire memory page to your application when you only need room for a single variable. By modifying your application, you can save this page and thus lower your memory requirements.

You can also reduce the size of your data blocks by declaring data in the read/write sections as “const.” Windows CE then moves all “const” data to the read-only section. Windows CE places all constant data items in the read-only data block instead of in the read/write data block. However, if declaring all of your constant data forces Windows CE to allocate another page, you can move some of that data into the read/write block.


Identifying Low-Memory Situations

No matter how efficiently you allocate memory and how efficiently your application uses RAM, your device might run low on memory. At a programming level, a low-memory situation can manifest itself to the application in the following ways:

  • A call to VirtualAlloc returns a 0, indicating that the function failed to allocate memory.
  • Either LocalAlloc or HeapAlloc returns a 0, indicating that an attempt to increase the size of a heap has failed.
  • Windows CE returns a stack fault error to your application, indicating that a stack allocation has failed.

Note Low-memory situations may be caused indirectly. For example, a call to the CreateWindow function can cause these types of memory failures.

To avoid the problems associated with low memory, Windows CE constantly monitors the amount of memory that is available and tries to prevent low-memory situations from occurring. It does this in several ways:

  • When an application attempts to allocate memory, Windows CE filters the request. Filtering prevents a single application from using all of the available memory with one large allocation by lowering the maximum allocation limit.
  • When Windows CE enters a low-memory situation, it lowers the memory limit further.

To recognize a low-memory situation, monitor the return values for your memory allocation functions. The platform you program on may also have additional functions to help you deal with low-memory situations. For more information, see the platform-specific documentation.


Sharing Memory-Mapped Objects Between Processes

You can use a memory-mapped object to share data between processes. However, do not create an unnamed object and pass a memory pointer to the different processes: one process can close the unnamed object without informing the other process. To avoid this memory error, Windows CE supports naming the unnamed object. Instead of passing a pointer to the object, you can now pass the name of the object. The other process then accesses the object through its name. Accessing the object through the name informs Windows CE which processes have access to the object. Windows CE then deletes the object only when both processes have closed the object.

To share data between processes by using memory-mapping

  1. Call the CreateFileMapping function to create the memory object, using the lpName parameter to pass in a name for the memory-mapped object.
  2. Pass the name of the memory-mapped object to the process that you want to communicate with.
  3. Call CreateFileMapping in the second process, using the name of the object that you passed with the first object.

    The name of the memory-mapped object is global. When the second process calls CreateFileMapping, Windows CE passes back the handle to the original object.

  4. Use the MapViewOfFile function in either process to gain access to the memory-mapped object.


Accessing the Object Store, Database, and Registry

A Windows CE–based device has two types of memory: read-only memory (ROM) and random access memory (RAM). ROM typically stores the Windows CE operating system (OS) and any bundled applications that are included on your device. While some ROM-based applications must be loaded into RAM in order to work properly, many applications are designated as execute in place (XIP). While an XIP application can run from ROM, any data that the user creates with the application must be stored in RAM.


Using the File System

The Windows CE file system supports files that are stored in RAM, ROM, and installed file systems. Like Windows-based platforms, Windows CE employs handle-based file access. The CreateFile function returns a handle that references the created or opened file. The read, write, and information functions all use that handle to determine which file to act on. The read and write functions also use a file pointer to determine where in the file they read and write.

Windows CE uses a variety of techniques to simplify memory management and to reduce memory overhead. First, Windows CE does not use the current directory concept. Instead, all references to an object are given in the full path name. Further, Windows CE automatically compresses all files in the object store; therefore, no file has a flag to indicate compression. Of course, a flag does exist that distinguishes between a file in ROM and a file in RAM.

One of the sections of RAM on a Windows CE–based device is called the object store. The object store is where the directory, databases, registry, and file objects in volatile memory exist. The object store can be up to 16 megabytes (MB) in size. On Windows CE version 2.0 and earlier, a file can be up to 4 MB in size. Directories, databases, the registry, and files on Windows CE 2.10 and later are not limited to the 4-MB size restriction. Windows CE stores each database in the object store in a separate database volume. A database volume is a file that contains all of the data that is necessary for a database. Like the object store, a database volume can be up to 16 MB. All of the data in the object store is transactioned, which protects against data loss. If a Windows CE–based device loses power during a data transaction, Windows CE reverts all partial operations to the last known good state.

In addition to the object store, a user can install a file system such as the file allocation table (FAT) file system. An installed file system can provide access to a PC Card or to other external storage devices. You can divide an external storage device into multiple volumes, each of which is mounted separately. Each mounted volume is visible to the user as a folder in the root directory of the installed file system. Like the object store, a mounted volume is limited to 16 MB, and is transactioned to avoid data loss. While you can back up data to an external storage device, the working registry and RAM file system can only exist in the object store.

Note For programming purposes, Windows CE considers the object store as a special type of volume.


Using an Object Identifier

Windows CE assigns each object that is created within a volume a unique Windows CE object identifier. Each Windows CE object identifier is unique within a given volume, but not across multiple volumes. Windows CE also gives all volumes a Windows CE globally unique identifier (CEGUID). The object store also has a CEGUID, which is predefined. The most common use for a Windows CE object identifier is to access object data, such as a database record, and to obtain object data. Use the CEGUID in conjunction with the Windows CE object identifier to uniquely reference each record and object. The following table shows where to obtain the Windows CE object identifier for the various types of objects in the object store.

Object type
Where to obtain the Windows CE object identifier
Directory or file | The dwOID member of the WIN32_FIND_DATA structure, which is returned by the FindFirstFile and FindNextFile functions. Also in the dwOID member of the BY_HANDLE_FILE_INFORMATION structure, which is returned by the GetFileInformationByHandle function.
Database | The return value of the CeCreateDatabaseEx or CeFindNextDatabaseEx function
Database record | The return value of the CeSeekDatabase, CeReadRecordPropsEx, or CeWriteRecordProps function
Mounted database volume | The CeMountDBVol and CeEnumDBVolume functions return the CEGUID of the mounted database volume.

Use the CeOidGetInfoEx function to return object data associated with the Windows CE object identifier. This function returns object data in a CEOIDINFO structure. The CEOIDINFO wObjType member contains a flag indicating the object type, such as OBJTYPE_DATABASE for a database object, and also identifies which object structure to use to access the data. CEOIDINFO also contains a member that returns data on either a file, directory, database, or database record. These values correspond to the type of object that is indicated by the wObjType member. For example, using CeOidGetInfoEx on a mounted database volume returns the database name, type identifier, number of records, database size, and sort order in a CEDBASEINFO structure, as well as the OBJTYPE_DATABASE value.

The following code example shows how to retrieve data from the object store or mounted volume by using the Windows CE object identifier.

void UsingCeOidGetInfo (void)
{
  CEOID CeOid;            // Object identifier
  PCEGUID pceguid;        // GUID of the mounted volume
  CEOIDINFO CeObjectInfo; // Structure into which to retrieve
                          // object data
  TCHAR szMsg[200];       // String to use with object data

  // Assign to pceguid the CEGUID of the mounted volume
  // on which the object resides.
  // ...

  // Assign to CeOid the OID of the object about which
  // you want to get data.
  // ...

  if (CeOidGetInfoEx (pceguid, CeOid, &CeObjectInfo))
  {
    switch (CeObjectInfo.wObjType)
    {
      case OBJTYPE_FILE:
        wsprintf (szMsg, TEXT("Object is a file: %s"),
                  CeObjectInfo.infFile.szFileName);
        break;

      case OBJTYPE_DIRECTORY:
        wsprintf (szMsg, TEXT("Object is a directory: %s"),
                  CeObjectInfo.infDirectory.szDirName);
        break;

      case OBJTYPE_DATABASE:
        wsprintf (szMsg, TEXT("Object is a database: %s"),
                  CeObjectInfo.infDatabase.szDbaseName);
        break;

      case OBJTYPE_RECORD:
        wsprintf (szMsg, TEXT("Object is a record"));
        break;

      case OBJTYPE_INVALID:
        wsprintf (szMsg,
                  TEXT("The object store does not contain a valid ")
                  TEXT("object that has this object identifier."));
        break;

      default:
        wsprintf (szMsg, TEXT("Object is of unknown OBJTYPE"));
        break;
    }
  }
  else
  {
    // Your error-handling code goes here.

    if (GetLastError () == ERROR_INVALID_HANDLE)
      wsprintf (szMsg, TEXT("Invalid Object ID"));
    else
      wsprintf (szMsg, TEXT("Unknown error"));
  }
} // End of UsingCeOidGetInfo code


Determining Available Disk Space

Memory is usually at a premium on a Windows CE–based device. Use the GetDiskFreeSpaceEx function to determine organizational data about a file system partition, including the number of bytes per sector, the number of sectors per cluster, the number of free clusters, and the total number of clusters. GetDiskFreeSpaceEx works on any file system, including installed file systems. You can also use the GetStoreInformation function to determine the current size of the object store, and how much free memory is available in the object store.


Creating and Opening a File or Directory

Call the CreateFile function to create a new file or to open an existing file. When you call CreateFile, Windows CE searches for a file in the directory that you specify. Depending on the parameters that you pass into CreateFile, Windows CE either opens the file, creates a new file, or truncates the old file. When you open a file, you set the read/write access for that file. Further, you also determine if Windows CE can share the file. If you choose not to share the file, Windows CE enables another call to CreateFile on the file only after your application closes the file. Once the file is opened or created, Windows CE assigns a unique file handle to the file. An application can use the file handle in functions that read from, write to, or describe the file. Note that files cannot be created with the overlapped attribute set.

Call the CreateDirectory function, with the full path of the new directory in the lpPathName parameter, to create a directory. The lpSecurityAttributes parameter is a holdover from Windows NT, and it should be set to NULL.

The following code example shows how to use CreateFile to open an existing file for reading.

void OpenFileExample (void)
{
  HANDLE hFile;

  hFile = CreateFile (TEXT("\MYFILE.TXT"),    // Open MYFILE.TXT
                      GENERIC_READ,           // Open for reading
                      FILE_SHARE_READ,        // Share for reading
                      NULL,                   // No security
                      OPEN_EXISTING,          // Existing file only
                      FILE_ATTRIBUTE_NORMAL,  // Normal file
                      NULL);                  // No template file

  if (hFile == INVALID_HANDLE_VALUE)
  {
    // Your error-handling code goes here.
    return;
  }
} // End of OpenFileExample code


Reading from and Writing to a File

Once you open or create a file with CreateFile, use the returned file handle to gain access to the file. Windows CE maintains a file pointer to read and write data between a file and a buffer. When you open a file for the first time, Windows CE places the file pointer at the beginning of the file. Windows CE advances the file pointer after reading or writing each byte between the buffer and the file.

Accessing the data buffer while Windows CE is performing a read or write operation with that buffer might lead to data corruption. Therefore, be sure that you do not modify a data buffer currently in use by a read or write operation. If you use multiple threads or semaphores, be sure that one thread does not access the data buffer while another thread performs a read/write operation.


Reading from a File

Use the ReadFile function to read data from a file. ReadFile uses the returned handle from CreateFile in the hFile parameter to identify the file from which to read. ReadFile begins reading at the location that is pointed to by the file pointer, and it continues reading up to the number of bytes that is specified in the nNumberOfBytesToRead parameter or to the end of the file. If ReadFile finds the end of the file, it does not return an error value. Instead, Readfile returns as much data as it reads up until the end of the file. Therefore, be sure to check the value returned in lpNumberOfBytesRead against the value passed to the function in nNumberOfBytesToRead. ReadFile returns the read data through the buffer that is pointed to in the lpBuffer parameter. During the copying process, Readfile does not perform any formatting or parsing; ReadFile reads the data exactly as the data exists in the file.

Note Windows CE does not support simultaneous read/write operations. Further, ReadFile does not support asynchronous read operations nor does it support read operations through a socket.


Writing to a File

Use the WriteFile function to place data into a file. Like ReadFile, WriteFile uses the handle that is returned from CreateFile in the hFile parameter to identify which file to write to. WriteFile then copies a specified number of bytes from the buffer that is pointed to by the lpBuffer parameter into the specified file. CreateFile begins placing the buffered data at the location within the file that is pointed to by the file pointer. Like ReadFile, WriteFile does not perform any formatting on the data; WriteFile writes the data exactly as the data exists in the buffer.

Note Windows CE does not support simultaneous read/write operations. Further, WriteFile does not support asynchronous write operations.

When writing to a file, use the FlushFileBuffers function to be sure that all data is properly written from the data buffer to the file. This situation is common when writing to a file on an installed file system, such as the FAT file system.

If you want to truncate the file when you close it, call the SetEndOfFile function. SetEndOfFile truncates the file at the current location of the pointer, and then it closes the file.


Setting the File Pointer in a File

Use the SetFilePointer function to move the file pointer a specified number of bytes. Like ReadFile and WriteFile, SetFilePointer uses the handle that is returned by CreateFile in the hFile parameter to identify the file in which to move the pointer. The number of bytes that are described by the lDistanceToMove and lpDistanceToMoveHigh parameters can be relative to the beginning of the file or relative to the current position of the pointer. Set which process you prefer by using the dwMoveMethod parameter. If you pass in a positive number of bytes, SetFilePointer moves the pointer toward the end of the file. If you pass in a negative number, SetFilePointer moves the file pointer toward the beginning of the file.


Read/Write Example

The following code example shows how to append one file to the end of another file. The example uses the CreateFile function to open two files: One.txt for reading and Two.txt for writing. Then the example uses ReadFile and WriteFile to append the contents of One.txt to the end of Two.txt by reading and writing 4-kilobyte (KB) blocks.

void AppendExample (void)
{
  HANDLE hFile, hAppend;
  DWORD dwBytesRead, dwBytesWritten, dwPos;
  char buff[4096];
  TCHAR szMsg[1000];

  // Open the existing file.

  hFile = CreateFile (TEXT("\ONE.TXT"),       // Open One.txt.
                      GENERIC_READ,           // Open for reading
                      0,                      // Do not share
                      NULL,                   // No security
                      OPEN_EXISTING,          // Existing file only
                      FILE_ATTRIBUTE_NORMAL,  // Normal file
                      NULL);                  // No template file

  if (hFile == INVALID_HANDLE_VALUE)
  {
    // Your error-handling code goes here.
    wsprintf (szMsg, TEXT("Could not open ONE.TXT"));
    return;
  }

  // Open the existing file, or if the file does not exist,
  // create a new file.

  hAppend = CreateFile (TEXT("\TWO.TXT"),       // Open Two.txt.
                        GENERIC_WRITE,          // Open for writing
                        0,                      // Do not share
                        NULL,                   // No security
                        OPEN_ALWAYS,            // Open or create
                        FILE_ATTRIBUTE_NORMAL,  // Normal file
                        NULL);                  // No template file

  if (hAppend == INVALID_HANDLE_VALUE)
  {
    wsprintf (szMsg, TEXT("Could not open TWO.TXT"));
    CloseHandle (hFile);            // Close the first file.
    return;
  }

  // Append the first file to the end of the second file.

  do
  {
    if (ReadFile (hFile, buff, 4096, &dwBytesRead, NULL))
    {
      dwPos = SetFilePointer (hAppend, 0, NULL, FILE_END);
      WriteFile (hAppend, buff, dwBytesRead,
                 &dwBytesWritten, NULL);
    }
  }
  while (dwBytesRead == 4096);

  // Close both files.

  CloseHandle (hFile);
  CloseHandle (hAppend);

  return;
} // End of AppendExample code


Reading and Writing File Attributes

Windows CE provides a variety of functions to identify and modify the status of a file. You can set the parameters of a file when you first create the file with CreateFile. After you create the file, you can view the file attributes with the GetFileAttributes function and you can modify the file attributes with the SetFileAttributes function. The following table describes what file attributes you can interact with by using the GetFileAttributes, SetFileAttributes, and CreateFile functions.

Flag

CreateFile
GetFile
attributes
SetFile
attributes
FILE_ATTRIBUTE_ARCHIVE | X | X | X
FILE_ATTRIBUTE_COMPRESSED | X
FILE_ATTRIBUTE_DIRECTORY | X
FILE_ATTRIBUTE_ENCRYPTED | X
FILE_ATTRIBUTE_HIDDEN | X | X | X
FILE_ATTRIBUTE_INROM | X
FILE_ATTRIBUTE_NORMAL | X | X | X
FILE_ATTRIBUTE_OFFLINE | X | X
FILE_ATTRIBUTE_READONLY | X | X | X
FILE_ATTRIBUTE_REPARSE_POINT | X
FILE_ATTRIBUTE_ROMMODULE | X
FILE_ATTRIBUTE_SPARSE_FILE | X
FILE_ATTRIBUTE_SYSTEM | X | X | X
FILE_ATTRIBUTE_TEMPORARY | X | X | X
FILE_FLAG_WRITE_THROUGH | X
FILE_FLAG_RANDOM_ACCESS | X
FILE_FLAG_SEQUENTIAL_SCAN | X
FILE_ATTRIBUTE_ROMSTATICREF | X

You can also use the GetFileSize function to return the size of a file.


Memory Mapping a File

You can access a file though a memory object that is directly mapped to the file. Windows CE reflects any change to the memory-mapped object back to the file. Memory mapping can be more complicated to set up than traditional memory access; however, memory mapping simplifies file access. Instead of using a system-maintained pointer to write to the file, you can write directly to memory.

To set up and access a file by using memory mapping

  1. Call the CreateFileForMapping function to open or create the memory-mapped file.

    You can open any file, including files that are created by CreateFile, for memory mapping. Like CreateFile, you can also create a file with CreateFileForMapping. For Windows CE version 2.10 and earlier, you can use CreateFileForMapping for read-only permission.

  2. Use the CreateFileMapping function to create an object in memory and to tie the object to the file that is opened by CreateFileForMapping.
  3. Use the MapViewOfFile function to create a view of the memory-mapped object.
  4. Use the pointer that is returned by MapViewOfFile to gain direct access to the memory-mapped object.
  5. Call the UnmapViewOfFile function to unmap the object view.
  6. Call the CloseHandle function to close the memory object.
  7. Call CloseHandle to close the file.


Searching for a File or Directory

Use the FindFirstFile, FindNextFile, and FindClose functions to search for file or directory names that match a specified pattern. The pattern must be a valid file name and can include the asterisk (*) and question mark (?) wildcards.

To find a single file or a series of files

  1. Call the FindFirstFile function with the file name passed in through the lpFileName parameter.

    FindFirstFile returns a handle that you can use in FindNextFile. FindFirstFile also returns the file name, alternate file name, file size, CEOID, attributes, creation time, last access time, and last write time of the found file.

  2. If necessary, call the FindNextFile function with the handle that was returned by FindFirstFile.

    FindNextFile returns the same data as FindFirstFile. You can call FindNextFile multiple times to find variations of the same file.

  3. Modify the file, as necessary.
  4. Destroy the handle that was created by FindFirstFile by using FindClose.

The following code example shows how to copy all text files to a new directory of read-only files named Textro. If necessary, the example changes all files in the new directory to read-only. The example uses FindFirstFile and FindNextFile to search the root directory for all .txt files. It then copies each .txt file to Textro. If a file is not read-only, the example changes to the Textro directory and uses SetFileAttribute to convert the copied file to read-only. After the example copies all .txt file, it uses FindClose to close the search handle.

void FindFileExample (void)
{
  WIN32_FIND_DATA FileData;   // Data structure describes the file found
  HANDLE hSearch;             // Search handle returned by FindFirstFile
  TCHAR szMsg[100];           // String to store the error message
  TCHAR szNewPath[MAX_PATH];  // Name and path of the file copied
  TCHAR szDirPath[] = TEXT("\TEXTRO");

  BOOL bFinished = FALSE;

  // Create a new directory.

  if (!CreateDirectory (szDirPath, NULL))
  {
    wsprintf (szMsg, TEXT("Unable to create new directory."));
    return;
  }

  // Start searching for .txt files in the root directory.

  hSearch = FindFirstFile (TEXT("\*.txt"), &FileData);
  if (hSearch == INVALID_HANDLE_VALUE)
  {
    wsprintf (szMsg, TEXT("No .TXT files found."));
    return;
  }

  // Copy each .txt file to the new directory and change it to
  // read-only, if it is not already read-only.

  while (!bFinished)
  {
    lstrcpy (szNewPath, szDirPath);
    lstrcat (szNewPath, TEXT("\\"));
    lstrcat (szNewPath, FileData.cFileName);

    if (CopyFile (FileData.cFileName, szNewPath, FALSE))
    {
      if (!(FileData.dwFileAttributes & FILE_ATTRIBUTE_READONLY))
      {
        SetFileAttributes (szNewPath,
                FileData.dwFileAttributes | FILE_ATTRIBUTE_READONLY);
      }
    }
    else
    {
      wsprintf (szMsg, TEXT("Unable to copy file."));

      // Your error-handling code goes here.
    }

    if (!FindNextFile (hSearch, &FileData))
    {
      bFinished = TRUE;

      if (GetLastError () == ERROR_NO_MORE_FILES)
      {
        wsprintf (szMsg, TEXT("Found all of the files."));
      }
      else
      {
        wsprintf (szMsg, TEXT("Unable to find next file."));
      }
    }
  }

  // Close the search handle.

  if (!FindClose (hSearch))
  {
    wsprintf (szMsg, TEXT("Unable to close search handle."));
  }
} // End of FindFileExample code


Moving and Copying Files and Directories

Use the CopyFile and MoveFile functions to copy and move files, respectively. Both functions receive the name of the file to copy or move in their respective lpExistingFileName parameters and copy or move the file to the location described in their respective lpNewFileName parameters. CopyFile contains the bFailIfExists parameter, which lets you determine if you want CopyFile to copy over a file if a version of the file exists in the target directory. MoveFile does not have a corresponding pFailIfExists parameter; instead, MoveFile automatically fails if the file already exists in the target directory. You can use both CopyFile and MoveFile on directories. Using MoveFile on a directory moves all of the files and subdirectories within that directory.

CopyFile and MoveFile can copy and move a file within the object store, from a mounted drive to the object store, or from the object store to a mounted drive. To copy or move a file or directory from one volume to another, place the file or directory in the object store as an intermediary step between the two volumes.

You can also use the DeleteAndRenameFile function to move a file from one directory to another. Like MoveFile, DeleteAndRenameFile deletes a file after moving that file to another directory. However, you have the option of renaming the new version of the file. This function works only on RAM-based files.


Manipulating File Times

Windows CE provides the GetFileTime function, which returns the file creation time, the last time that the file was accessed, and the last time that the file was written to. Windows CE creates file times in coordinated universal time (UTC) format, also known as Greenwich mean time. Use the FileTimeToLocalFileTime and FileTimeToSystemTime functions to translate the UTC time into the proper time zone format.

You can also set file times with the SetFileTime function. Depending on the file system, before using SetFileTime, you might need to gain access to the file by using CreateFile to open the file. If you change one of the dates on a file in the Windows CE store—for example, the file creation time—by default the other two dates—for example, the last time that the file was accessed and the last time that the file was written to—are updated to the new time as well. This technique works on the current object store, but does not work on a FAT file system.


Retrieving File and Directory Information

Windows CE provides three functions for retrieving data about a given file or directory. The following table describes these functions.

Function
Returns
GetFileAttributes | The flag settings for the file attributes, including archive, compression, encryption, and read/write status
GetFileSize | The file size
GetFileInformationByHandle | The same information as GetFileTime, GetFileAttributes, and GetFileSize, as well as the object identifier of the file


Deleting a File or Directory

Once you are finished with a file, close the file handle by using the CloseHandle function. CloseHandle is a generic function that closes a variety of handles, including the handle to a file. If you leave a file open when your application terminates, Windows CE automatically closes the file.

To close and delete a file

  1. Close the file with a call to CloseHandle.
  2. Delete the file with a call to the DeleteFile function.

    Windows CE cannot delete a file that has an open handle.

Call the RemoveDirectory function, with the full path name of the directory in the lpPathName parameter, to delete a directory. You must delete all of the files in the directory before calling RemoveDirectory.


Accessing Data on Other Storage Media

A Windows CE–based device has other areas to store applications besides the object store. You can access a file stored in ROM just like any other file: by calling the file application programming interface (API). However, you cannot alter a file that is stored in ROM. Instead, ROM-based files are marked with the FILE_ATTRIBUTE_INROM value, indicating that they are read-only. Windows CE also uses the FILE_ATTRIBUTE_ROMMODULE value to indicate that a file is designated to be executed in ROM, rather than copied to RAM. You cannot use CreateFile to open files that are designated with FILE_ATTRIBUTE_ROMMODULE. Instead, use the LoadLibrary and CreateProcess functions to gain access to the module.

Also, Windows CE supports PC Cards, such as Advanced Technology Attachment (ATA) flash cards, and linear flash cards. These cards can have an installed file system that can utilize the storage space for files and databases. However, a mounted file system must be used in conjunction with an installed file system driver, such as FAT. Once you install a PC Card, you can copy database objects between the object store and the mounted volume.

Windows CE does not assign a letter label to a storage card in the same manner that a desktop computer assigns a drive letter to a hard disk. Instead, the file driver creates directories in the root directory representing each partition on each storage card. In Windows CE 2.0 and earlier, these directories were given default names, such as Storage Card or PC Card. In Windows CE 2.10, the FAT file system driver queries the PC Card driver for a default name. If the PC Card driver does not supply a default name, Windows CE uses Storage Card. You can also tell the difference between a object store directory and a mounted volume directory by the file attributes. All directories on a mounted file system have the FILE_ATTRIBUTE_TEMPORARY file attribute flag set.

The following code example tries to open any Storage Card directories that exist and tests them to see if they are located on a storage card.

void FindingStorageCards (void)
{
  TCHAR szMsg[100];       // String to store the error message
  HANDLE hSearch;         // Search handle returned by FindFirstFile
  WIN32_FIND_DATA  fd;    // Data structure describes the file found
  BOOL bFinished = FALSE; // Flag to indicate whether the loop is done
  TCHAR *szFname = TEXT("\\Storage Card*");
                          // Name that matches all storage cards

  // Be sure Storage Card exists.
  hSearch = FindFirstFile(szFname, &fd);

  if (hSearch == INVALID_HANDLE_VALUE)
  {
    wsprintf(szMsg, TEXT("No storage card found."));
    return;
  }

  do {
    // Test whether the file is really on a storage card, and
    // not just a directory in the root directory.
    // It must have both the directory attribute and
    // the temporary attribute.

    if (  (fd.dwFileAttributes & FILE_ATTRIBUTE_TEMPORARY)
       && (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) )
    {
      wsprintf (szMsg,
                TEXT("%s is a storage card."), fd.cFileName);
    }
    else
    {
      wsprintf (szMsg,
                TEXT("%s is not a storage card."), fd.cFileName);
    }

    if (!FindNextFile (hSearch, &fd))
    {
      bFinished = TRUE;
      if (GetLastError () != ERROR_NO_MORE_FILES)
      {
        wsprintf (szMsg,
                  TEXT("Error trying to find files matching \"%s\"."),
                  szFname);
      }
    }
  }
  while (!bFinished);

  FindClose (hSearch);  // Close the search handle.

} // End of FindingStorageCards example code


Using a Windows CE Database

Windows CE provides a simple database API containing a single level and a maximum of four sort indices. Prior to Windows CE 2.10, a Windows CE database could exist only in the object store. Windows CE 2.10 and later enables you to store and access a database in a database volume, anywhere on the device. This includes PC Cards or other installed file systems. The file is called a volume because it can contain more than one database. Use the database API to create and store a simple database, such as a phone number listing or an e-mail repository.

A Windows CE database consists of one or more records. The maximum size of a record is defined in the CEDB_MAXRECORDSIZE constant in Windbase.h; for Windows CE 2.10, CEDB_MAXRECORDSIZE is 131,072 bytes. A record can have a variable number of properties, but it cannot contain another record. The maximum size of a property is defined in the CEDB_MAXPROPDATASIZE constant, which is 65,471 bytes. Windows CE allocates space for a record or property only when necessary. These same constants are defined in Rapi.h for remote API (RAPI) calls. The following table shows the different types of available record properties.

Record property type
Contains
CEVT_BOOL | Boolean value
CEVT_CEBLOB | Binary object
CEVT_R8 | 8-byte signed value
CEVT_FILETIME | Time and date data
CEVT_I2 | 2-byte signed integer
CEVT_I4 | 4-byte signed integer
CEVT_LPWSTR | Long pointer to a Unicode string
CEVT_UI2 | 2-byte unsigned integer
CEVT_UI4 | 4-byte unsigned integer

Note The BOOL and Double properties are available only on Windows CE version 2.10 and later.

The overhead associated with creating a record is 20 bytes per record. The overhead for a property is 4 bytes per property.

In addition to records, a database contains a name and type identifier. The database name is a null-terminated string of up to 32 characters. The type identifier is application-specific and is commonly used to identify similar databases. For example, the Microsoft Pocket suite uses the number 24 as a type identifier for all Contacts databases.

Because Windows CE is designed to operate in a relatively volatile environment, the Windows CE database does not update automatically only when the database opens or closes. Instead, the database updates after each individual transaction, such as a CeWriteRecordProps call.


Mounting and Unmounting a Database Volume

In order to create and use a database volume with Windows CE 2.10 and later, you must first mount, or open, the volume. This does not include the object store, which is always mounted and identified by a system globally unique identifier (GUID). Once all operations are complete, you must unmount, or close, the volume. Mounting and unmounting adds a level of complexity to your database programming but allows you to store a database elsewhere on a device besides the object store. Note that you can still use the original database API in Windows CE 2.10 and later to gain access to a database in the object store. However, the earlier API exists for backward compatibility only; all new applications should use the new API.

Call the CeMountDBVol function to mount a volume that can store Windows CE databases on any file system. This file system can include the object store or an installed file system, such as the FAT file system on a PC Card. CEMountDBVol acts as a specialized version of CreateFile; instead of opening or creating a generic file, CEMountDBVol opens or creates a database volume. CEMountDBVol accepts the location of the file that is the database volume and returns a PCEGUID. Functions designed to manipulate mounted database volumes use the PCEDUID as a handle to identify a given volume.

Call the CEUnmountDBVol function to unmount a database volume. Like CloseHandle, CeUnmountDBVol returns resources that the database volume was using back to the system. To be sure that Windows CE writes any cached data to permanent storage, call the CeFlushDBVol function prior to unmounting the volume. The database can also be closed by user reset or a power failure. Because Windows CE automatically flushes the cache on a periodic basis, you will lose only data altered since the last cache flush. Also, the volume itself is not altered by a power failure or reset.

Note that more than one process can mount the same database volume. Windows CE keeps track of how many processes have access to a given database volume. When the last process unmounts the database, Windows CE closes the file.

The following code example shows how to unmount a database volume. For an example showing how to mount a database volume, see “Mounted Database Example.”

void UnmountingDBs (PCEGUID pceguid)
{
  TCHAR szMsg[100];

  if (!CeUnmountDBVol (pceguid))
  {
    wsprintf (szMsg, TEXT("Failed unmounting the database."));

    // Your error-handling code goes here.
  }
} // End of UnmountingDBs example code


Creating a Database

Use the CeCreateDatabaseEx function to create a database in any volume. CeCreateDatabaseEx identifies the volume, names the database, identifies the sort order, and lets you pass in a user-defined type identifier. The sort order is an index that is applied to a database to manipulate the record ordering. Although you define the sort order when the database is created, you can alter the sort order later. The type identifier, while user-defined, is commonly used to identify similar types of databases.

To create a database within the object store with Windows CE 2.10 and later

  1. Call the CREATE_SYSTEMGUID macro to create an object identifier for the object store.

    The returned GUID can act as a standard identifier for a handle.

  2. Call CeCreateDatabaseEx, using the CEGUID created in the previous call to CREATE_SYSTEMGUID.

When you create a database on a mounted volume, it is usually a good idea to make the database uncompressed. Accessing a mounted volume is usually slower than accessing the object store: compressing and decompressing slows the process even further. Therefore, create your database as an uncompressed database, unless you expect the database to be larger than the storage card can contain.

To set compression on a database after the database is created

  1. Create a CEDBASEINFO structure with the CEDB_VALIDBFLAGS value set in the dwFlags value.
  2. Toggle off the CEDB_NOCOMPRESS bit in the dwFlags value by using the following code fragment.
    dwFlags &= ~(CEDB_NOCOMPRESS);
  3. Call the CeSetDatabaseInfoEx function with the previous CEDBASEINFO structure.


Opening a Database

Once you create a database, use the CeOpenDatabaseEx function to open it. CeOpenDatabaseEx specifies the sort order you use on the database during a given session in the propid parameter. To use a different sort order, you must close the database and open it up again with a different sort order. You can also pass a handle to a window in the hwndNotify member of the CENOTIFYREQUEST structure. Windows CE sends messages to the specified window when other processes or other threads modify the open database. When a change occurs, Windows CE sends a WM_DBNOTIFICATION message with the lParam set to CENOTIFICATION. The following table describes the possible values of the uType parameter in the CENOTIFICATION structure.

Message
Description
DB_CEIOD_CHANGED | Record changed
DB_CEIOD_CREATED | Record created
DB_CEIOD_RECORD_DELETED | Record deleted

Remember that you must mount the volume with CEMountDBVol before opening the database. In addition, CeOpenDatabaseEx lets you receive additional data regarding other processes and threads through CENOTIFYREQUEST. CENOTIFYREQUEST lets you choose either the original Windows CE messages for database changes or the WM_DBNOTIFICATION message. The lParam of WM_DBNOTIFICATION points to a CENOTIFICATION structure. The following table describes the different parameters of the CENOTIFICATION structure.

Parameter
Description
dwSize | CENOTIFICATION size
dwParam | Application-defined value
uType | Why the message was sent
guid | GUID of the relevant database volume
oid | Object identifier of the relevant database record
oidParent | Object identifier of the parent of the database record

Windows CE places CENOTIFICATION in a heap that you define in CENOTIFYREQUEST. If you do not specify a heap in CENOTIFYREQUEST, Windows CE creates the heap in your default process heap. Once finished, free memory is allocated to CENOTIFICATION with a call to the CeFreeNotification function. You must delete the CENOTIFICATION structure each time you receive a WM_DBNOTIFICATION message.

The following code example attempts to open a database of addressees by calling CeOpenDatabaseEx. If the database does not exist, the code example calls CeCreateDatabaseEx to create a new address database with three different sort orders. After creating the database, the example attempts to open the database again.

HANDLE OpenDatabase (
      HWND hwndNotify,  // Handle to the window to which
                        // notification messages are posted
      PCEGUID pceguid,  // Pointer to the mounted database
                        // volume where the database to
                        // be opened resides
      CEOID CeOid)      // Object identifier of the database
                        // to be opened
{
  int index;
  DWORD dwError;              // Return value from GetLastError
  HANDLE hDataBase;           // Open handle to the address database
  CENOTIFYREQUEST *pRequest;  // CENOTIFYREQUEST structure
  CEDBASEINFO CEDBInfo;       // Structure containing the
                              // database data
  TCHAR szError[100];         // String to use with error messages

  // Allocate memory for pRequest.
  pRequest = (CENOTIFYREQUEST *) LocalAlloc (LPTR,
                                        sizeof (CENOTIFYREQUEST));

  pRequest->dwSize = sizeof (CENOTIFYREQUEST);
  pRequest->hwnd = hwndNotify;
  pRequest->hHeap = NULL; // Let system allocate memory properly.
  pRequest->dwFlags = 0;  // Notifications are handled as they
                          // were in Windows CE version 1.0.

  hDataBase = CeOpenDatabaseEx (
          pceguid,            // Pointer to the mounted volume
          &CeOid,             // Location for the database identifier
          TEXT("MyDBase"),    // Database name
          0,                  // Sort order; 0 indicates to ignore
          CEDB_AUTOINCREMENT, // Automatically increase seek pointer
          pRequest);          // Pointer to a CENOTIFYREQUEST
                              // structure

  if (hDataBase == INVALID_HANDLE_VALUE)
  {
    dwError = GetLastError ();

    if (dwError == ERROR_NOT_ENOUGH_MEMORY)
    {
      wsprintf (szError, TEXT("Not enough memory"));
    }
    else
    {
      // Possibility of nonexisting database; create it.

      // Initialize structure CEDBInfo
      memset (&CEDBInfo, 0, sizeof(CEDBInfo));

      // Create the database with the following specified flags.
      // Create the database as uncompressed.
      // You can use CeSetDataBaseInfoEx to compress the database.
      CEDBInfo.dwFlags =
          CEDB_VALIDNAME        // szDbaseName is valid
          | CEDB_VALIDTYPE      // dwDbaseType is valid
          | CEDB_VALIDDBFLAGS   // HIWORD of dwFlag is valid
          | CEDB_VALIDSORTSPEC  // rgSortSpecs is valid
          | CEDB_NOCOMPRESS;    // The database is not compressed.

      // Assign the database name as MyDBase.
      wcscpy (CEDBInfo.szDbaseName, TEXT("MyDBase"));

      // Assign the database type.
      CEDBInfo.dwDbaseType = 0;

      // Set the number of active sort orders to 4.
      // This is the maximum number allowed.
      CEDBInfo.wNumSortOrder = 4;

      // Initialize the array of sort order descriptions.
      for (index = 0; index < CEDBInfo.wNumSortOrder; ++index)
      {
        // Sort in descending order.
        CEDBInfo.rgSortSpecs[index].dwFlags = CEDB_SORT_DESCENDING;

        // Assign the identifier of the properties to sort by.
        // CEDBInfo.rgSortSpecs[index].propid = ...;
      }

      // Create database "MyDBase".
      CeOid = CeCreateDatabaseEx (pceguid, &CEDBInfo);

      if (CeOid == NULL)
      {
        wsprintf (szError,
                  TEXT("ERROR: CeCreateDatabaseEx failed (%ld)"),
                  GetLastError ());
      }
      else  // Succeeded in creating the database; open it.
      {
        hDataBase = CeOpenDatabaseEx (pceguid, &CeOid,
                          TEXT("MyDBase"), 0, 0, pRequest);
      }
    }
  }

  // Return the database handle.
  return hDataBase;

} // End of OpenDatabase example code

/*


Modifying the Sort Order

When you are creating a database, you can define up to four different sort orders. Typically, each record in a database contains a similar set of properties, and each type of property shares the same property identifier. For example, each record in a Contacts database might contain a name, street address, city, state or province, postal code, and telephone number. All name properties would have the same property identifier, all street addresses would have the same property identifier, and so on. You can select one of these properties and direct the system to sort the records based on that property. However, you cannot perform a sort on a binary property. The order in which the records are sorted affects the order in which the CeSeekDatabase database-seeking function finds records in the database.

You specify which of the four sort orders you want to use on a database when you call the CeOpenDatabase or CeOpenDatabaseEx function with the SORTORDERSPEC structure. SORTORDERSPEC contains the identifier of a single property on which the database properties are to be sorted. SORTORDERSPEC also includes a combination of flags that indicate whether to sort the records in ascending or descending order, whether the sort is case-sensitive, and whether to place records that do not contain the specified property before or after all other records. By default, sorting is done in ascending order and is case-sensitive. Windows CE places all records not containing the specified property at the end of all other records. Although you can have only one sort order active for each handle, you can open multiple handles to a given database. Using multiple handles, you can use more than one sort order.

You can change these four sort orders or other database properties with a call to the CeSetDatabaseInfoEx function. CeSetDatabaseInfoEx lets you change the database name, type, or any of the four sort orders. When you change a sort order, Windows CE parses the database and changes each record. This process might take several minutes on a large database. It is more efficient to create the database with the necessary sort orders to begin with. If you must change the sort order, be sure that you inform the user of the projected time delay.


Searching for a Record

Before you can read and write a record in a database, you must find the record. Use CeSeekDatabase to move the database seek pointer to the record that you want to read from or write to. CeSeekDatabase always uses the current sort order as it is specified in the call to CeOpenDatabaseEx. CeSeekDatabase can search through a database for a relative or exact property value, a relative or exact location, or a record object identification. However, Windows CE can perform a seek operation only on a sorted property value. When CeSeekDatabase finds a record, Windows CE positions the seek pointer at that record. Any subsequent read operation takes place at the location of the seek pointer. If you set the CEDB_AUTOINCREMENT flag in your call to CeOpenDatabaseEx, each read operation automatically increments the seek pointer from the current record to the next record.

Seek operations are affected by the sort order that is associated with the open database handle. For example, suppose that the Contacts database was opened by using a sort operation on the name property. If you specify the CEDB_SEEK_VALUEFIRSTEQUAL flag and a value of "John Smith,” CeSeekDatabase searches from the beginning of the database looking only at the name property of each record. It stops when, and if, it finds a matching property.

The following code example shows how to search for a record by using CeSeekDatabase.

void UsingCeSeekDatabase (void)
{
  CEOID CeOid;          // Object identifier
  CEOID CeOidSeek;      // Object identifier
  DWORD dwIndex;        // Index of record to seek
  TCHAR szError[100];   // String for displaying error messages
  HANDLE hDataBase;     // Handle to a user-allocated heap

  // Assign to CeOidSeek the object identifier of the record
  // being searched for.
  // ...

  // Call CeOpenDatabaseEx to open the database.
  // hDataBase = CeOpenDatabaseEx {...}

  // Perform the seek. This type of seek operation is very efficient.
  if ((CeOid = CeSeekDatabase(
              hDataBase,        // Handle of the database
              CEDB_SEEK_CEOID,  // Finding an object with the
                                // same identifier
              CeOidSeek,        // Specifies the record to seek
              &dwIndex          // If successful, moves the database
                                // pointer to point to the record
                )) == 0)
  {
    wsprintf (szError, TEXT("ERROR: CeSeekDatabase failed (%ld)"),
                          GetLastError ());
  }

} // End of UsingCeSeekDatabase example code


Reading a Record

Once you find a record, use the CeReadRecordProps or CeReadRecordPropsEx functions to read the properties of the record. To indicate the properties to be read, specify an array of property identifiers. Also, specify the buffer to which the functions write the property data and the size of the buffer. Setting the CEDB_ALLOWREALLOC flag in the dwFlags parameter instructs Windows CE to reallocate the buffer if the returned data is too large. If the database is stored in a compressed format, the database engine must decompress records in 4-KB sections as they are read.

CeReadRecordProps can return selected record properties or a full view of all the properties. If you choose to view all the properties, you can have CeReadRecordProps allocate memory from the local heap to return all the record values. CeReadRecordPropsEx can use any heap to return record values, not just the local heap. For efficiency, your application should read all of the desired properties in a single call rather than in several separate calls.

If you do read for a specific property, make sure to check for the CEDB_PROPNOTFOUND flag in the CEPROPVAL structure for that property. You should check for this flag because the record in question might not have the property you are looking for.

When Windows CE reads a record property successfully, the system copies the property information into the specified buffer as an array of CEPROPVAL structures. CeReadRecordProps and CeReadRecordPropsEx also return the Windows CE object identifier of the record. All the variable-size data, such as strings and binary large objects (BLOBs), are copied to the end of the buffer. The CEPROPVAL structures contain pointers to this data.

The following code example shows how to create and write four properties: a 16-bit signed integer, a 32-bit signed integer, a null-terminated string, and a BLOB to the new record.

void ReadingDBRecords (void)
{
  CEOID CeOid;            // Object identifier of the record read
  PCEGUID pceguid;        // Pointer to the mounted database volume
  WORD wcPropID;          // Number of properties retrieved
  DWORD dwcbBuffer,       // Count of bytes of the *lpBuffer
        dwError;          // Return value of the GetLastError function
  TCHAR szMsg[100];       // String for displaying the error message
  LPBYTE lpBuffer = NULL; // Pointer to a buffer that receives record
                          // property data
  HANDLE hDataBase,       // Handle to a database to be opened
         hHeap;           // Handle to the heap for allocating records

  // Assign to pceguid the GUID of the mounted volume
  // where the database resides.
  // ...

  // Create the heap by calling HeapCreate to allocate the database
  // records.
  // ...

  // Open the database with the current seek position to be
  // automatically incremented with each call.

  hDataBase = CeOpenDatabaseEx (
          pceguid,            // Pointer to the mounted volume
          &CeOid,             // Location of the database identifier
          TEXT("MyDBase"),    // Database name
          0,                  // Sort order; 0 indicates to ignore.
          CEDB_AUTOINCREMENT, // Automatically increase seek pointer
          NULL);              // Does not need to receive notification

  // Check for errors on the hDataBase handle before continuing.

  if (hDataBase == INVALID_HANDLE_VALUE)
  {
    // Your error handling code goes here.
    return;
  }

  while ((CeOid = CeReadRecordPropsEx (
            hDataBase,          // Handle of the database.
            CEDB_ALLOWREALLOC,  // Use LocalAlloc to get the buffer.
            &wcPropID,          // Number of properties retrieved
            NULL,               // NULL means retrieve all properties.
            &lpBuffer,          // Buffer receives property data.
            &dwcbBuffer,        // Count of bytes in *lpBuffer.
            hHeap)) != 0)       // Handle to the heap for allocating
                                // the record when
                                // CEDB_ALLOWREALLOC is specified.
  {
    // The record is now available in the lpBuffer. Add code here to
    // manipulate the properties in this record.
    // ...
  }

  // Error handling if CeReadRecordPropsEx fails

  dwError = GetLastError ();

  if (dwError == ERROR_NO_MORE_ITEMS)
  {
    wsprintf (szMsg,
              TEXT("Read through all records in the database."));
  }
  else if (dwError == ERROR_INSUFFICIENT_BUFFER)
  {
    wsprintf (szMsg,
              TEXT("Re-allocation of database records failed."));
  }
  else
  {
    wsprintf (szMsg, TEXT("Other errors."));
  }

  // Close the database handle.

  CloseHandle (hDataBase);

} // End of ReadingDBRecords example code


Writing and Creating a Record

Use the CeWriteRecordProps function to write to a record. Like CeReadRecordProps, CeWriteRecordProps uses an array of CEPROPVAL structures to pass property information into the record. To create a new record, call CeWriteRecordProps with the oidRecord parameter set to 0. Once you finish writing or creating a record, Windows CE updates the database.

When writing to a mounted volume, Windows CE caches all write operations. The database subsystem periodically requests a cache flush after a series of operations. If memory is low, Windows CE flushes the cache to permanent storage. Windows CE cannot choose to flush only part of the cache: all database blocks are flushed during a flush.


Deleting Database Information

Windows CE enables you to delete properties, records, or entire databases. To delete a property, call CeWriteRecordProps with the CEDB_PROPDELETE flag set in the wFlags parameter and a CEPROPVAL structure describing the property to delete in the rgPropVal parameter. You can delete a property with CeWriteRecordProps in any volume.

Note A record that has been deleted and then restored receives a new Windows CE object identifier.

Call the CeDeleteRecord function to delete a single record in a database. Like CeWriteRecordProps, CeDeleteRecord works on any record, regardless of the location of the record. Call the CeDeleteDatabaseEx function to delete a database in a volume. Be sure that the database is not open before you attempt to delete the database.


Enumerating a Database and Database Volumes

Enumerating databases is the process of sequentially accessing each database in a group. The group can include all databases in the object store, databases of a specified type, or databases in all mounted volumes. Use enumeration when you need to change all databases of a certain type or when you need to synchronize data between a desktop computer and a Windows CE–based device.

Use the CeFindFirstDatabaseEx and CeFindNextDatabaseEx functions to enumerate databases within a specified database volume. The volume can be the object store or any mounted database volume. The primary difference between these two sets of functions is that CeFindFirstDatabaseEx and CeFindNextDatabaseEx have an additional parameter identifying the CEGUID of the volume. If you pass in an invalid CEGUID, Windows CE searches all of the volumes on a Windows CE–based device, including the object store. You can also enumerate the mounted database volumes with a call to CeEnumDBVol.

When you are finished, call CloseHandle to close the enumeration handle.

The following code example shows how to enumerate the databases within the object store.

void EnumeratingDBs (void)
{
  DWORD dwError;          // Return value of GetLastError function.
  TCHAR szMsg[100];       // String to display error message
                          // and database data.
  HANDLE hEnumDB;         // Handle to a database enumerator.
  CEOID CeOid;            // Object identifier of a database.
  CEOIDINFO CeObjectInfo; // Structure that contains
                          // database data.
  PCEGUID pceguid = NULL; // Pointer to the mounted volume identifier.
                          // NULL means all mounted database volumes
                          // are to be searched.

  // Find the first database. Set the database type to 0, so all types
  // of databases are enumerated. If pceguid is set to NULL or an
  // invalid GUID is created by CREATE_INVALIDGUID, all mounted
  // database volumes are searched.

  hEnumDB = CeFindFirstDatabaseEx (pceguid, 0);

  if (hEnumDB == INVALID_HANDLE_VALUE)
  {
    if (GetLastError () == ERROR_OUTOFMEMORY)
      wsprintf (szMsg, TEXT("Out of memory."));
    else
      wsprintf (szMsg, TEXT("Unknown error."));

    return;
  }

  while ((CeOid = CeFindNextDatabaseEx (hEnumDB, pceguid)) != 0)
  {
    // Retrieve database data.
    if (!CeOidGetInfoEx (pceguid, CeOid, &CeObjectInfo))
    {
      // Your error-handling code goes here.

      CloseHandle (hEnumDB);  // Close the search handle.

      return;
    }
    else
    {
      wsprintf (szMsg, TEXT("The name of the database is: %s"),
                  CeObjectInfo.infDatabase.szDbaseName);
    }
  }

  // Error handling if CeFindNextDatabaseEx fails
  dwError = GetLastError ();

  if (dwError == ERROR_KEY_DELETED)
  {
    // A database has been deleted during enumeration. You must
    // restart the enumeration process.
    wsprintf (szMsg,
              TEXT("A database was deleted during enumeration."));
  }
  else if (dwError == ERROR_NO_MORE_ITEMS)
  {
    wsprintf (szMsg, TEXT("No more item to enumerates."));
  }
  else
  {
    wsprintf (szMsg, TEXT("Unknown error."));
  }

  // Close the search handle.
  CloseHandle (hEnumDB);

} // End of EnumeratingDBs example code


Mounted Database Example

The following code example shows how to mount the MyDBVol database volume. The example opens the database volume if the volume already exists, or it creates the database if the database does not exist. The example also shows how to enumerate all of the mounted database volumes and transfer the data into permanent storage.

void MountingDBVolume (void)
{
  PCEGUID pceguid;            // Pointer to the mounted volume
  LPWSTR lpBuf;               // Buffer to store the volume name
  TCHAR szError[100];         // String to display with error message
  DWORD dwError,              // Return value of GetLastError function
        dwNumChars;           // Length of the buffer in characters
  TCHAR * szFname = TEXT("\\Storage Card\\MyDBVol");
                              // Name of the database volume
  BOOLEAN bFinished = FALSE;  // Loop (enumeration) control

  // Allocate memory for pceguid.
  pceguid = (PCEGUID) LocalAlloc (LPTR, sizeof (CEGUID));

  // Open the database volume MyDBVol on the storage card if it exists.
  // Create it if it does not exist.
  if (!CeMountDBVol (pceguid,         // Pointer to a CEGUID.
                     szFname,         // Database volume name.
                     OPEN_ALWAYS))    // Create the database volume
                                      // if it does not exist.
  {
    // Your error-handling code goes here.

    // If the database volume was not opened or created
    wsprintf (szError, TEXT("ERROR: CeMountDBVol failed (%ld)"),
                        GetLastError());
  }

  // Allocate memory for lpBuf.
  lpBuf = (LPWSTR) LocalAlloc (LPTR, MAX_PATH);

  // Assign MAX_PATH to dwNumChars.
  dwNumChars = MAX_PATH;

  // Create an invalid pceguid as the input parameter of
  // CeEnumDBVolumes.
  CREATE_INVALIDGUID (pceguid);

  while (!bFinished)
  {
    // Enumerates database volumes
    if (!CeEnumDBVolumes (pceguid,      // Points to a mounted volume
                          lpBuf,        // Buffer to store the
                                        // volume name
                          dwNumChars))  // Length of buffer in char

    {
      // If error occurs in enumerating

      bFinished = TRUE;    // Finished enumerating

      dwError = GetLastError();

      wsprintf (szError, TEXT("ERROR: CeMountDBVol failed (%ld)"),
                          dwError);

      if (dwError == ERROR_INSUFFICIENT_BUFFER)
      {
        // To avoid having to restart the enumeration,
        // free lpBuf and reallocate a bigger buffer.

        bFinished = FALSE;  // Continue with enumerating
        LocalFree (lpBuf);  // Free lpBuf

        // Your code to reallocate a bigger buffer goes here.
        // ...
      }
    }
    else  // Succeeded in enumerating
    {
      // Flush the database volume's data to permanent storage.

      if (!CeFlushDBVol (pceguid))
      {
        wsprintf (szError, TEXT("ERROR: CeFlushDBVol failed."));

        // Your error-handling code goes here.
      }
    }
  }
} // End of MountingDBVolume example code


Manipulating the Registry

The registry is a database that stores data about applications, drivers, user preferences, and other data that Windows CE needs to perform properly. For example, a user’s default preferences for Pocket Word are stored in the registry. The registry is organized in a hierarchical system of keys and values. A key is similar to a directory, and can contain values and other keys. Windows CE supports three root keys. The following table shows the three root keys that Windows CE supports, and what type of data you should store under these keys.

Key name
Contains
HKEY_LOCAL_MACHINE | Hardware and driver configuration data
HKEY_CURRENT_USER | User configuration data
HKEY_CLASSES_ROOT | OLE and file type matching configuration data

A value is the basic piece of data that is stored in the registry, and it can be a variety of types, including string or binary. Each value has a name and an associated piece of data. For example, a device running the Windows CE, Handheld PC Professional Edition, software uses Wrap to Window in the HKEY_LOCAL_MACHINE\Software\Microsoft\Pocket Word\Settings key to store a numerical value.

The following table describes the different limits in the registry.

Limit
Description
Key or value name length | 255 characters
Data size | 4 KB
Key hierarchy depth | Up to 16 nested subkeys

Use the registry to store data that your application needs for each session. For example, you can save the state of your application during the shutdown process. Your application can search the registry on startup to reinstate the previously saved settings. When programming with the registry, try to keep name and data size small. Note that registry values in Windows CE take up less memory than registry keys. Design your registry hierarchy to use as few keys as possible.

Note Windows CE implements the registry as a RAM-based heap file. If the RAM, loses power, the registry data may be lost if the OEM has not implemented a registry backup procedure. Windows CE must then reload the initial registry from ROM.


Creating and Opening a Registry Key

Create a registry key with a call to the RegCreateKeyEx function. Windows CE automatically opens the key once you create it. If the key already exists, a call to RegCreateKeyEx simply opens the key for processing. Also, you can use the RegOpenKeyEx function to open a key. The difference between the two functions is that RegOpenKeyEx does not create a new key if the key did not previously exist.


Reading a Registry Key or Value

Use the RegQueryValueEx function to read a specified value from the registry. You can also use RegQueryInfoKey to receive data about a specified key.


Writing and Creating a Registry Value

Use the RegSetValueEx function to add a value to or alter a value in a registry key. If the value that you want to change does not exist, RegSetValueEx creates a value and the associated data. You can also choose to name the type of data as you enter the data into the key. Because all values are stored and returned in a binary format, labeling data does not affect how Windows CE views it. However, for the sake of a third-party registry editor, you should attempt to label your values appropriately.


Enumerating Registry Keys

Use the RegEnumKeyEx function to enumerate the subkeys of a specified key. Call the RegEnumValue function to enumerate the values in a specified key. Both functions receive an index number that moves the function to the next key or value. When there are no more keys or values to return, both functions return the ERROR_NO_MORE_ITEMS value.


Deleting a Registry Key or Value

Call the RegDeleteKey or RegDeleteValue function to delete registry keys and values, respectively. The key must be closed before you can properly delete it.


Closing the Registry

Once you are done with a registry key, close the key with a call to the RegCloseKey function. Calling RegCloseKey instructs Windows CE to flush any unwritten data to the key before closing the key.


Flushing the Registry

One option that might be available is the RegFlushKey function. RegFlushKey is an OEM-implemented function that attempts to flush the entire registry to a platform-supported storage. If it is available, you should call RegFlushKey after each major change or group of changes to your registry. Depending on the implementation, RegFlushKey can require a great deal of system resources. So while RegFlushKey may be an effective way to back up the registry, calling the function multiple times for minor changes might impede the performance of your application.


Integrating Engines into an Application

An engine is a section of an application that determines how that application manages and manipulates a type of data. From a developmental standpoint, an engine can also be application or module with an open application programming interface (API) to which your application passes data in order to accesses the engine's processing capabilities.

An engine can be another application, such as the Microsoft Help for Windows CE system. Applications pass Help files to the Help system, and Help for Windows CE displays those files. An engine can also be part of the operating system (OS), such as the spelling checker. An application passes the spelling checker text strings, and the spelling checker checks the spelling of the words in those strings.

This section discusses the Help and spelling checker engines, how they work, and how you can use their capabilities in your applications.


Creating a Help System

Using the Help for Windows CE engine, you can display Hypertext Markup Language (HTML)–based Help files on your Handheld PC or Palm-size PC device. Help for Windows CE consists of Peghelp.exe and an HTML rendering application that enables a user to display HTML-based Help files. The display window for Help for Windows CE contains a content area and a toolbar. This window is either full-screen or partial-screen, depending on the specific platform. The following illustration shows a Help for Windows CE window for the Handheld PC.

Currently, there are three versions of Help for Windows CE: 2.0, 2.01, and 2.10. The version numbers correspond to the version of Windows CE that the Help engine was released with. To determine which version of Help for Windows CE you are using, consult the platform-specific sections of the SDK. The following table shows the user interface (UI) elements that are contained in the different versions of Help for Windows CE.

Button
Versions 2.0 and 2.10
Version 2.01
Contents | Displays the first-level contents for the current Help file. | Displays the first-level contents for the current Help file.
Back | Displays the previous topic view during the current session. The history is not persistent from session to session. | Displays the previous topic in the history list. The history is persistent from session to session and retains the last 10 visited topics.
Forward | N/A | Displays the next topic in the history list.
All Topics/
Other Help 		Displays a list of Microsoft-provided Help files. Displays a list of Microsoft-provided Help files.
Full Screen | Displays Help in a full screen view. | N/A
Partial Screen | Displays Help in a partial screen view. | N/A

The following table describes the three ways a user can gain access to Help files.

Action
Versions 2.0 and 2.10
Version 2.01
Help command, on the Start menu | Displays a list of Microsoft-provided Help files | Displays the context-sensitive Help topic for the current area of the UI
? button | Displays the context-sensitive Help topic for the current area of the UI | N/A
ALT + H | Displays the context-sensitive Help topic for the current area of the UI | Displays the context-sensitive Help topic for the current area of the UI

A Help system for Windows CE consists of HTML files and graphics. Help for Windows CE version 2.10 includes support for an All Topics list, using link (.lnk) files. You can also create an index.

To write a Help file

  1. Create the Help file.
  2. Add content to the Help file.

    If necessary, check your content with an HTML and link validator.

  3. Link the file into your application.


Creating the Help File

To create Help for a Windows CE platform, first create the appropriate files. Use an HTML editor, such as NotePad or Microsoft FrontPage®, a web site creationto do so. The following table shows what extensions to use for your Help files.

Help for Windows CE version number
File extension
2.0 | .htp
2.01 | .htm
2.10 | .htm

Note Other than the file extension, .htp and .htm files are identical.


Adding Content to a Help File

Once you create the Help file, use the Windows CE HTML version 3.0 subset to add content. This section discusses the differences between HTML 3.0 and the Windows CE HTML version, including the following topics:

  • General Content Guidelines
  • Using Jumps in a Help File
  • Using Graphics in a Help File
  • Separating Help Topics
  • Creating an Index

The section concludes with a code example of an HTML topic for Help for Windows CE.


General Content Guidelines

Use the following guidelines to create Help for Windows CE:

  • Do not use HTML Help on a Windows CE–based device. HTML Help files do not run on Windows CE platforms.
  • Try to make the UI intuitive so that the user does not need Help. If possible, move all Help text into the UI.
  • Because the user might not have ready access to the manual, supply as much information as possible in your Help file.
  • When writing Help content, use abbreviations and other shorthand methods. In addition to taking up less memory, abbreviations let the user quickly scan a Help topic. For example, use “Choose File>Open” instead of “On the File menu, choose Open.”
  • Limit the number of styles you use in the Help file to make your Help file consistent and easier to read.
  • Use generic wording in your Help files. Because you do not necessarily know what type of device your Help file will run on, you can increase portability by using generic phrases such as “Choose Find” instead of “Tap Find” or “Click Find.”
  • Although graphics are useful in Help files, limit your use of graphics, to conserve memory. If you must use a graphic, use a black-and-white version.


Using Jumps in a Help File

You can create two kinds of jumps in a Help for Windows CE file:

  • A jump that points to another file. This type of jump uses the standard <A> tag. For example:
    <A HREF="soltr.htm">
  • A jump that points to another anchor within a file. For example:
    <A HREF="soltr.htm#about">

    Even if both the jump and the anchor are within the same file, this type of jump requires the file name, in addition to the anchor name.


Using Graphics in a Help File

You can include both graphics and text in your Help for Windows CE files. Use the .2bp file extension for your graphics; Help for Windows CE does not support the .jpg or .gif file extensions.

The following HTML example shows how to add a bitmap to a topic, using the image source tag <IMG SRC> to specify the file name of the bitmap to include.

<IMG SRC="button.2bp">

If you want to use screen shots in your Help file, use the Windows CE Zoom application. This application ships with the Windows CE Toolkit. You can use this application to take a screenshot from a connected Windows CE–based device.


Separating Help Topics

Help for Windows CE uses the <!-- PegHelp --!> tag to separate each topic in a Help file. There is a space on both sides of the word PegHelp. Place this tag at the end of a topic to mark the section of text as an individual topic in Help for Windows CE.

Note Use some sort of tag, such as <!-- **Topic Break** --!>, to mark topic breaks. While not necessary, tags such as this can help you locate a topic in HTML code more easily.


Creating an Index

An index helps the user navigate through a Help file by listing topics alphabetically. Most OEMs create a general index for the applications that ship on their platform. Do not attempt to alter this index. Instead, create your index in your Help file.

The following code example shows a five-word index.

<A NAME="index"></A><B>Index</B>
<A HREF="inkwrite.htm#pagestyle">backgrounds, yellow</A><BR>
<A HREF="inkwrite.htm#create_a_bulleted_list">bulleted lists</A><BR>
<A HREF="inkwrite.htm#DocCreate">documents, creating</A><BR>
<A HREF="inkwrite.htm#DocWork">documents, editing</A><BR>
<A HREF="inkwrite.htm#mailrecipient">documents, e-mailing</A><BR>
<A HREF="inkwrite.htm#pagestyle">documents, page style</A><BR>
<A HREF="inkwrite.htm#printingdocument">documents, printing</a><br>

To save memory, do not create an index for every Help file. In general, create an index only if the table of contents for your Help file has 10 or more topics.


HTML Topic Example

The following code example shows a topic from Solitaire Help.

<A NAME ="setting_solitaire_options"></A><B>Setting Solitaire
Options</B>

<OL>
<LI>Tap the <b>Options</B> button.<BR>
<IMG SRC="_optonB.2bp">
<LI>Select desired options. See <A
HREF="wince.htm#view_toolbar_button">context help</A> for details.
</ol>

< B>See Also</ B><BR>
<A HREF="soltr.htm#playing_the_game">Playing the Game</A><BR>
<A HREF="soltr.htm#solitaire_scoring_systems">Solitaire Scoring
Systems</A>

<BR CLEAR=ALL >
<!-- PegHelp -->
<HR>
<!-- **********Topic Break********** -->

The following illustration shows the preceding code example on the Palm-size PC.


Testing a Help File

When you finish adding content to your Help for Windows CE file, test the links in the file. Because Help for Windows CE requires you to insert the name of the file in a jump, some link validators do not work correctly with Help for Windows CE files.

To test Help for Windows CE files in an incompatible link validator

  1. Create a copy of your Help file.
  2. In this copy, replace all instances of .htm# with #.
  3. Run the link validator on the copied file.

    This test will show any errors both in the copied file and the original file.

  4. Make any changes you need in the original file.


Adding Help to an Application

Once you test your Help file, link the Help file to an application in one of two ways: through the All Topics list or through context-sensitive Help.


Adding a Help File to the All Topics List

The All Topics list is a list of HTML links to all the available Help files on a Windows CE platform. The All Topics list for Help for Windows CE versions 2.0 and 2.01 is a separate HTML file that lists links to all the Help files on the Windows CE platform. The manufacturer creates this file; do not alter it. Version 2.10 creates an All Topics list dynamically by selecting the name of the first topic in each Help file and a corresponding link (.lnk) file.

To add a topic to the All Topics list for version 2.10

  1. Name the first topic in each Help file Main_Contents.

    Place this name only in files that you want to include in the table of contents.

  2. Include “Main_Contents” in the <META> tag for each of these files.

    The following code example shows how to include a topic in the dynamic table of contents.

    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<HTML>
<HEAD>
<META HTTP-EQUIV="Htm-Help" CONTENT="soltr.htm#Main_Contents">
<TITLE>Solitaire Help</TITLE>
</HEAD>
<BODY BGCOLOR="#FFFFFF" TEXT="#000000">
<!-- PegHelp -->
<P><A NAME="Main_Contents"></A><B>Solitaire Help</B></P>
<A HREF="soltr.htm#playing_the_game">Playing the Game</A><BR>
<A HREF="soltr.htm#setting_solitaire_options">Setting Solitaire
Options</A><BR>
<A HREF="soltr.htm#solitaire_scoring_systems">Solitaire Scoring
Systems</A><BR>
<BR CLEAR=ALL>
<!-- PegHelp --><HR>
<!-- ************************Topic Break************************* -->
  3. Create a link file for each Help file in the table of contents.

    Create a link file with NotePad or another ASCII editor. Name the link file the way you want Help to display the link in the table of contents. For example, the Solitaire Help file is Soltr.htm and the link file is Solitaire.lnk. The link in the table of contents is Solitaire. You can place spaces in the link file name. For example, the link file name for Pocket Word is Pocket Word.lnk.

    The text in the link file tells Help for Windows CE where to aim the link in the table of contents. The following code example is one entry in the Solitaire.lnk file:

    18#\windows\soltr.htm

    The number 18 in this example represents the number of characters in \Windows\Soltr.htm

  4. During installation, install your link file in the \Windows\Help directory.

    Install the Help file anywhere you want in the Windows CE platform.


Creating Context-Sensitive Help

The most common way to call Help in Help for Windows CE versions 2.0 and 2.10 is from the ? button in the upper-right corner of the application screen or dialog box. The user accesses Help in version 2.01 by choosing Help on the Start menu.

The following code example shows how to call Help from an application by using the CreateProcess function.

CreateProcess(TEXT,"peghelp.exe",
TEXT("file:inkwrite.htm#font_and_ink"), NULL, NULL, FALSE, 0, NULL,
NULL, NULL, NULL;

Pass the “peghelp.exe” string in as the first parameter, using the TEXT macro first to convert the string to Unicode. Use TEXT on the second argument, this time passing a string containing the file name of the contents or specific Help topic that you want to display. Set all other parameters to NULL, FALSE, or 0, respectively.


Working with the Spelling Checker

Windows CE provides a spelling checker engine. Unlike the Help engine, the spelling checker is integrated into the Windows CE operating system as a module. Also unlike the Help engine, the spelling checker does not have any UI. Instead, your application passes text strings directly to the spelling checker. The spelling checker searches supplied dictionaries and returns suggestions for correct spelling. You must decide how to display this data to users.

To integrate the spelling checker engine into an application

  1. Initialize the spelling checker engine with a call to the SplInit function.

    Use the call to SplInit to add any user-defined spelling dictionaries and to initialize user settings.

  2. Use the SplCheck, SplSuggest, SplOptionSet, SplReplace, and SplLimitSet functions to check spelling, suggest alternate words, and replace misspellings.
  3. When you are finished, close the spelling checker engine with a call to the SplQuit function.


Initializing the Spelling Checker

Before you can use the spelling checker in your application, call the SplInit function to initialize a spelling session. SplInit performs the following tasks to initialize the spelling checker engine:

  • Creates a handle for the spelling session

    Your application uses this handle to associate a spelling checker function call to a specific spelling session.

  • Loads the main, internal, and external user dictionaries


Creating the Spelling Checker Handle

A spelling session is defined as all of the resources that the spelling checker is using for a particular application, including any dictionaries and created structures. The first instruction your spelling checker performs is to create a spelling checker handle. Your application uses this handle to associate a spelling checker function call to a specific spelling session. Because a device may have several applications using the spelling checker engine, all of the functions in the spelling checker API use the spelling checker handle to distinguish different spelling sessions.


Loading the Dictionaries

Once SplInit creates the spelling checker handle, the function loads the four required dictionaries and any other optional dictionaries:

  • First, SplInit loads the read-only main dictionary. Microsoft defines the main dictionary for each localized version of Windows CE. Currently, Windows CE does not support replacing the main directory with a dictionary supplied by an application. However, Windows CE can specify additional dictionaries to load while initializing the spelling checker.
  • Next, in addition to loading the main directory, SplInit creates three internal user dictionaries for each spelling session. SplInit creates these dictionaries empty. During a spelling session, you can add to and modify these dictionaries. However, Windows CE does not specifically save these dictionaries when you exit a spelling session.

You can instruct SplInit to load external user dictionaries at the beginning of a spelling session. An external user dictionary is a dictionary that is defined by an application, user, or other third party. A common use of an external user directory is to store unique words that are not found in the main directory but are commonly used by an individual user. You can save the external dictionary at the end of a spelling session, and load it again at the beginning of the next spelling session.

Use the ppwz parameter of SplInit to point to a list of external user dictionaries. Each element on the list includes the dictionary path and file name. SplInit assigns identifiers to the internal user dictionaries and to any external user dictionaries pointed to by ppwsz.

The following table describes the values you can provide in the ppwz parameter.

Dictionary name
nID value

Description
SPL_IGNORE_LEX | –3 | Contains words that the spelling checker finds, but the user does not want to change. The spelling checker ignores words in this list for the remainder of the spelling session.
SPL_CHANGE_ALWAYS_LEX | –2 | Contains words that the spelling checker finds, paired with words with which the spelling checker replaces subsequent occurrences of the word. For example, thru paired with through.
SPL_CHANGE_ONCE | –1 | Contains words that the spelling checker finds and the user changes. The spelling checker continues to find subsequent occurrences of these words.
External user dictionary 1 | 0 | The first external user dictionary described in ppwz.
External user dictionary 2 | 1 | The second external user directory described in ppwz.
External user dictionary clex | clex-1 | The last external user directory described in ppwz.

Use the nID value to direct the spelling checker API to a specific dictionary. The clex value is the total number of external dictionaries you provide in the ppwz parameter.


Initializing the Spelling Session with Single and Multiple Applications

Initializing a spelling session requires the device to load a set of data into active memory. Further, the initialization process might reserve a large amount of resources. Monopolizing resources might not be a problem if a user is running only one application on the device. However, you need to take into account the possibility that other applications may need to use the spelling checker at the same time as your application.

In situations where you can be certain that only one application uses the spelling checker, initialize the spelling session when the application starts. Initializing the spelling checker at the beginning of an application means that you can avoid unnecessary processing in the middle of your application. Similarly, keep the spelling session active until the application ends.

Initializing a spelling session is different when you program in a multiple-application environment. A spelling session has exclusive access to external user directories until the calling application closes the spelling session. This means that a second application cannot load external user dictionaries already in use. Therefore, if your application runs in an environment where other applications use the spelling checker, you may want to initialize the spelling checker when the application requests it. Initializing the spelling checker only when an application needs it reduces the number of external user dictionaries tied up in any given application. When the application is done with the spelling checker, call the SplQuit function to release the dictionaries so that other applications can use them.

The main directory is available for multiple spelling sessions because it is read-only. The internal user directories are created for each spelling session and are removed at the end of each spelling session. Therefore, access to the internal user dictionaries by multiple spelling sessions is not a concern.


Setting the Spelling Session Options

After initializing a spelling session, call the SplOptionSet function to customize the Spelling Checker operations. For example:

  • Use SplOptionSet to instruct the spelling checker to find repeating words, ignore Roman numerals, or ignore capitalization errors.
  • Call SplOptionGet to retrieve the current settings of the spelling session. You can then display the options to the user, and change the options with a call to SplOptionSet. By default, all options are turned off at the beginning of the spelling session.


Using the Spelling Checker

Once you initialize the spelling session and set the spelling checker options, you can call a variety of functions to check spelling and suggest alternate words to use.

To use the spelling checker

  1. Set up the SPLBUFFER structure.

    SPLBUFFER contains data concerning the words to check, the location of the input and output buffers, and the status of the spelling session in progress.

  2. Perform the spelling check with a call to the SplCheck or SplReplace functions:
    • SplCheck checks the input buffer of SPLBUFFER with the dictionaries.
    • SplReplace checks the input buffer against the dictionaries and suggests a limited number of possible alternative spellings.
  3. If you called SplCheck, call SplSuggest to receive more possible alternative spellings.
  4. If necessary, alter the spelling of the misspelled word or ignore the suggestion and move to the next word.


Setting Up the SPLBUFFER Structure

Before you call SplCheck or SplSuggest, set up the SPLBUFFER structure. A SPLBUFFER structure contains a variety of data for the spelling checker functions. First, it contains the input buffer that holds the text that you are checking. It also contains parameters for containing spelling suggestions returned by the spelling checker, the type of spelling error that occurred, where in the input buffer the spelling checker should look next, and how the spelling checker should proceed.

To set up the SPLBUFFER structure for a call to SplCheck

  1. Set pwzIn to point to the buffer containing the text that you want to check the spelling of.
  2. Set IwcIn to 0.

    IIwcIn contains the offset for where SplCheck begins searching for misspelled words. Setting IwcIn to 0 causes SplCheck to begin searching at the beginning of the input buffer.

  3. Set cwcIn to the size of the input buffer relative to pwzIn.
  4. Set dwMode to SPL_NO_STATE_INFO or SPL_STARTS_SENTENCE.
    • Use SPL_NO_STATE_INFO if you do not know anything about the contents of the input buffer.
    • Use SPL_STARTS_SENTENCE if you know that the input buffer contains text that starts at the beginning of a sentence.
  5. Set dwError to receive an error code from SplCheck.

    SplCheck places the results of the spelling session in dwError.

  6. Set iwcErr to receive the index of the location of the spelling error, if such an error exists.
  7. Set cwcErr to receive the length of the word containing the spelling error, if such an error exists.


Performing a Spelling Check with the SplCheck Function

Once you have set SPLBUFFER, call SplCheck. SplCheck finds the first word in the input buffer and searches the main dictionary, internal user dictionaries, and external dictionaries for that word.

If the spelling checker finds that the word is spelled correctly, it moves on to the next word. If SplCheck finds a misspelled word, it returns the location of the word in the buffer, the length of the word, and an error code describing the general type of misspelling. If the spelling checker gets to the end of the input buffer without finding a spelling error, it returns SPL_OK to SPLBUFFER.


Performing a Spelling Check with the SplReplace Function

You can also perform a spelling check with the SplReplace function. In addition to checking the spelling of the text in the input buffer, SplReplace returns a list of suggested replacements in the output buffer. However, SplReplace returns only a limited list of possible suggestions. You can define the limits placed on SplReplace with the SplLimitSet function. A common use of SplLimitSet is to limit the suggestions to obvious mistakes, such as suggesting “the” for “teh.”

Note Because the spelling checker API is designed for a variety of purposes, SplReplace does not replace the checked word with a suggestion. You need to add this capability to your application.


Receiving Spelling Suggestions from the Spelling Checker

Once you have determined that a word in the input buffer of SPLBUFFER is misspelled, call SplSuggest. Like SplReplace, SplSuggest returns one or more suggestions for a misspelled word. However, SplReplace can return up to eight possible suggestions.

In addition to returning spelling suggestions, SplSuggest can also score the suggestions. A score is a number that indicates how much a replacement word differs from the original misspelled word. A low score indicates that the misspelled word was changed slightly, while a high score indicates that the word was changed a great deal. You can use suggestion scores in your application to exclude some words from the list of alternatives.

Once SplSuggest finishes processing, you can access the output buffer through the aspl member of SPLBUFFER. The aspl member is an array of SPLSUGGEST structures. The first member of a SPLSUGGEST structure points to a word in the output buffer. If you chose to use scores, the second member contains a score for that word.

To receive spelling suggestions from the spelling checker

  1. If you want to use scores, set SPL_SCORE_SUGGESTIONS with SplSetOptions.
  2. Set the size and location of the output buffer with the cwcOut and pwszOut parameters of SPLBUFFER.

    This SPLBUFFER is the same structure returned by your original call to SplCheck.

  3. Set the aspl, cwcUsed, and cspl parameters of the SPLBUFFER structure to receive the appropriate data.

    SplSuggest returns an array of pointers to the output buffer in aspl, the size of the output buffer used in cwcUsed, and the number of suggestions in cspl.

  4. Call SplSuggest.


Changing a Spelling Error in Your Application

Once you receive the suggested spellings, you must implement the actual change. A common practice is to alter the misspelled word in the input buffer. When you finish the entire spelling check on the text string in the input buffer, you can then make any modifications to the actual information.


Ignoring a Spelling Error or Moving to the Next Word

There may be times when you want to skip an error without correcting it. For example, a user may type in his or her name and choose not to add it to one of the dictionaries. Because the name may not be in any defined dictionary, it will show up as a spelling error. Therefore, you need to instruct the spelling checker to skip over a word. Use the same technique to move to the next misspelled word in the input buffer.

To ignore a spelling error or move to the next word

  1. To continue with a spelling check:
    • If you are skipping a misspelled word, set the dwMode parameter of SPLBUFFER to SPL_IS_CONTINUED.

      SPL_IS_CONTINUED instructs the spelling checker to ignore the spelling error.

    • If you are continuing with the spelling check after correcting a misspelled word, set dwMode to SPL_IS_EDITED_CHANGE.
  2. Call SplCheck.

    SplCheck will replace the value contained in iwcIn with the value contained in iwcErr plus the value contained in cwcEIrr. SplCheck continues the spelling check just after the misspelled word.


Modifying External and Internal Dictionary Lists

There may be times when you need to check the spelling of words that are not in the main dictionary or in the external user-defined dictionary. For example, a user may want to enter his or her name, or may want to use a variety of technical language in a specific document. You can modify both the internal and external dictionary lists to suit the needs of your users.

The internal and external user dictionaries are lists of null-terminated words. Two null characters in a row indicate the end of a dictionary. The following code example shows a series of possible listings from a user dictionary:

…thier\0their\0wierd\0weird\0\0

Words are listed with the incorrect word first and the correct word second. This is the format that the SPL_CHANGE_ALWAYS and SPL_CHANGE_ONCE internal dictionaries use.

The following table describes the functions that you can use during a spelling session to modify internal and external user dictionaries.

Function
Description
SplAddUserDict | Adds a single word or word pair to the specified dictionary
SplRemUserDict | Removes a single word or word pair from the specified dictionary
SplClrUserDict | Clears all entries from the specified dictionary

SplAddUserDict, SplRemUseDict, and SplClrUserDict all use the spelling dictionary identifiers specified in the ppwz parameter of SplInit.

The SplEnumUserDict function enumerates the contents of a specified internal or external user dictionary. The function writes the dictionary contents to the output buffer specified by the pwszOut parameter in the SPLBUFFER structure. One use of enumeration is to present a dictionary's contents to a user, who can then select words to be added and removed with SplAddUserDict and SplRemUserDict.


Ending the Spelling Session

Once you finish with the spelling checker, call the SplQuit function to end the spelling session. SplQuit performs the following tasks:

  • Invalidates the spelling session handle that was created in the SplInit function call
  • Frees all resources that were allocated by SplInit
  • Deletes the internal user dictionaries
  • Saves external user dictionaries to the files that are specified in the SplInit parameters