Writing a Windows Service in C++

In the Windows operating system, a service is a kind of application that runs in the background. A service can run even when no user is logged on, and can be started automatically at boot or manually on demand. Windows services are managed by the Service Control Manager (SCM), and must implement a specific API, so the SCM can interact with them. Let’s look at how to build one.

A traditional C++ Windows application starts in the w­Win­Main function. The same applies for services, however, the only thing a service should do in its standard entrypoint is to register with the SCM.

Registering with the SCM is done by passing it a service table, which contains the address of the Service­Main function — a function that actually implements the logic of the service (“logic” here just being a fancy term for “the thing you want the service to do”).

Within Service­Main, you then have to register a Service­Ctrl­Handler function, which is responsible for handling commands from the SCM (such as stopping the service).

Initial Declarations

Let’s start with the forward declarations of the two aforementioned functions. It is not a good idea to put the entire logic of the service to Service­Main, so we additionally declare a third function called Service­Worker.

VOID WINAPI ServiceMain(DWORD argc, LPTSTR* argv);

VOID WINAPI ServiceCtrlHandler(DWORD ctrlCode);

void ServiceWorker(int& exitCode);

For the sake of simplicity, we’ll put the service table in a global variable, as well as the information about the current status of the service and a handle that we can use to inform the SCM about changes to the status.

TCHAR ServiceName[] = _T("");

SERVICE_TABLE_ENTRY ServiceTable[] = {
	{ServiceName, (LPSERVICE_MAIN_FUNCTION) ServiceMain},
	{NULL, NULL}
};

SERVICE_STATUS ServiceStatus = {0};

SERVICE_STATUS_HANDLE SvcStatusHandle = NULL;

Since we’ll be running the service in its own process, Service­Name can be empty (it will be ignored anyways).

Program Entrypoint

In the program entrypoint, the service should immediately register with the SCM. The SCM will then start Service­Main in a new thread, while the main thread remains blocked at the call to Start­Service­Ctrl­Dispatcher until the service stops.

int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE hPrInst, PWSTR pCmdLn, int nCmdSh) {
	if (!StartServiceCtrlDispatcher(ServiceTable)) {
		DWORD errno = GetLastError();

		if (errno == ERROR_FAILED_SERVICE_CONTROLLER_CONNECT) {
			// executable launched outside Service Control Manager
		}

		return 1;
	}

	return 0;
}

You can compile a single executable to be used both as a service and a regular application: check the return value and error code of Start­Service­Ctrl­Dispatcher to detect which way the executable was launched.

Main Loop

Service­Main must register a handler that responds to commands from the Service Control Manager (for example stopping the service). Then it must inform the SCM that the service has started and actually begin executing the service logic.

VOID WINAPI ServiceMain(DWORD argc, LPTSTR* argv) {
	// register handler for commands from SCM
	SvcStatusHandle = RegisterServiceCtrlHandler(ServiceName), ServiceCtrlHandler);
	if (SvcStatusHandle == NULL) return;

	// inform SCM the service is starting
	ServiceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
	ServiceStatus.dwControlsAccepted = 0;
	ServiceStatus.dwCurrentState = SERVICE_START_PENDING;
	SetServiceStatus(SvcStatusHandle, &ServiceStatus);

	// start service logic
	int exitCode = 0;
	std::thread worker(ServiceWorker, std::ref(exitCode));

	// inform SCM the service has started
	ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP;
	ServiceStatus.dwCurrentState = SERVICE_RUNNING;
	SetServiceStatus(SvcStatusHandle, &ServiceStatus);

	// wait until the service has finished
	worker.join();

	// inform SCM the service has stopped
	ServiceStatus.dwControlsAccepted = 0;
	ServiceStatus.dwCurrentState = SERVICE_STOPPED;
	ServiceStatus.dwWin32ExitCode = exitCode;
	SetServiceStatus(SvcStatusHandle, &ServiceStatus);
}
Caution

The SCM waits until the service reports SERVICE_RUNNING. Defer any lengthy processing to after you set this status, especially if your service starts at boot, since all other services (and the entire boot process) will wait for your service. A service has 30 seconds to transition to the running state until the system terminates it.

In our architecture, Service­Worker is where you implement the actual thing you want the service to do.

void ServiceWorker(int& exitCode) {
	while (ServiceStatus.dwCurrentState != SERVICE_STOP_PENDING) {
		// do something instead of just sleeping
		std::this_thread::sleep_for(std::chrono::seconds(1));
	}
}

Control Handler

The service control handler receives and handles commands from the SCM. For a simple service it’s enough to support just the SERVICE_CONTROL_STOP command.

VOID WINAPI ServiceCtrlHandler(DWORD ctrlCode) {
	switch (ctrlCode) {
		case SERVICE_CONTROL_STOP:
			if (ServiceStatus.dwCurrentState != SERVICE_RUNNING) break;

			// inform SCM the service has received the stop command
			// and signal to the program it should stop

			ServiceStatus.dwControlsAccepted = 0;
			ServiceStatus.dwCurrentState = SERVICE_STOP_PENDING;
			SetServiceStatus(SvcStatusHandle, &ServiceStatus);
			break;

		default:
			break;
	}
}

If you changed the implementation of Service­Worker, make sure Service­Ctrl­Handler is still able to stop it (maybe it’ll need to terminate the worker thread forcefully, for example).

Service Installation

After you stitch all code blocks above together one after another, compile the code to an .exe file. Then, use the sc.exe tool to install the service (note that the spaces after equals signs are necessary):

sc.exe create "mysvc" displayname= "My Service" binPath= "C:\mysvc.exe"

Now the service should be visible in the Services control panel and you should be able to start it.

References

Further information can be found on MSDN: