Table of Contents
Introduction
This post will demonstrate a basic Windows service implementation using Zig.
Windows APIs
In order to get access to the Windows APIs, we need to tell our application about the functions. To do this we can declare extern
functions. For example, the GetAdaptersAddresses
function.
extern fn GetAdaptersAddresses(u32, u32, ?*anyopaque, ?*IP_ADAPTER_ADDRESSES, ?*u32) callconv (@import("std").os.windows.WINAPI) u32;
Not so bad, right?
Oops I almost forgot: IP_ADAPTER_ADDRESSES
isn't defined yet. Let's add this real quick... Ok, maybe not - that's a lot of fields and a lot of typing.
Thankfully, Jonathan Marler created a binding generator appropriately named zigwin32 that generates Zig bindings for Win32. We can fetch zigwin32 with Zig's built-in package manager and make it available to our build.zig
script.
zig fetch --save 'git+https://github.com/marlersoft/zigwin32#main'
After adding it to our exe
's root_module
, we can continue.
main.zig
Skeleton
For a starting point, we'll create three functions: main
, serviceMain
, and serviceControl
.
Our main
function is our entry point to our application. No surprises here. The serviceMain
and serviceControl
functions will be called by the service control manager. We'll get into the details of these functions a bit later, but take note of their signatures and calling conventions.
pub fn main() void {
}
pub fn serviceMain(argc: u32, argv: ?*?[*:0]const u8) callconv(std.os.windows.WINAPI) void {
}
pub fn serviceControl(
code: u32,
event_type: u32,
event_data: ?*anyopaque,
context: ?*anyopaque,
) callconv(std.os.windows.WINAPI) u32 {
}
const std = @import("std");
const win32 = @import("win32");
Connect to the service control manager
Our service program may run several different services within one process. We can define the services that run within our process as a service table. Each entry in this table contains a string holding the service name and a function pointer that points to the service's entry point. In this case, it's our serviceMain
function. Let's define the service table in our main function.
const service_name = "My Awesome Service";
pub fn main() void {
const service_table = [_]win32.system.services.SERVICE_TABLE_ENTRYA{
.{
.lpServiceName = @constCast(service_name.ptr),
.lpServiceProc = serviceMain,
},
.{ .lpServiceName = null, .lpServiceProc = null },
};
}
After we define the services our process will run, we need to connect our service program to the service control manager and tell it about our services. We do this by calling StartServiceCtrlDispatcherA
, passing the table we created in the previous step.
pub fn main() void {
// -- snip --
if (win32.system.services.StartServiceCtrlDispatcherA(&service_table[0]) > 0) {
// error
}
}
Note that the call to StartServiceCtrlDispatcherA
doesn't return until all services within our service table are stopped, so we can just return after this call. It is possible this function doesn't return for a very long time!
Initialize the service
After calling StartServiceCtrlDispatcherA
and creating the connection, the service control manager will call our serviceMain
function we registered in the table earlier.
Once we enter serviceMain
, the service control manager needs to send events to our service. For example, in the services console in Windows, right-clicking a service displays a menu with some actions: Start, Stop, Pause, Restart, etc. These are all events that our service needs to handle. But before we handle anything, we need to tell the service control manager where to send its events. This is where the serviceControl
function comes in.
Passing data between functions
Since serviceMain
doesn't directly call serviceControl
, we need some way of sharing data between these two functions. One way is to create global variables that the two functions can access. However, I really don't like global data, and try avoid it as much as possible.
You'll note that in the following section I choose to register our serviceControl
function via RegisterServiceCtrlHandlerExA
instead of RegisterServiceCtrlHandlerA
. This is intentional. The former allows us to pass in a pointer that gets forwarded to serviceMain
.
Let's create a simple ServiceData
struct that we can provide to the control registration.
const ServiceData = struct {
handle: isize = -1,
status: win32.system.services.SERVICE_STATUS = .{
.dwServiceType = win32.system.services.SERVICE_WIN32_OWN_PROCESS,
.dwCurrentState = .START_PENDING,
.dwControlsAccepted = 0,
.dwWin32ExitCode = 0,
.dwServiceSpecificExitCode = 0,
.dwCheckPoint = 0,
.dwWaitHint = 0,
},
stop_event: ?*anyopaque = null,
};
Don't worry too much about these fields for now, we'll get to them later.
Registering the control handler
Inside our serviceMain
function we'll register the serviceControl
function with the service control manager, telling it to call this function when an event is generated. We do this with the RegisterServiceCtrlHandlerExA
function. We'll also pass the address of a default ServiceData
struct.
pub fn serviceMain(argc: u32, argv: ?*?[*:0]const u8) callconv(std.os.windows.WINAPI) void {
const service_data = ServiceData{};
const status_handle = win32.system.services.RegisterServiceCtrlHandlerExA(service_name.ptr, serviceControl, &service_data);
if (status_handle == 0) {
// error
return;
}
}
This call returns a handle that we'll also need in serviceControl
, so let's save the handle to the status_handle
variable.
Thread synchronization
I hope I didn't scare you too badly with that section title, it's not that bad, I promise.
Our process needs to stay open as long as our services are running. If we return from serviceMain
, our process closes and we lose our services. This is not what we want. You might be tempted to put a while (true) {}
block at the end of serviceMain
but that has its own problems I won't get into here.
Instead, we'll create a waitable event using with CreateEventA
. This function will create a synchronization primitive that we can use as a signaling mechanism to block the main thread until something happens. For now, let's save this event object to the stop_event
variable.
pub fn serviceMain(argc: u32, argv: ?*?[*:0]const u8) callconv(std.os.windows.WINAPI) void {
// -- snip --
const stop_event = win32.system.threading.CreateEventA(null, 0, 1, null);
if (stop_event == null) {
// error
return;
}
}
Up and running
We now have everything we need to tell the service control manager we're running and ready to handle events. Let's make sure we assign the status_handle
and stop_event
we created in the previous steps to our ServiceData
struct.
pub fn serviceMain(argc: u32, argv: ?*?[*:0]const u8) callconv(std.os.windows.WINAPI) void {
// -- snip --
service_data.handle = status_handle;
service_data.stop_event = stop_event;
}
Next, let's set our ServiceData
state to running and tell the service control manager we'll handle stop and shutdown commands. We'll do this by updating the dwCurrentState
and dwControlsAccepted
fields in the ServiceData.status
we just initialized. Then, we'll update the service control manager with this new data with the SetServiceStatus
function.
pub fn serviceMain(argc: u32, argv: ?*?[*:0]const u8) callconv(std.os.windows.WINAPI) void {
// -- snip --
service_data.status.dwCurrentState = .RUNNING;
service_data.status.dwControlsAccepted = win32.system.services.SERVICE_CONTROL_STOP | win32.system.services.SERVICE_CONTROL_SHUTDOWN;
if (win32.system.services.SetServiceStatus(service_data.handle, &service_data.status) == 0) {
// error
}
}
Finally, we'll await a signal from the stop_event
we created earlier to block the process using WaitForSingleObject
. Since this call is blocks until we signal the stop_event
object, we can assume if we get past this our service was stopped and we can update our status with the service control manager.
pub fn serviceMain(argc: u32, argv: ?*?[*:0]const u8) callconv(std.os.windows.WINAPI) void {
// -- snip --
if (win32.system.threading.WaitForSingleObject(service_data.stop_event, win32.system.windows_programming.INFINITE) != 0) {
// error
return;
}
service_data.status.dwCurrentState = .STOPPED;
if (win32.system.services.SetServiceStatus(service_data.handle, &service_data.status) != 0) {
// error
}
Handling events
Now that we've initialized our service and are blocking the process waiting for events, we can get implement the serviceControl
function that handles these events.
We can retrieve our service_data
pointer we passed previously by casting it to our ServiceData
type. Also, we'll switch on the code
, which is the event sent to us by the service control manager. Our service is pretty dumb, so we won't have a lot of robust handling here. For now, we'll just return NO_ERROR
.
pub fn serviceControl(
code: u32,
event_type: u32,
event_data: ?*anyopaque,
context: ?*anyopaque,
) callconv(std.os.windows.WINAPI) u32 {
var service_data: *ServiceData = @alignCast(@ptrCast(context.?));
const err: win32.foundation.WIN32_ERROR = switch (code) {
else => .NO_ERROR,
};
return @intFromEnum(err);
}
The control handler documentation says that we should return ERROR_CALL_NOT_IMPLEMENTED
if we don't handle an event, and NO_ERROR
for the SERVICE_CONTROL_INTERROGATE
, event if we don't handle it.
// -- snip --
const err: win32.foundation.WIN32_ERROR = switch (code) {
win32.system.services.SERVICE_CONTROL_INTERROGATE => .NO_ERROR,
else => .ERROR_CALL_NOT_IMPLEMENTED,
};
// -- snip --
Next, we'll handle the stop and shutdown events. When we handle these, we'll need to make sure deinitialization happens properly either here in our control handler or serviceMain
. Remember, in the Up and running section we created and awaited our stop event which is blocking our process. If we never signaled the object, the process would never terminate. In order to signal the event object, we'll use the SetEvent
function.
// -- snip --
const err: win32.foundation.WIN32_ERROR = switch (code) {
win32.system.services.SERVICE_CONTROL_STOP, win32.system.services.SERVICE_CONTROL_SHUTDOWN => blk: {
service_data.status.dwCurrentState = .STOP_PENDING;
if (win32.system.services.SetServiceStatus(service_data.handle, &service_data.status) == 0) {
// error
}
if (win32.system.threading.SetEvent(service_data.stop_event) == 0) {
// error
}
break :blk .NO_ERROR;
},
// -- snip --
};
// -- snip --
Wrap up
I hope this post was helpful in showing how to use Zig to create Windows services. I just did the bare minimum here, but there's so much room for improvement, starting with refactoring this code to read more like idiomatic Zig code.
It's also nearly impossible to diagnose crashes. Logging would be a great addition as well.
Although our service can be started, stopped, and restarted, doesn't really do anything. That's neither practical nor useful. However, the possibilities are endless. Here are some examples:
- File monitoring
- Data backup
- Content synchronization
- Web APIs
- Notifications
Full example
const service_name = "My Awesome Service";
pub fn main() void {
const service_table = [_]win32.system.services.SERVICE_TABLE_ENTRYA{
.{
.lpServiceName = @constCast(service_name.ptr),
.lpServiceProc = serviceMain,
},
.{ .lpServiceName = null, .lpServiceProc = null },
};
if (win32.system.services.StartServiceCtrlDispatcherA(&service_table[0]) > 0) {
// error
}
}
pub fn serviceMain(argc: u32, argv: ?*?[*:0]const u8) callconv(std.os.windows.WINAPI) void {
_ = argc;
_ = argv;
var service_data = ServiceData{};
const status_handle = win32.system.services.RegisterServiceCtrlHandlerExA(service_name.ptr, serviceControl, &service_data);
if (status_handle == 0) {
// error
return;
}
const stop_event = win32.system.threading.CreateEventA(null, 0, 1, null);
if (stop_event == null) {
// error
return;
}
service_data.handle = status_handle;
service_data.stop_event = stop_event;
service_data.status.dwCurrentState = .RUNNING;
service_data.status.dwControlsAccepted = win32.system.services.SERVICE_CONTROL_STOP | win32.system.services.SERVICE_CONTROL_SHUTDOWN;
if (win32.system.services.SetServiceStatus(service_data.handle, &service_data.status) == 0) {
// error
}
if (win32.system.threading.WaitForSingleObject(service_data.stop_event, win32.system.windows_programming.INFINITE) != 0) {
// error
return;
}
service_data.status.dwCurrentState = .STOPPED;
if (win32.system.services.SetServiceStatus(service_data.handle, &service_data.status) != 0) {
// error
}
}
pub fn serviceControl(
code: u32,
event_type: u32,
event_data: ?*anyopaque,
context: ?*anyopaque,
) callconv(std.os.windows.WINAPI) u32 {
_ = event_type;
_ = event_data;
var service_data: *ServiceData = @alignCast(@ptrCast(context.?));
const err: win32.foundation.WIN32_ERROR = switch (code) {
win32.system.services.SERVICE_CONTROL_STOP, win32.system.services.SERVICE_CONTROL_SHUTDOWN => blk: {
service_data.status.dwCurrentState = .STOP_PENDING;
if (win32.system.services.SetServiceStatus(service_data.handle, &service_data.status) == 0) {
// error
}
if (win32.system.threading.SetEvent(service_data.stop_event) == 0) {
// error
}
break :blk .NO_ERROR;
},
win32.system.services.SERVICE_CONTROL_INTERROGATE => .NO_ERROR,
else => .ERROR_CALL_NOT_IMPLEMENTED,
};
return @intFromEnum(err);
}
const std = @import("std");
const win32 = @import("win32");
const ServiceData = struct {
handle: isize = -1,
status: win32.system.services.SERVICE_STATUS = .{
.dwServiceType = win32.system.services.SERVICE_WIN32_OWN_PROCESS,
.dwCurrentState = .START_PENDING,
.dwControlsAccepted = 0,
.dwWin32ExitCode = 0,
.dwServiceSpecificExitCode = 0,
.dwCheckPoint = 0,
.dwWaitHint = 0,
},
stop_event: ?*anyopaque = null,
};