Creating a Simple Windows NT Service in C++

Nigel Thompson
Microsoft Developer Network Technology Group

November 1995

Click to open or copy the files for the NTService sample application.

Click to open or copy the files for the NTServCpl sample application.

Click to open or copy the files for the NTServCtrl sample application.

Abstract

This article describes how to create simple Microsoft® Windows NT™ services using Microsoft Visual C++™. The services are created using a single C++ class that provides a simple interface between your service and the operating system. Using this class approach, your own implementation is simply a matter of overriding a few virtual functions in the base class. Three sample applications accompany this article:

Introduction

A service in Microsoft® Windows NT™ is a program that runs whenever the computer is running the operating system. It does not require a user to be logged on. Services are needed to perform user-independent tasks such as directory replication, process monitoring, or services to other machines on a network, such as support for the Internet HTTP protocol.

Creating a service for Windows NT is not particularly hard. Debugging a service is, however, a little more difficult. For my own work I prefer to create my applications in C++ using Microsoft Visual C++™. Most Windows NT service samples are in C, so I thought it would be interesting to see if I could create a C++ class to perform the rudimentary functions of a Windows NT service. As it turns out, one can create Windows NT services in C++ quite simply. The base class I developed for this should be an adequate starting point for your own work.

Creating a service involves a bit more that just the service code. Additionally, you must write code to:

Most other service examples use one program to install the service and another to remove it. I built these functions into the service itself so you have only one .EXE to distribute. You can run the service application directly from the command line and ask it to install, uninstall, or report its version information. The NTService sample supports the following command-line arguments:

By default, when the system starts the service there will be no command-line arguments passed to it.

Creating the Application Framework

I have been creating applications based on the Microsoft Foundation Class Library (MFC) for too long. When I initially set out to build my Windows NT service, I started with the Visual C++ AppWizard and created an SDI/MFC application. I intended to remove the document and view classes, icons, and so on and just leave the framework. As it turns out, by the time you've removed all that stuff and the main window (since we can't have one), there isn't anything left. Very silly. So I went back to AppWizard and created a Console Application project with a single source file that would contain the main entry point function. I called this file NTServApp.cpp. I used the cpp extension rather than just c because I wanted to write the entire project using C++ rather than straight C. We'll look at the implementation of the code in this file later.

Since I wanted to build my service from a C++ class, I created the NTService.h and NTService.cpp files in which I would implement the CNTService base class. I also created the MyService.h and MyService.cpp files in which I would implement my own service class (CMyService) derived from CNTService. Again, well look at the code a bit later.

When I start a new project, I like to get something working as soon as possible, so I decided that the first thing my service should do is make some entries in the system's application log file. Having implemented a mechanism for making these entries, I'd be able to track when the service was started, stopped, and so on; I'd also have a way to record any errors that might occur in the service. Making log entries turned out to be much more complicated than I thought.

Making Log Entries

I figured that since the log files were a part of the operating system, there would be some application programming interface (API) support for making entries into them. So I broke out my trusty MSDN CD and dug around until I found the ReportEvent function. Now if you don't know about this stuff, you'd probably think that this function would need to know in which log file you want to make the entry, and the text of the message you want to insert. Well, that's sort of what it does, but to simplify internationalization of error messages, this function takes a message ID and looks up the message in a message table you provide. So the problem is not so much what message you want to put in the log, as how to add these messages to your application. Here's a step-by-step guide:

  1. Create a text file with the extension .MC containing the descriptions of the messages. I called mine NTServMsg.mc. The format is very specific and is covered in the "Compiling Messages" section of the Win32® Software Development Kit (SDK) Tools User's Guide.

  2. Run the message compiler (MC.EXE) against your source file, which by default creates an output file called MSG00001.BIN. The compiler also creates a header file (in my case NTServMsg.h) and an .RC file (NTServMsg.rc). You need to repeat this step any time you change the .MC file in your project, so it's handy to add a tool entry in your Visual C++ menu to do this.

  3. Create an .RC file for your project and #include in it the WINDOWS.H file and the .RC file produced by the message compiler.

  4. Include the header file produced by the message compiler in your main project header file so all modules have access to the symbolic message names.

Let's look at some of these files in more detail so you can see what you need to create and what the message compiler creates for you. We won't look at the entire set of messages, just one or two to show you how it works. Here's the first part of my message source file, NTServMsg.mc:

MessageId=100
SymbolicName=EVMSG_INSTALLED
Language=English
The %1 service was installed.
.

MessageId=
SymbolicName=EVMSG_REMOVED
Language=English
The %1 service was removed.
.

MessageId=
SymbolicName=EVMSG_NOTREMOVED
Language=English
The %1 service could not be removed.
.

Each entry has an ID value that, if not specifically set, is simply one more than the value assigned to the message before it. Each entry also has a symbolic name for use in your code, a language identifier, and the text of the actual message. Messages can span more than one line and are terminated by a line containing a single period on its own.

The message compiler outputs a binary file to be used as a resource in the application and two files for inclusion in your source code. Here's my .RC file:

// NTServApp.rc
#include <windows.h>

// Include the message table resource script 
// generated by the message compiler (MC).
#include "NTServMsg.rc"

Here's the .RC file the message compiler generated:

LANGUAGE 0x9,0x1
1 11 MSG00001.bin

As you can see, there's not a lot of text in these files!

The last file generated by the message compiler is a header file for you to include in your code. Here's just a part of the one generated for the sample:

 [..........]
//
// MessageId: EVMSG_INSTALLED
//
// MessageText:
//
//  The %1 service was installed.
//
#define EVMSG_INSTALLED                  0x00000064L

//
// MessageId: EVMSG_REMOVED
//
// MessageText:
//
//  The %1 service was removed.
//
#define EVMSG_REMOVED                    0x00000065L
[...........]

You might have noticed that several of my messages include argument substitution items (%1 and so on). Let's see how these are used in the code when we actually want to write a message to one of the system's log files. For an example, let's look at the part of the installation code that records successful installation in the event log. This is part of my CNTService::IsInstalled function:

 [....]
LogEvent(EVENTLOG_INFORMATION_TYPE, EVMSG_INSTALLED, m_szServiceName);
[....]

LogEvent is another CNTService function that uses the type of event (information, warning, or error), the ID of the event message, and up to three argument substitution strings to form the log message:

// This function makes an entry into the application event log.
void CNTService::LogEvent(WORD wType, DWORD dwID,
                          const char* pszS1,
                          const char* pszS2,
                          const char* pszS3)
{
    const char* ps[3];
    ps[0] = pszS1;
    ps[1] = pszS2;
    ps[2] = pszS3;

    int iStr = 0;
    for (int i = 0; i < 3; i++) {
        if (ps[i] != NULL) iStr++;
    }

    // Check to see if the event source has been registered,
    // and if not then register it now.
    if (!m_hEventSource) {
        m_hEventSource = ::RegisterEventSource(NULL,            // local machine
                                               m_szServiceName); // source name
    }

    if (m_hEventSource) {
        ::ReportEvent(m_hEventSource,
                      wType,
                      0,
                      dwID,
                      NULL,   // sid
                      iStr,
                      0,
                      ps,
                      NULL);
    }
}

As you can see, the majority of the work is handled by the ReportEvent system function.

So now we have a way to record events in the system event log by calling CNTService::LogEvent. Now we can move on to creating some of the code for the service itself.

Writing the Service Code

The Win32 SDK "Overviews" section has a subsection called "System Services." This section contains most of what you need to know in order to construct a simple Windows NT service. The sample code in the section is in C and is quite easy to follow. I based my CNTService class on the material in this code.

A service contains three major functions:

Service Callback Functions

