Services
Before Windows CE .NET 4.0, Windows CE did not have the concept of a service. To make up for the lack of service support, so-called device drivers were written, not to interface with hardware, but rather to manage some software interface such as a telnet server. The problem with this design was that these services ran in the same process as most of the other device drivers. If there was a bug in the service code, the service could corrupt a device driver, some of which are critical to the operation of the system.Starting with Windows CE .NET 4.0, a new component was provided by the operating system, the Services Manager. The Services Manager is quite similar in design to the Device Manager; it loads services when the operating system boots by looking at a list in the registry, the manager can also load services upon request from an application, and finally, it expects the service to be implemented as a DLL with the same 10 external entry points expected of a Windows CE device driver.In addition to the similarities, the Services Manager has a quite convenient capability beyond the Device Manager. The Services Manager implements a super service that monitors upon request connections to TCP/IP ports on the device. Because many of the services implemented for Windows CE are server related, such as a telnet server or Web server, the super service alleviates the need for a number of services to create a thread and open a socket just to monitor a port. Instead, the super service does this and notifies the service that has requested monitoring when the port is connected.
Service Architecture
The architecture of a Windows CE service belies the history of using device drivers as service providers under Windows CE. A Windows CE service is a DLL that is constructed almost identically to a stream device driver. Like a stream driver, a Windows CE service exports the same 10 entry points, from xxx_Init to xxx_PowerDown. Also, like a stream driver, a service has a three-character prefix that, along with an instance number, is used to identify the loaded service.One convenient characteristic of a service is that the Services Manager doesn't require that all 10 stream functions be exported from a service. If the service isn't intended to be exposed as an interface by the standard stream functions, the service only needs to export xxx_Init, xxx_Deinit, and xxx_IOControl. Although this arrangement generally just saves writing and exporting a handful of null functions, it's still a handy feature.To aid in debugging, or in the rare cases where a service needs to be isolated in its own process space, a service can be loaded in a standalone copy of the Services Manager. When run in an isolated process, the service isn't enumerated with the other services. Also, the service can't be opened by other applications using the File API. It's preferable to avoid using isolated services, not just because of the limited functionality, but also because the isolated copy of the Services Manager uses an addition process slot.
The Life of a Service
Services are always in one of two basic states, started or stopped, or transitioning between these two states. When stopped, the service should not respond to net connections or perform any local processing that the service was designed to support. The service can be programmatically started and stopped by applications with an IOCTL command.
Services can be loaded on reset or manually loaded by an application. To load a service automatically on reset, add a registry key under the key [HKEY_LOCAL_MACHINE]\Services. The name of the key created is used by the Services Manager to identify the service. The contents of the key are quite similar to the contents of a device driver key. The same values used in device keys—DLL, Prefix, Context, and so forth—are used in the Services key. Figure 22-4 shows the registry key for the OBEX service.

Figure 22-4: The registry key for the OBEX service
There are a few differences between a registry entry for a device and a service. The service entry must contain the Index value that is optional for a device. Also, the Context value in the registry has a defined use. It's used to determine what state the service is in when it loads. Context can be one of the following values:
0Indicates that the service should auto-start itself.
1Indicates that the service is initially stopped. If a Super Service key is present, the super service is automatically started.
2Indicates that the service will be loaded in a standalone copy of the Services Manager.
These values correspond to the values SERVICE_INIT_STARTED, SERVICE_INIT_STOPPED, and SERVICE_INIT_STANDALONE discussed in the service Init routine on page 1088.
However the service is loaded, the Services Manager will load the DLL implementing the service into its process space. Using the information gathered from the registry or the RegisterService function, the Services Manager uses the prefix to generate the names of the entry points to the service and uses GetProcAddress to get their addresses. Aside from the required Init, Deinit, and IOControl entry points, pointers to any of the other entry points that aren't found are simply redirected to a dummy routine that returns the error code ERROR_NOT_SUPPORTED.Once the DLL is loaded, the service's Init function is called. The single parameter is the Context value either read from the registry or passed in the RegisterService function. If the service returns a nonzero value, the Init call is deemed to be a success and the service is then added to the chain of active services.The service can be started and stopped by sending it IOCTL commands using either ServiceIoControl, or, if the service supports the full stream function set, DeviceIoControl. If the service receives a start command and it's currently stopped, it should start any processing that is the task of the service. If running, the service can be stopped by another IOCTL command. A stopped service isn't unloaded. Instead, it waits in memory until restarted or unloaded. Aside from stopping super service support, the Services Manager doesn't prevent a stopped service from performing any action. It's up to the service to heed the start and stop commands.When the service is requested to be unloaded, the Services Manager sends an IOCTL command to the service asking if it can be unloaded. If the service refuses, the service remains in memory and the unload command fails. Otherwise, the Deinit function of the service is called, and the DLL is unloaded from the process space of the Services Manager.
Application Control of a Service
Applications can load, unload, and communicate to a service using a series of dedicated functions. An application can load a service using one of two calls. If the service has a registry key defined, the function ActivateService function can be used. ActivateService is defined as
HANDLE ActivateService (LPCWSTR lpszDevKey, DWORD dwClientInfo);
The first parameter is the name of the registry key that provides load information on the service. The registry key must be located under [HKEY_LOCAL_ MACHINE]\Services. The format of the key must be the same as mentioned earlier for service registry keys. The second parameter is reserved and must be set to zero.An application can also load a service with the function RegisterService. Like RegisterDevice, RegisterService doesn't require a registry entry for the service to load. The function is defined as
HANDLE RegisterService (LPCWSTR lpszType, DWORD dwIndex, LPCWSTR lpszLib,
DWORD dwInfo);
The parameters are quite similar to RegisterDevice; the prefix string of the service is passed in the first parameter, the index value in the second, the name of the DLL implementing the service in the third, and the context value, passed to the Init function, in the fourth parameter.The return value for RegisterService as well as ActivateService is the handle to the instance of the service. This value can be used later to send IOCTL commands to the service or unload the service.The handle to a service can also be obtained by calling GetServiceHandle defined as
HANDLE GetServiceHandle (LPWSTR szPrefix, LPWSTR szDllName,
DWORD *pdwDllBuf);
The first parameter, szPrefix, is somewhat misnamed because the parameter should be set to the complete name of the service, which is the prefix plus the instance number, as in SRV0:, not just SRV. The szDllName string receives the name of the DLL that implements the service. (If you don't need to know the name of the DLL implementing the service, szDllName can be NULL.) The pdwDllBuf parameter should point to a DWORD that initially contains the size of the szDllName buffer, in bytes.The service handle can be used to send IOCTL commands to a service using the function ServiceIoControl. Its definition is identical to the definition of DeviceIoControl, but it's listed here for convenience.
BOOL ServiceIoControl (HANDLE hService, DWORD dwIoControlCode, LPVOID lpInBuf,
DWORD nInBufSize, LPVOID lpOutBuf, DWORD nOutBufSize,
LPDWORD lpBytesReturned, LPOVERLAPPED lpOverlapped);
Both ServiceIoControl and DeviceIoControl can be used to send IOCTL commands to services. The difference is in the definition of the first parameter of both calls, the handle to the service. In ServiceIoControl, the handle is the service handle returned by ActivateService, RegisterService, or GetServiceHandle. For DeviceIoControl, the handle must be a valid file handle returned by CreateFile. If the service doesn't implement the functions, Open, CreateFile will fail leaving ServiceIoControl as the only method of sending IOCTL commands. Also, without a valid file handle, the other standard file functions, ReadFile and WriteFile, cannot be used.A list of the currently running services can be obtained with the EnumServices function. One limitation of this function is that services running in standalone copies of the Services Manager are not enumerated. EnumServices is defined as
BOOL EnumServices (PBYTE pBuffer, DWORD *pdwServiceEntries,
DWORD *pdwBufferLen);
The pBuffer parameter points to a buffer that will be filled with an array of ServiceEnumInfo structures combined with a series of strings containing the names of the DLLs implementing the services. The function places one ServiceEnumInfo structure for each service managed by the Services Manager. The pdwServiceEntries parameter points to a DWORD that will be filled with the number of ServiceEnumInfo structures placed in the buffer by the function. The pdwBufferLen parameter points to a DWORD that should be initialized with the size of the buffer pointed to by pBuffer. When the function returns, the value is set to the number of bytes placed in the buffer.The ServiceEnumInfo structure is defined as
typedef struct_ServiceEnumInfo {
WCHAR szPrefix[6];
WCHAR szDllName;
HANDLE hServiceHandle;
DWORD dwServiceState;
} ServiceEnumInfo;
Each instance of the structure describes one service. The somewhat misnamed szPrefix field contains the complete name of the service, as in SRV0:, which is a combination of the three-character service prefix along with its instance number and a trailing colon. The szDllName field points to a string naming the DLL implementing the service. The hServiceHandle field contains the handle of the service, whereas the dwServiceState field contains the current state, running, stopped, and so forth, of the service.A service can be unloaded with the function DeregisterService defined as
BOOL DeregisterService (HANDLE hDevice);
The only parameter is the handle to the service. The Services Manager will first ask the service if it can be unloaded. If the service assents, the service will be unloaded; otherwise, the function will fail.
The Service DLL Entry Points
Because the architecture of the services is so similar to a device driver, I'm only going to discuss the differences between the service and the driver. The first difference is in how the service is initialized, so let's look at the Init function.
xxx_Init
The Init function follows the legacy Init prototype as in
DWORD xxx_Init (DWORD dwData);
The only parameter is a flag indicating the initial state of the service. The parameter can contain one of the following flags: SERVICE_INIT_STARTED indicates the service should provide its own initialization to start the service, SERVICE_INIT_STOPPED indicates that the service is currently stopped but may be started by the super service, and SERVICE_INIT_STANDALONE indicates that the service has been loaded in a standalone copy of the Services Manager.Like a device driver, a service should perform any necessary initialization during the call to the Init function. If an error is discovered, the Init function should return a zero indicating that the service should fail to load. The Services Manager will then unload the DLL implementing the service. The Services Manager interprets any nonzero value as a successful initialization. Also, as with a driver, the service isn't really a service until the Init function returns. This means that the Init function can't make any call that expects the service to be up and running.
xxx_Deinit
The Deinit function is called when the service is unloaded. The prototype of Deinit shown here matches the device driver Deinit function.
DWORD xxx_Deinit (DWORD dwContext);
The only parameter is the value that was returned from the Init function.
xxx_IOControl
The IOControl function is much more structured than the similarly named counterpart in a device driver. Instead of being a generic call that the driver can use as it pleases, in a service the IOControl call must support a series of commands used both by the Services Manager and by applications communicating with the service.The prototype of the IOControl entry point is shown here.
DWORD xxx_IOControl (DWORD dwData, DWORD dwCode, PBYTE pBufIn, DWORD dwLenIn,
PBYTE pBufOut, DWORD dwLenOut, PDWORD pdwActualOut);
The parameters are the same as the ones used in xxx_IOControl for the device driver. The dwData parameter can either contain the value returned by the service's Open function or the value returned by the Init function. The service must be written to accept the value returned by Init or the values returned by both Init and Open if it implements an Open function. Because there is an extensive list of commands, they are discussed individually in the following section.
Other Entry Points
The other entry points to the driver, Open, Close, Read, Write, Seek, PowerUp, and PowerDown, are optional. However, if the service doesn't export at least Open and Close, applications will not be able to open the service to send it IOCTL commands through DeviceIoControl. The application could still use ServiceIoControl using the handle returned by GetServiceHandle.
The Service IOCTL Commands
A Windows CE service must field a series of IOCTL commands sent through the IOControl function. These commands can be grouped into a series of categories such as commands used to control the service, those used to query the state of the service, those commands used to help debug the service and those commands used for super service support.For each of the following commands, the service should return TRUE if the command was successful and FALSE if an error occurred. Extended error information should be sent by calling SetLastError before returning.
IOCTL_SERVICE_START
The first command, IOCTL_SERVICE_START, is sent to the service to start it. This command isn't sent by the system when the service is loaded. Instead, it's only sent by an application that wants to start a stopped service. If not already running, the service should make any connections or perform any initialization necessary to provide the service for which it was designed.If the service has registry entries that have the super service automatically start port monitoring, the super service will start and bind to the specified ports if this IOCTL command returns a nonzero value.
IOCTL_SERVICE_STOP
The IOCTL_SERVICE_STOP command is sent by applications to stop a currently running service. The service won't be unloaded from memory just because it was stopped.If the service has a super service running and the registry entry for the service is configured to auto-start a super service, the super service will be shut down if the service returns a nonzero value from this command.
IOCTL_SERVICE_REFRESH
The IOCTL_SERVICE_REFRESH command is sent by an application or the Services Manager to tell the service to reread its configuration data from the registry. Any changes in the configuration read should immediately be reflected in the service.
IOCTL_SERVICE_INSTALL
This optional command is sent to have the service modify the registry to have the service automatically started on reset. This command is similar in action to the DllRegisterServer function of a COM in-proc server. Although optional, the command is convenient to have because any installation program for the service will not have to have knowledge of the registry entries required by the service. The registry entries needed for auto-load are described later in the "Super Service" section.
IOCTL_SERVICE_UNINSTALL
The complement to the IOCTL_SERVICE_INSTALL command, also optional, is the IOCTL_SERVICE_UNINSTALL command, which removes the registry entries that cause the driver to load on boot. An install/remove application can use this command to have a service remove its own registry entries so that the application need not enumerate the registry to find the installation entries.This completes the list of IOCTL commands sent by applications; now let's look at the queries that are sent by both applications and the Services Manager to query the state of the service.
IOCTL_SERVICE_STATUS
The IOCTL_SERVICE_STATUS command is sent to query the state of the service. The state is returned in the output buffer pointed to by the pOut parameter of the IOControl call. The service should verify that pOut is nonzero and that dwOut indicates the buffer is large enough to hold a DWORD.The service state can be one of the following, rather self-explanatory, values.
SERVICE_STATE_OFF
SERVICE_STATE_ON
SERVICE_STATE_STARTING_UP
SERVICE_STATE_SHUTTING_DOWN
SERVICE_STATE_UNLOADING
SERVICE_STATE_UNINITIALIZED
SERVICE_STATE_UNKNOWN
IOCTL_SERVICE_QUERY_CAN_DEINIT
This command is sent by the Services Manager to ask the service whether it can be unloaded. This command is typically sent in response to an application calling DeregisterService. If the service can be unloaded, it should place a nonzero value in the first DWORD of the output buffer. If the service can't be unloaded, the DWORD should be set to zero. The service should verify that the output buffer exists and is large enough to hold the DWORD before writing the value.If the service can always uninstall, it doesn't have to respond to this command. If this command returns FALSE, which is the default response to any unhandled command, the Services Manager assumes that the service can be removed.
IOCTL_SERVICE_CONSOLE
This optional command is sent to have the service display a service console. A service does not have to implement this command, but it can be handy in some situations.The command is sent with a string in the input buffer. If the string is "On", or if the input buffer pointer is NULL, the service should display a service console. If the input buffer contains the string "Off", the service should remove the service console.
IOCTL_SERVICE_CONTROL
This command is basically the IOCTL of the IOCTL commands. That is, it's a generic command that can be used by the applications to communicate custom commands to the service. The format of the input and output buffers is defined by the service-defined command.
IOCTL_SERVICE_DEBUG
This command is sent to set the debug zone bitmask for the service. The first DWORD of the input buffer contains the new state for the zone bitmap. The service should verify that the input buffer exists and is at least a DWORD in size.Because the debug zone structure dpCurrParams is typically only defined for debug builds of the service, the code fielding this command is typically couched in an #ifdef block to prevent it from being compiled in a nondebug build.There are examples of where this command has been extended to perform debug duties beyond the settings of the zone mask. To extend the functionality, the service can use the size of the input buffer, specified in dwIn, to determine the meaning of the input buffer data. To be compatible, the service should default to setting the debug zone mask if dwIn is set to the size of a DWORD.
IOCTL_SERVICE_CALLBACK_FUNCTIONS
This command, introduced in Windows CE .NET 4.2, is used if the service is loaded with a standalone copy of the Services Manager. The input buffer points to a ServicesExeCallbackFunctions structure, defined as
typedef struct _ServicesExeCallbackFunctions {
PFN_SERVICE_SHUTDOWN pfnServiceShutdown;
} ServicesExeCallbackFunctions;
The only field in this structure is a pointer to a callback routine, in the Services Manager, that can be called to control the standalone copy of the Services Manager. The structure currently provides one field that points to a function that will shut down the standalone Services Manager.
IOCTL_SERVICE_SUPPORTED_OPTIONS
This command, supported in Windows CE .NET 4.2 and later, queries the currently supported options of the service. The option flags are returned in a DWORD in the output buffer.
Super Service
The super service provides all services with a convenient method for monitoring TCP/IP ports without having to have customized code to monitor the port inside the service. The super service can either work automatically, if the proper registry settings are in place for the service, or manually through a series of function calls. It's more convenient to use the registry method for configuring the super service, so I will cover that method first.If the service wants the super service to start automatically when the service is loaded, a subkey, named Accept, must be present under the service's key. Under the Accept key, there should be one or more subkeys each providing the IP address of port to monitor. The Services Manager doesn't use the name of the subkey under the Accept key, although the key is traditionally named TCP-xxx, where xxx is the port number to be monitored. Each subkey should contain a binary value named SockAddr. The data in SockAddr should comprise bytes that make up a SOCKADDR structure that describes the port being monitored. The subkey can optionally contain a Protocol value that specifies the protocol for the socket. If this value isn't present, the protocol value is assumed to be zero. The following code initializes a SOCKADDR structure and then writes it to the registry.
int AddRegSuperServ (HKEY hKey, WORD wPort, DWORD dwProtocol) {
SOCKADDR_IN sa;
HKEY hSubKey;
TCHAR szKeyName[128];
DWORD dw;
int rc;
memset (&sa, 0, sizeof (sa));
sa.sin_family = AF_INET;
sa.sin_port = htons(wPort);
sa.sin_addr.s_addr = INADDR_ANY;
// Create accept key for this service
wsprintf (szKeyName, TEXT("Accept\\TCP-%d"), wPort);
rc = RegCreateKeyEx (hKey, szKeyName, 0, NULL, 0, NULL,
NULL, &hSubKey, &dw);
if (rc == ERROR_SUCCESS)
rc = RegSetValueEx (hSubKey, TEXT("SockAddr"), 0, REG_BINARY,
(PBYTE)&sa, sizeof (sa));
rc = RegSetValueEx (hSubKey, TEXT("Protocol"), 0, REG_DWORD,
(PBYTE)&dwProtocol, sizeof (DWORD));
return rc;
}
As we will soon see, the ServiceAddPort function has the capability to create this registry key as well. It's still handy to be able to write the key manually in the case in which the service doesn't want to start the super service when it's writing the key.In addition to the Accept keys, the registry entry for the service must have a Context value of 1. If the Context value is 0, the super service will not start, nor will it start if the service is loaded in a standalone copy of the Services Manager.When a service is started either during system startup or with the ActivateService function, the service is loaded, its Init function is called, and then, if the Context value is 1, the super service queries the service through an IOCTL command to determine whether it wants super service support. If so, the super service enumerates the Accept keys and creates sockets to monitor the ports described in the keys. As each socket is opened and bound to the appropriate address, the service is notified, through an IOCTL command, that the socket is being monitored. Then, once all sockets are opened, and if the service is first being loaded, it sends a final IOCTL indicating that all the sockets are listening.
When a connection is made to one of the listening sockets, another IOCTL command is sent to the service along with the socket handle of the connection. The service then must create a new thread to handle the communication with the socket. The IOCTL call must return quickly because the calling thread is necessary for monitoring other ports. After the communication is complete, the service should close the socket handle passed during the connection notification. When the service shuts down, IOCTL commands are sent to the service notifying it that the sockets monitoring the ports have been closed.
Programmatically Controlling the Super Service
It's possible to have super service support without entries in the registry but it's more complicated. In this scheme, the service must tell the super service about each port to be monitored. This can be done with the function ServiceAddPort, defined as
ServiceAddPort (HANDLE hService, SOCKADDR pSockAddr, INT cbSockAddr,
INT iProtocol, WCHAR szRegWritePath);
The first parameter is the handle to the service, which, ironically, is somewhat difficult for the service to get. The SOCKADDR structure should be initialized with the address information for the listening socket. The iProtocol value should contain the protocol to be used by the socket. The szRegWritePath parameter can optionally specify a registry key name where this information will be written so that the next time the service is started, the super service will start automatically.The issue with a service getting its own handle is that GetServiceHandle requires not just the three-character prefix of the service but also the instance number of the service. If the service was loaded with RegisterService, determining the service instance isn't easy. If, however, the service was loaded because of a registry key entry, the instance value is specified in the registry. Of course, if the service was loaded due to a registry entry, it's just as convenient to have the registry key also specify that the super service automatically start.A specific port can be closed for monitoring by calling the ServiceClosePort function. Its prototype is
BOOL ServiceClosePort (HANDLE hService, SOCKADDR* pSockAddr, int cbSockAddr,
int iProtocol, BOOL fRemoveFromRegistry);
The parameters are identical to the ServiceAddPort function with the exception of the last parameter, fRemoveFromRegistry, which is a Boolean flag that tells the function whether the corresponding registry entry should be removed for the port.To close all the ports being monitored by a service, ServiceUnbindPorts can be used.
BOOL ServiceUnbindPorts (HANDLE hService);
The only parameter is the handle to the service.
SuperService IOCTLs
Services that use the super service must respond to a series of additional IOCTL commands. These commands are either queries to check for support or are notifications indicating an event has occurred within the super service.
IOCTL_SERVICE_REGISTER_SOCKADDR
This command is sent at least twice during the initialization of the super service. The super service first sends this command to query whether the service will accept super service support. In this case, the input buffer pointer, pIn, is NULL.The super service next sends this command again, once for each port the service is monitoring to verify the socket has been created to monitor the requested address. During these subsequent calls to verify the individual addresses, pIn points to a SOCKADDR structure that describes the socket address being monitored.
IOCTL_SERVICE_STARTED
This notification is sent when the super service completes its initialization after the service is first loaded. When this notification has been received, the service can assume that the super service is listening on all the ports requested. This notification isn't sent when the service is restarted after it has been stopped.
IOCTL_SERVICE_DEREGISTER_SOCKADDR
This notification is sent after the super service has closed the socket monitoring a given socket address. The pIn parameter points to the SOCKADDR structure that describes the socket address. This notification can be sent if the service is being stopped or because the service is being unloaded.
IOCTL_SERVICE_CONNECTION
The IOCTL_SERVICE_CONNECTION notification is sent when another application connects to the socket address being monitored by the super service. The input parameter pIn points to a socket handle for the connected socket. It's the responsibility of the service to spawn a thread to handle communication on this socket. The service must also close the socket when communication is complete.
IOCTL_SERVICE_NOTIFY_ADDR_CHANGE
For systems running Windows CE .NET 4.1 or later, this notification is sent if the system's IP address changes. The input buffer is filled with an IP_ADAPTER_ INFO structure defined as
typedef struct _IP_ADAPTER_INFO {
struct _IP_ADAPTER_INFO* Next;
DWORD ComboIndex;
Char AdapterName[MAX_ADAPTER_NAME_LENGTH + 4];
char Description[MAX_ADAPTER_DESCRIPTION_LENGTH + 4];
UINT AddressLength;
BYTE Address[MAX_ADAPTER_ADDRESS_LENGTH];
DWORD Index;
UINT Type;
UINT DhcpEnabled;
PIP_ADDR_STRING CurrentIpAddress;
IP_ADDR_STRING IpAddressList;
IP_ADDR_STRING GatewayList;
IP_ADDR_STRING DhcpServer;
BOOL HaveWins;
IP_ADDR_STRING PrimaryWinsServer;
IP_ADDR_STRING SecondaryWinsServer;
time_t LeaseObtained;
time_t LeaseExpires;
} IP_ADAPTER_INFO, *PIP_ADAPTER_INFO;
The fairly self-explanatory IP_ADDRESS_INFO structure contains everything from the IP address of the system to gateway, Dynamic Host Configuration Protocol (DHCP), and Windows Internet Naming Service (WINS) information.
Services.exe Command Line
In addition to being the Services Manager for the system, the application services.exe also has a command-line interface. For systems with a console, simply type:
services help
This command produces a list of the available commands. Services can list the current services, start them and stop them, load them and unload them, and even add and remove them from them from the registry.For systems without console support, services can be launched with an –f command-line switch and the name of a file to send the output to, as in
services –f Outfile.txt
Other command-line parameters include –d to send the output to the debug serial port and –q to suppress output entirely.
TickSrv Example Service
The TickSrv example demonstrates a service that uses the super service. TickSrv monitors port 1000 on a Windows CE device and, for any application that connects, provides the current tick count and the number of milliseconds the system has been running since it was reset. TickSrv is implemented as a standard Windows CE service. Because there is no reason for a local application to use the service, it doesn't implement the standard stream exports, Open, Close, Read, Write, or Seek. The source code for TickSrv is shown in Listing 22-2.Listing 22-2: The TickSrv example
TickSrv.h
//======================================================================
// Header file
//
// Written for the book Programming Windows CE
// Copyright (C) 2003 Douglas Boling
//======================================================================
#define dim(a) (sizeof (a)/sizeof(a[0]))
//
// Declare the external entry points here. Use declspec so we don't
// need a .def file. Bracketed with extern C to avoid mangling in C++.
//
#ifdef __cplusplus
extern "C" {
#endif //__cplusplus
__declspec(dllexport) DWORD TCK_Init (DWORD dwContext);
__declspec(dllexport) BOOL TCK_Deinit (DWORD dwContext);
__declspec(dllexport) DWORD TCK_IOControl (DWORD dwOpen, DWORD dwCode,
PBYTE pIn, DWORD dwIn,
PBYTE pOut, DWORD dwOut,
DWORD *pdwBytesWritten);
__declspec(dllexport) void TCK_PowerDown (DWORD dwContext);
__declspec(dllexport) void TCK_PowerUp (DWORD dwContext);
#ifdef __cplusplus
} // extern "C"
#endif //__cplusplus
// Suppress warnings by declaring the undeclared.
#ifndef GetCurrentPermissions
DWORD GetCurrentPermissions(void);
DWORD SetProcPermissions (DWORD);
DWORD GetCallerProcess(void);
PVOID MapPtrToProcess (PVOID, DWORD);
#endif //GetCurrentPermissions
int RegisterService (void);
int DeregisterService (void);
DWORD WINAPI AcceptThread (PVOID pArg);
//
// Service state structure
//
typedef struct {
DWORD dwSize; // Size of structure
CRITICAL_SECTION csData; // Crit Section protecting this struct
int servState; // Service state
} SRVCONTEXT, *PSRVCONTEXT;
TickSrv.cpp
//======================================================================
// TickSrv - Simple example service for Windows CE
//
// Written for the book Programming Windows CE
// Copyright (C) 2003 Douglas Boling
//======================================================================
#include <windows.h> // For all that Windows stuff
#include <winsock.h> // Socket support
#include "service.h" // Service includes
#include "TickSrv.h" // Local program includes
#define REGNAME TEXT("TickSrv") // Reg name under services key
#define PORTNUM 1000 // Port number to monitor
//
// Globals
//
HINSTANCE hInst; // DLL instance handle
//
// Debug zone support
//
#ifdef DEBUG
// Used as a prefix string for all debug zone messages.
#define DTAG TEXT ("TickSrv: ")
// Debug zone constants
#define ZONE_ERROR DEBUGZONE(0)
#define ZONE_WARNING DEBUGZONE(1)
#define ZONE_FUNC DEBUGZONE(2)
#define ZONE_INIT DEBUGZONE(3)
#define ZONE_DRVCALLS DEBUGZONE(4)
#define ZONE_IOCTLS DEBUGZONE(5)
#define ZONE_THREAD DEBUGZONE(6)
#define ZONE_EXENTRY (ZONE_FUNC | ZONE_DRVCALLS)
// Debug zone structure
DBGPARAM dpCurSettings = {
TEXT("TickSrv"), {
TEXT("Errors"),TEXT("Warnings"),TEXT("Functions"),
TEXT("Init"),TEXT("Driver Calls"),TEXT("Undefined"),
TEXT("IOCtls"),TEXT("Thread"), TEXT("Undefined"),
TEXT("Undefined"),TEXT("Undefined"),TEXT("Undefined"),
TEXT("Undefined"),TEXT("Undefined"),TEXT("Undefined"),
TEXT("Undefined") },
0x0003
};
#endif //DEBUG
//======================================================================
// DllMain - DLL initialization entry point
//
BOOL WINAPI DllMain (HANDLE hinstDLL, DWORD dwReason,
LPVOID lpvReserved) {
hInst = (HINSTANCE)hinstDLL;
switch (dwReason) {
case DLL_PROCESS_ATTACH:
DEBUGREGISTER(hInst);
// Improve performance by passing on thread attach calls
DisableThreadLibraryCalls (hInst);
break;
case DLL_PROCESS_DETACH:
DEBUGMSG(ZONE_INIT, (DTAG TEXT("DLL_PROCESS_DETACH\r\n")));
break;
}
return TRUE;
}
//======================================================================
// TCK_Init - Driver initialization function
//
DWORD TCK_Init (DWORD dwContext) {
PSRVCONTEXT pSrv;
DEBUGMSG (ZONE_INIT | ZONE_EXENTRY,
(DTAG TEXT("TCK_Init++ dwContext:%x\r\n"), dwContext));
// Init WinSock
WSADATA wsaData;
WSAStartup(0x101,&wsaData);
// Allocate a drive instance structure.
pSrv = (PSRVCONTEXT)LocalAlloc (LPTR, sizeof (SRVCONTEXT));
if (pSrv) {
// Initialize structure.
memset ((PBYTE) pSrv, 0, sizeof (SRVCONTEXT));
pSrv->dwSize = sizeof (SRVCONTEXT);
pSrv->servState = SERVICE_STATE_UNKNOWN;
InitializeCriticalSection (&pSrv->csData);
switch (dwContext) {
case SERVICE_INIT_STARTED:
pSrv->servState = SERVICE_STATE_ON;
break;
case SERVICE_INIT_STOPPED:
pSrv->servState = SERVICE_STATE_OFF;
break;
case SERVICE_INIT_STANDALONE:
break;
default:
break;
}
} else
DEBUGMSG (ZONE_INIT | ZONE_ERROR,
(DTAG TEXT("TCK_Init failure. Out of memory\r\n")));
DEBUGMSG (ZONE_FUNC, (DTAG TEXT("TCK_Init-- pSrv: %x\r\n"), pSrv));
return (DWORD)pSrv;
}
//======================================================================
// TCK_Deinit - Driver de-initialization function
//
BOOL TCK_Deinit (DWORD dwContext) {
PSRVCONTEXT pSrv = (PSRVCONTEXT) dwContext;
DEBUGMSG (ZONE_EXENTRY,
(DTAG TEXT("TCK_Deinit++ dwContex:%x\r\n"), dwContext));
if (pSrv && (pSrv->dwSize == sizeof (SRVCONTEXT))) {
// Free the driver state buffer.
LocalFree ((PBYTE)pSrv);
}
DEBUGMSG (ZONE_FUNC, (DTAG TEXT("TCK_Deinit--\r\n")));
return TRUE;
}
//======================================================================
// TCK_IOControl - Called when DeviceIOControl called
// ServiceEnumInfo
DWORD TCK_IOControl (DWORD dwOpen, DWORD dwCode, PBYTE pIn, DWORD dwIn,
PBYTE pOut, DWORD dwOut, DWORD *pdwBytesWritten) {
PSRVCONTEXT pSrv;
DWORD err = ERROR_INVALID_PARAMETER;
pSrv = (PSRVCONTEXT) dwOpen;
DEBUGMSG (ZONE_EXENTRY,
(DTAG TEXT("TCK_IOControl++ dwOpen: %x dwCode: %x %d\r\n"),
dwOpen, dwCode, pSrv->servState));
switch (dwCode) {
// -------------
// Commands
// -------------
// Cmd to start service
case IOCTL_SERVICE_START:
DEBUGMSG (ZONE_IOCTLS, (DTAG TEXT("IOCTL_SERVICE_START\r\n")));
EnterCriticalSection (&pSrv->csData);
if ((pSrv->servState == SERVICE_STATE_OFF) |
(pSrv->servState == SERVICE_STATE_UNKNOWN)) {
pSrv->servState = SERVICE_STATE_ON;
err = 0;
} else
err = ERROR_SERVICE_ALREADY_RUNNING;
LeaveCriticalSection (&pSrv->csData);
break;
// Cmd to stop service
case IOCTL_SERVICE_STOP:
DEBUGMSG (ZONE_IOCTLS, (DTAG TEXT("IOCTL_SERVICE_STOP\r\n")));
EnterCriticalSection (&pSrv->csData);
if ((pSrv->servState == SERVICE_STATE_ON)) {
pSrv->servState = SERVICE_STATE_SHUTTING_DOWN;
} else
err = ERROR_SERVICE_NOT_ACTIVE;
LeaveCriticalSection (&pSrv->csData);
break;
//Reread service reg setting
case IOCTL_SERVICE_REFRESH:
DEBUGMSG (ZONE_IOCTLS, (DTAG TEXT("IOCTL_SERVICE_REFRESH\r\n")));
// No settings in example service to read
break;
//Config registry for auto load on boot
case IOCTL_SERVICE_INSTALL:
DEBUGMSG (ZONE_IOCTLS, (DTAG TEXT("IOCTL_SERVICE_INSTALL\r\n")));
err = RegisterService();
break;
//Clear registry of auto load stuff
case IOCTL_SERVICE_UNINSTALL:
DEBUGMSG (ZONE_IOCTLS, (DTAG TEXT("IOCTL_SERVICE_UNINSTALL\r\n")));
err = DeregisterService();
break;
//Clear registry of auto load stuff
case IOCTL_SERVICE_CONTROL:
DEBUGMSG (ZONE_IOCTLS, (DTAG TEXT("IOCTL_SERVICE_CONTROL\r\n")));
err = 0;
break;
#ifdef DEBUG
// Set debug zones
case IOCTL_SERVICE_DEBUG:
DEBUGMSG (ZONE_IOCTLS, (DTAG TEXT("IOCTL_SERVICE_DEBUG\r\n")));
if (!pIn || (dwIn < sizeof (DWORD)))
break;
__try {
dpCurSettings.ulZoneMask = *(DWORD *)pIn;
err = 0;
}
__except (EXCEPTION_EXECUTE_HANDLER) {
;
}
#endif
// -------------
// Queries
// -------------
// Query for current service state
case IOCTL_SERVICE_STATUS:
DEBUGMSG (ZONE_IOCTLS, (DTAG TEXT("IOCTL_SERVICE_STATUS\r\n")));
if (!pOut || (dwOut < sizeof (DWORD)))
break;
__try {
*(DWORD *)pOut = pSrv->servState;
if (pdwBytesWritten)
*pdwBytesWritten = sizeof (DWORD);
err = 0;
}
__except (EXCEPTION_EXECUTE_HANDLER) {
;
}
break;
// Query for unload.
case IOCTL_SERVICE_QUERY_CAN_DEINIT:
DEBUGMSG (ZONE_IOCTLS,
(DTAG TEXT("IOCTL_SERVICE_QUERY_CAN_DEINIT\r\n")));
if (!pOut || (dwOut < sizeof (DWORD)))
break;
__try {
*(DWORD *)pOut = 1; // non-zero == Yes, can be unloaded.
if (pdwBytesWritten)
*pdwBytesWritten = sizeof (DWORD);
err = 0;
}
__except (EXCEPTION_EXECUTE_HANDLER) {
;
}
break;
// Query to see if sock address okay for monitoring
case IOCTL_SERVICE_REGISTER_SOCKADDR:
DEBUGMSG (ZONE_IOCTLS,
(DTAG TEXT("IOCTL_SERVICE_REGISTER_SOCKADDR\r\n")));
// Calling to see if service can accept super service help
if (!pIn || (dwIn < sizeof (DWORD))) {
if ((pSrv->servState == SERVICE_STATE_OFF) |
(pSrv->servState == SERVICE_STATE_UNKNOWN))
pSrv->servState = SERVICE_STATE_STARTING_UP;
err = 0;
break;
}
// Confirming a specific sock address
DEBUGMSG (ZONE_IOCTLS, (DTAG TEXT("Socket:%x\r\n"), *pIn));
err = 0;
break;
// -------------
// Notifications
// -------------
// Notify that sock address going away
case IOCTL_SERVICE_DEREGISTER_SOCKADDR:
DEBUGMSG (ZONE_IOCTLS,
(DTAG TEXT("IOCTL_SERVICE_DEREGISTER_SOCKADDR\r\n")));
EnterCriticalSection (&pSrv->csData);
if (pSrv->servState == SERVICE_STATE_SHUTTING_DOWN)
pSrv->servState = SERVICE_STATE_OFF;
LeaveCriticalSection (&pSrv->csData);
err = 0;
break;
// All super service ports open
case IOCTL_SERVICE_STARTED:
DEBUGMSG (ZONE_IOCTLS, (DTAG TEXT("IOCTL_SERVICE_STARTED\r\n")));
EnterCriticalSection (&pSrv->csData);
if ((pSrv->servState == SERVICE_STATE_STARTING_UP) |
(pSrv->servState == SERVICE_STATE_UNKNOWN))
pSrv->servState = SERVICE_STATE_ON;
LeaveCriticalSection (&pSrv->csData);
err = 0;
break;
// Notification that connect has occurred
case IOCTL_SERVICE_CONNECTION:
DEBUGMSG (ZONE_IOCTLS,
(DTAG TEXT("IOCTL_SERVICE_CONNECTION\r\n")));
if (!pIn || (dwIn < sizeof (DWORD)))
break;
// Create thread to handle the socket
CreateThread (NULL, 0, AcceptThread, (PVOID)*(DWORD*)pIn, 0,
NULL);
err = 0;
break;
default:
DEBUGMSG (ZONE_ERROR | ZONE_IOCTLS,
(DTAG TEXT("Unsupported IOCTL code %x (%d)\r\n"),
dwCode, (dwCode & 0x00ff) / 4));
return FALSE;
}
SetLastError (err);
DEBUGMSG (ZONE_FUNC, (DTAG TEXT("TCK_IOControl-- %d\r\n"), err));
return (err == 0) ? TRUE : FALSE;
}
//======================================================================
// TCK_PowerDown - Called when system suspends
//
// NOTE: No kernel calls, including debug messages, can be made from
// this call.
//
void TCK_PowerDown (DWORD dwContext) {
return;
}
//======================================================================
// TCK_PowerUp - Called when resumes
//
// NOTE: No kernel calls, including debug messages, can be made from
// this call.
//
void TCK_PowerUp (DWORD dwContext) {
return;
}
//----------------------------------------------------------------------
// AddRegString - Helper routine
//
int AddRegString (HKEY hKey, LPTSTR lpName, LPTSTR lpStr) {
return RegSetValueEx (hKey, lpName, 0, REG_SZ, (PBYTE)lpStr,
(lstrlen (lpStr) + 1) * sizeof (TCHAR));
}
//----------------------------------------------------------------------
// AddRegDW - Helper routine
//
int AddRegDW (HKEY hKey, LPTSTR lpName, DWORD dw) {
return RegSetValueEx (hKey, lpName, 0, REG_DWORD, (PBYTE)&dw, 4);
}
//----------------------------------------------------------------------
// AddRegSuperServ - Helper routine
//
int AddRegSuperServ (HKEY hKey, WORD wPort) {
SOCKADDR_IN sa;
HKEY hSubKey;
TCHAR szKeyName[128];
DWORD dw;
int rc;
DEBUGMSG (ZONE_FUNC, (DTAG TEXT("AddRegSuperServ++ %d\r\n"), wPort));
memset (&sa, 0, sizeof (sa));
sa.sin_family = AF_INET;
sa.sin_port = htons(wPort);
sa.sin_addr.s_addr = INADDR_ANY;
// Create key for this service
wsprintf (szKeyName, TEXT("Accept\\TCP-%d"), wPort);
rc = RegCreateKeyEx (hKey, szKeyName, 0, NULL, 0, NULL,
NULL, &hSubKey, &dw);
DEBUGMSG (1, (TEXT("RegCreateKeyEx %d %d\r\n"), rc, GetLastError()));
if (rc == ERROR_SUCCESS)
rc = RegSetValueEx (hSubKey, TEXT("SockAddr"), 0, REG_BINARY,
(PBYTE)&sa, sizeof (sa));
DEBUGMSG (ZONE_FUNC, (DTAG TEXT("AddRegSuperServ-- %d\r\n"),rc));
return rc;
}
//----------------------------------------------------------------------
// RegisterService - Add registry settings for auto load
//
int RegisterService () {
HKEY hKey, hSubKey;
TCHAR szModName[MAX_PATH], *pName;
DWORD dw;
int rc;
// Open the Services key
rc = RegOpenKeyEx(HKEY_LOCAL_MACHINE,TEXT("Services"),0, 0, &hKey);
if (rc == ERROR_SUCCESS) {
// Create key for this service
rc = RegCreateKeyEx (hKey, REGNAME, 0, NULL, 0, NULL,
NULL, &hSubKey, &dw);
if (rc == ERROR_SUCCESS) {
GetModuleFileName (hInst, szModName, dim (szModName));
// Scan to filename
pName = szModName + lstrlen (szModName);
while ((pName > szModName) && (*pName != TEXT('\\')))
pName--;
if (*pName == TEXT('\\')) pName++;
AddRegString (hSubKey, TEXT ("DLL"), pName);
AddRegString (hSubKey, TEXT ("Prefix"), TEXT("TCK"));
AddRegDW (hSubKey, TEXT("Index"), 0);
AddRegDW (hSubKey, TEXT("Context"), SERVICE_INIT_STOPPED);
AddRegString (hSubKey, TEXT("DisplayName"),
TEXT("Tick Service"));
AddRegString (hSubKey, TEXT("Description"),
TEXT("Returns system tick cnt on Port 1000"));
AddRegSuperServ (hSubKey, PORTNUM);
} else
DEBUGMSG (ZONE_ERROR, (TEXT("Error creating key\r\n")));
RegCloseKey(hKey);
} else
DEBUGMSG (ZONE_ERROR, (TEXT("Error opening key\r\n")));
return (rc == ERROR_SUCCESS) ? 0 : -1;
}
//----------------------------------------------------------------------
// DeregisterService - Remove auto load settings from registry
//
int DeregisterService () {
HKEY hKey;
int rc;
// Open the Services key
rc = RegOpenKeyEx(HKEY_LOCAL_MACHINE,TEXT("Services"),0, 0, &hKey);
if (rc == ERROR_SUCCESS) {
// Delete key for this service
rc = RegDeleteKey (hKey, REGNAME);
if (rc != ERROR_SUCCESS)
DEBUGMSG(ZONE_ERROR, (DTAG TEXT("Error deleting key %d\r\n"),
GetLastError()));
RegCloseKey(hKey);
} else
DEBUGMSG (ZONE_ERROR, (TEXT("Error opening key\r\n")));
return (rc == ERROR_SUCCESS) ? 0 : -1;
}
//======================================================================
// AcceptThread - Thread for managing connected sockets
//
DWORD WINAPI AcceptThread (PVOID pArg) {
SOCKET sock;
int rc;
DWORD dwCmd, dwTicks;
sock = (SOCKET)pArg;
DEBUGMSG (ZONE_THREAD, (TEXT("AcceptThread++ %x\r\n"), pArg));
// Simple task, for any nonzero received byte, sent tick count back
rc = recv (sock, (char *)&dwCmd, sizeof (DWORD), 0);
while ((rc != SOCKET_ERROR) && (dwCmd != 0)) {
DEBUGMSG (ZONE_THREAD, (TEXT("Recv cmd %x\r\n"), dwCmd));
dwTicks = GetTickCount ();
DEBUGMSG (ZONE_THREAD, (TEXT("sending %d\r\n"), dwTicks));
rc = send (sock, (char *)&dwTicks, 4, 0);
// Read next cmd
rc = recv (sock, (char *)&dwCmd, sizeof (DWORD), 0);
}
closesocket (sock);
DEBUGMSG (ZONE_THREAD, (TEXT("AcceptThread-- %d %d\r\n"),rc, GetLastError()));
return 0;
}
The service interface is quite simple. Applications can query the tick count of the device by sending a nonzero DWORD to the device. The service will disconnect when the DWORD received is zero. To install TickSrv, copy TickSrv to the Windows CE device and run the following short Windows CE application, TSInst, provided with the companion files. The relevant code is shown here:
HANDLE hDrv = RegisterService (TEXT("TCK"), 0, TEXT("TickSrv.dll"), 0);
if (hDrv) {
printf ("Service loaded. %x\r\n", hDrv);
DWORD dwBytes;
ServiceIoControl (hDrv, IOCTL_SERVICE_INSTALL, 0, 0, 0, 0,
&dwBytes, NULL);
printf ("Install complete\r\n");
DeregisterService (hDrv);
} else
printf ("Service failed to load. rc %d\r\n", GetLastError());
The install application uses RegisterDevice and ServiceIoControl to have TickSrv update the registry. TickSrv will load on the next system reset, but it can also be loaded manually using the Services Manager. Run the following command line to load TickSrv manually:
services load TickSrv
Listing 22-3 is a simple PC-based client written in Visual Studio .NET that will open port 1000 on a specified device, send it a command to receive the tick count, wait a few milliseconds, ask again, and then terminate the connection and quit.
Listing 22-3: The PCClient example
PCClient.cpp
//======================================================================
// PCClient.cpp : Simple client for the tick server example
//
// Written for the book Programming Windows CE
// Copyright (C) 2003 Douglas Boling
//======================================================================
#include "stdafx.h"
int _tmain(int argc, _TCHAR* argv[])
{
SOCKET sock;
SOCKADDR_IN dest_sin;
WORD wPort = 1000;
int rc;
if (argc < 2) {
printf ("Syntax: %s <IP Addr> %d\r\n", argv[0], argc);
return 0;
}
// Init winsock
WSADATA wsaData;
if ((rc = WSAStartup(0x101,&wsaData)) != 0) {
printf ("WSAStartup failed\r\n");
WSACleanup();
return 0;
}
// Create socket
sock = socket( AF_INET, SOCK_STREAM, 0);
if (sock == INVALID_SOCKET) {
return INVALID_SOCKET;
}
// Set up IP address to access
memset (&dest_sin, 0, sizeof (dest_sin));
dest_sin.sin_family = AF_INET;
dest_sin.sin_addr.S_un.S_addr = inet_addr (argv[1]);
dest_sin.sin_port = htons(wPort);
printf ("Connecting to %s Port %d\r\n",
inet_ntoa (dest_sin.sin_addr), wPort);
// Connect to the device
rc == connect( sock, (PSOCKADDR) &dest_sin, sizeof( dest_sin));
if (rc == SOCKET_ERROR) {
printf ("Err in connect. %d\r\n", WSAGetLastError());
closesocket( sock );
return INVALID_SOCKET;
}
DWORD dwCmd = 1, dwTicks = 0;
// Ask for ticks
send (sock, (char *)&dwCmd, 4, 0);
recv (sock, (char *)&dwTicks, 4, 0);
printf ("Ticks: %d\r\n", dwTicks);
// Wait 1/4 second and ask again
Sleep(250);
send (sock, (char *)&dwCmd, 4, 0);
recv (sock, (char *)&dwTicks, 4, 0);
printf ("Ticks: %d\r\n", dwTicks);
// Terminate connection and close socket
dwCmd = 0;
send (sock, (char *)&dwCmd, 4, 0);
Sleep(100);
closesocket (sock);
return 0;
}
The Services Manager is a great addition to Windows CE. It provides support for those background tasks that are so often needed in embedded systems. Using a service instead of writing a standalone application also reduces the number of applications running on the device. Considering that Windows CE supports only 32 applications at any one time, writing a handful of services instead of writing a number of applications can be the difference in the system running or not.In the last chapter of the book, I will turn away from C, C++, and what is now referred to as unmanaged or native code. Instead, I will introduce the next wave of embedded programming tools, the .NET Compact Framework. Programs written in the Compact Framework are compiled to an intermediate language instead of to CPU instructions. This arrangement makes the applications CPU independent. The Compact Framework is a cool new technology.