Since the ServiceMain and Handler functions are called from the system, they must conform to the parameter-passing scheme and calling convention of the operating system. This means they can't simply be member functions of a C++ class. This is slightly inconvenient, since we want to encapsulate the functionality of a Windows NT service in a single C++ class. To get around this problem, I created my ServiceMain and Handler functions as static members of my CNTService class. This enabled me to create functions that are callable by the operating system. This doesn't provide a complete solution, however, because the system does not allow passing any form of user data to the called functions, so we have no way to identify a call to either ServiceMain or Handler with a specific instance of a C++ object. I use a very simple but limiting solution to this problem. I create a static variable that contains a pointer to the C++ object. The variable is initialized when the object is first created. This limits you to one C++ object per service application. I did not consider this to be too restrictive. Here's the declaration in the NTService.h file:

class CNTService
{
   [...]
   // static data
    static CNTService* m_pThis;   // nasty hack to get object ptr
  [...]
};

Here's how the m_pThis pointer gets initialized:

CNTService::CNTService(const char* szServiceName)
{
    // Copy the address of the current object so we can access it from
    // the static member callback functions.
    // WARNING: This limits the application to only one CNTService object. 
    m_pThis = this;
   [...]
}

The CNTService Class

When I create C++ objects to encapsulate groups of Microsoft Windows® functions, I try to do more than just make a member function for each Windows API I'm encapsulating. I try to make the object easy to use, and I try to help reduce the number of lines of code needed to implement a particular section of a project. So my object designs are based on "what do I want to do with this object?" rather than "what does Windows do with this set of APIs?".

The CNTService class contains member functions to parse a command line, to handle installing and removing the service, and to log events, and a set of virtual functions that you can override in your derived class to handle requests from the service control manager. We'll look at the use of most of these functions as we go through the implementation of the sample service.

If you want to create the simplest possible service, you need only override CNTService::Run, which is where you write the code to perform whatever task your service provides. You also need to implement the main function. If your service needs to perform some initialization, such as reading data from the registry, it also needs to override CNTService::OnInit. If you need to be able to send command messages to your service, you can do this by using the ControlService system function and handling the requests in the service by overriding CNTService::OnUserControl.

Using CNTService in the Sample Application

The NTService sample implements most of its functionality in the CMyService class, which is derived from CNTService. Here's the MyService.h header file:

// myservice.h

#include "ntservice.h"

class CMyService : public CNTService
{
public:
   CMyService();
   virtual BOOL OnInit();
      virtual void Run();
      virtual BOOL OnUserControl(DWORD dwOpcode);

      void SaveStatus();

   // Control parameters
   int m_iStartParam;
   int m_iIncParam;

   // Current state
   int m_iState;
};

As you can see, CMyService overrides OnInit, Run, and OnUserControl from CNTService. It also has a function called SaveStatus that is used to write data to the registry, and some member variables to hold the current state. The sample service increments an integer variable at regular intervals. The start value and increment value are both held as parameters in the registry. Not very exciting, but easy for you to follow. Let's move on now to see how the service is implemented.

Implementing the main Function

Having derived CMyService from CNTService, it's now a simple matter to implement the main function (in NTServApp.cpp):

int main(int argc, char* argv[])
{
    // Create the service object.
    CMyService MyService;
    
    // Parse for standard arguments (install, uninstall, version, etc).
    if (!MyService.ParseStandardArgs(argc, argv)) {

        // Didn't find any standard args so start the service.
        // Uncomment the DebugBreak line below to enter the debugger
        // when the service is started.
        //DebugBreak();
        MyService.StartService();
    }

    // When we get here, the service has been stopped.
    return MyService.m_Status.dwWin32ExitCode;
}

Not much code to look at here, but an awful lot happens when it's executed, so let's go through it step by step. First of all, we create an instance of the MyService class. The constructor sets the initial state and name of the service (MyService.cpp):

CMyService::CMyService()
:CNTService("NT Service Demonstration")
{
   m_iStartParam = 0;
   m_iIncParam = 1;
   m_iState = m_iStartParam;
}

A call is then made to ParseStandardArgs to see if the command line contains a request to install the service (-i), remove it (-u), or report its version number (-v). CNTService::ParseStandardArgs calls CNTService::IsInstalled, CNTService::Install, and CNTService::Uninstall to process these requests. If no recognizable command-line arguments are found, it is assumed that the service control manager is trying to start the service and a call is made to StartService. This function does not return until the service stops running. The call to DebugBreak causes a break into the debugger when the service is first started. When you are done debugging the code, you can comment out or delete this line.

Installing and Removing the Service

Installing the service is handled by CNTService::Install, which registers the service with the Windows NT service manager and makes entries in the registry to support logging messages when the service is running.

Removing the service is handled by CNTService::Uninstall, which simply informs the service manager that the service is no longer required. CNTService::Uninstall does not remove the actual service executable file.

Writing Your Service Code

Now we need to write the code that actually implements your service. For the NTService sample there are three major functions to write. These cover initialization, actually running the service, and responding to control requests.

Initialization

The registry has a location for services to store their parameters: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services. This is where I chose to store the configuration data for my service. I created a Parameters key and under that stored the values I wanted to save. So when the service starts, the OnInit function is called; it reads the initial settings from this place in the registry.

BOOL CMyService::OnInit()
{
  // Read the registry parameters.
    // Try opening the registry key:
    // HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\<AppName>\Parameters
    HKEY hkey;
  char szKey[1024];
  strcpy(szKey, "SYSTEM\\CurrentControlSet\\Services\\");
  strcat(szKey, m_szServiceName);
  strcat(szKey, "\\Parameters");
    if (RegOpenKeyEx(HKEY_LOCAL_MACHINE,
                     szKey,
                     0,
                     KEY_QUERY_VALUE,
                     &hkey) == ERROR_SUCCESS) {
        // Yes we are installed.
        DWORD dwType = 0;
        DWORD dwSize = sizeof(m_iStartParam);
        RegQueryValueEx(hkey,
                        "Start",
                        NULL,
                        &dwType,
                        (BYTE*)&m_iStartParam,
                        &dwSize);
        dwSize = sizeof(m_iIncParam);
        RegQueryValueEx(hkey,
                        "Inc",
                        NULL,
                        &dwType,
                        (BYTE*)&m_iIncParam,
                        &dwSize);
        RegCloseKey(hkey);
    }

  // Set the initial state.
  m_iState = m_iStartParam;

  return TRUE;
}

Now that we have the service parameters, we are ready to run the service.

Running the Service

The main body of the service code is executed when the Run function is called. My sample is rather simple:

void CMyService::Run()
{
    while (m_bIsRunning) {

    // Sleep for a while.
        DebugMsg("My service is sleeping (%lu)...", m_iState);
        Sleep(1000);

    // Update the current state.
    m_iState += m_iIncParam;
    }
}

Note that this function does not exit until the service is stopped. The CNTService::m_bIsRunning flag is set to FALSE when a request is made to stop the service. You can also override OnStop and/or OnShutdown if you need to perform cleanup when your service terminates.

Responding to Control Requests

You can communicate with your service in whatever way suits you—named pipes, thought transference, sticky notes, and so on—but for simple requests the system function ControlService is very easy to use. CNTService provides a handler for non-standard (that is, user) messages sent through the ControlService function. My sample uses a single message to save the current state of the service in the registry so that other applications can inspect it. I'm not proposing this as the best way to monitor a service, just one that was easy to implement and interesting to code. User messages sent by means of ControlService must be in the range 128 to 255. I defined a constant, SERVICE_CONTROL_USER, as the base value (128). Messages in the user range are sent to CNTService:: OnUserControl and are handled in the sample service this way:

BOOL CMyService::OnUserControl(DWORD dwOpcode)
{
    switch (dwOpcode) {
    case SERVICE_CONTROL_USER + 0:

        // Save the current status in the registry.
        SaveStatus();
        return TRUE;

    default:
        break;
    }
    return FALSE;   // say not handled
}

SaveStatus is a local function that saves the service state in the registry.

Debugging a Windows NT Service

The main function contains a call to DebugBreak, which causes the system debugger to be activated when the service is first started. You can monitor the debug messages from the service in the debugger's command window. You can use CNTService::DebugMsg from your service to report events of interest during debugging.

You'll need to install the system debugger (WinDbg) from the Win32 SDK in order to debug your service code.

One important point to note is that you really can't stop the service and single-step it when it's being controlled by the service manager, because the service manager will time out requests to the service and terminate the service thread. So you can really only get your service to spit out messages to track its progress and watch them in the debugger window.

When the service is started (for example, from the Services applet in Control Panel), the debugger will start with the service thread halted. You need to let the thread run by clicking the GO button or pressing the F5 key. Then you can observe the service's progress in the debugger window.

The following text shows an example of starting and stopping the service:

Module Load: WinDebug/NTService.exe  (symbol loading deferred)
Thread Create:  Process=0, Thread=0
Module Load: C:\NT351\system32\NTDLL.DLL  (symbol loading deferred)
Module Load: C:\NT351\system32\KERNEL32.DLL  (symbol loading deferred)
Module Load: C:\NT351\system32\ADVAPI32.DLL  (symbol loading deferred)
Module Load: C:\NT351\system32\RPCRT4.DLL  (symbol loading deferred)
Thread Create:  Process=0, Thread=1
*** WARNING: symbols checksum is wrong 0x0005830f 0x0005224f for C:\NT351\symbols\dll\NTDLL.DBG
Module Load: C:\NT351\symbols\dll\NTDLL.DBG  (symbols loaded)
Thread Terminate:  Process=0, Thread=1, Exit Code=0
Hard coded breakpoint hit
Hard coded breakpoint hit
[](130): CNTService::CNTService()
Module Load: C:\NT351\SYSTEM32\RPCLTC1.DLL  (symbol loading deferred)
[NT Service Demonstration](130): Calling StartServiceCtrlDispatcher()
Thread Create:  Process=0, Thread=2
[NT Service Demonstration](174): Entering CNTService::ServiceMain()
[NT Service Demonstration](174): Entering CNTService::Initialize()
[NT Service Demonstration](174): CNTService::SetStatus(3026680, 2)
[NT Service Demonstration](174): Sleeping...
[NT Service Demonstration](174): CNTService::SetStatus(3026680, 4)
[NT Service Demonstration](174): Entering CNTService::Run()
[NT Service Demonstration](174): Sleeping...
[NT Service Demonstration](174): Sleeping...
[NT Service Demonstration](174): Sleeping...
[NT Service Demonstration](130): CNTService::Handler(1)
[NT Service Demonstration](130): Entering CNTService::Stop()
[NT Service Demonstration](130): CNTService::SetStatus(3026680, 3)
[NT Service Demonstration](130): Leaving CNTService::Stop()
[NT Service Demonstration](130): Updating status (3026680, 3)
[NT Service Demonstration](174): Leaving CNTService::Run()
[NT Service Demonstration](174): Leaving CNTService::Initialize()
[NT Service Demonstration](174): Leaving CNTService::ServiceMain()
[NT Service Demonstration](174): CNTService::SetStatus(3026680, 1)
Thread Terminate:  Process=0, Thread=2, Exit Code=0
[NT Service Demonstration](130): Returned from StartServiceCtrlDispatcher()
Module Unload: WinDebug/NTService.exe
Module Unload: C:\NT351\system32\NTDLL.DLL
Module Unload: C:\NT351\system32\KERNEL32.DLL
Module Unload: C:\NT351\system32\ADVAPI32.DLL
Module Unload: C:\NT351\system32\RPCRT4.DLL
Module Unload: C:\NT351\SYSTEM32\RPCLTC1.DLL
Thread Terminate:  Process=0, Thread=0, Exit Code=0
Process Terminate:  Process=0, Exit Code=0
>

Summary

Perhaps C++ isn't ideal for creating a Windows NT service, but having a single class from which you can derive your own service certainly makes it easy to get your own service started. As usual, if you or your relatives, friends, pets, or acquaintances have comments or suggestions, please mail them to me: nigelt@microsoft.com.