Sending Notifications in Windows with Rust

Tagged under:

I needed to send some notifications for a project, so I went on an exploratory mission of how to do it!

I ended up in the Win32 API documentation, since I didn’t quite want to mess with the UWP equivalent of this functionality. Essentially I want to call Shell_NotifyIconW. The API documentation has the full details, but to summarize: this function takes a message and a pointer to a NOTIFYICONDATAW struct. What it does depends on the message you pass it (add a notification, remove a notification, modify an existing notification, or a few other more esoteric functions).

My project is written in Rust, so I used the winapi crate, which provides bindings to the Win32 API.

Creating a notification

To create a notification, we need to call Shell_NotifyIconW with the NIM_ADD message and a filled-out NOTIFYICONDATAW struct. The message isn’t particularly notable, but the struct, like most Win32 structs, has a ton of fields - not all of them are used for creating a notification. We need to set the struct’s uFlags bitfield to include NIF_INFO, which indicates that this NOTIFYICONDATAW is describing an informational notification. This means that the following fields of the struct are used:

String to u16 conversion

szInfo and szInfoTitle are of type [u16; 256] and [u16; 64], respectively. We have to convert string data to these; the easiest way I found to do this is:

let mut info = [0u16; 256];

let info_bytes: Vec<u16> = OsString::from(&input_info)
    .as_os_str()
    .encode_wide()
    .take(256)
    .collect();

unsafe {
    std::ptr::copy_nonoverlapping(
        info_bytes.as_ptr(),
        info.as_mut_ptr(),
        // Ensure we don't read past the end of info_bytes, or
        // copy too much memory.
        info_bytes.len().min(256)
    );
}

Note that this approach only works on Windows (since it requires std::os::windows::ffi::OsStrExt to provide the encode_wide method of OsStr), but since everything else only works on Windows, this isn’t a concern here.

Constructing NOTIFYICONDATAW

The easiest way to create the NOTIFYICONDATAW struct that we need is with its implementation of the Default trait, which just zeroes the fields of the struct:

let mut icon_data = NOTIFYICONDATAW {
    cbSize: std::mem::size_of::<NOTIFYICONDATAW>() as u32,
    uFlags: shellapi::NIF_INFO,
    szInfo: info,
    szInfoTitle: info_title,
    dwInfoFlags: shellapi::NIIF_NONE,
    ..Default::default()
};

Creating the notification

Now that we have the NOTIFYICONDATA struct, we can create the actual notification by calling Shell_NotifyIcon with the NIM_ADD message.

unsafe {
    let success = Shell_NotifyIconW(shellapi::NIM_ADD, &mut icon_data);

    if !success {
        let error_code = winapi::um::errhandlingapi::GetLastError();
        return Err(error_code)
    }

    return Ok(())
};

Why can’t I do it twice?

This worked…once. After that, the calls to Shell_NotifyIcon would fail with a very nondescriptive error code. Digging through it more, it turned out that since the notification wasn’t being deleted, the call was failing when I tried to add a notification that already existed.

This happens because you can identify notifications in the Win32 API in one of two ways:

Since I created the NOTIFYICONDATA struct with Default, it just zeroed out the hWnd, uID, and guidItem fields. The notification ID was always 0, so when I ran the program again the call failed, since a notification with that ID already existed.

Solution 1: Delete the notification

I can delete the notification after a certain amount of time, destroying it. This is easy to do: just call Shell_NotifyIcon with NIM_DELETE instead of NIM_ADD, and it will remove the described notification. This has some downsides, however: if the program crashes or is killed before it deletes the notification, the problem happens again.

Solution 2: Assign GUIDs

The solution I ended up going with is giving each notification its own unique GUID. This means that each notification will be a distinct entity. I have to add a flag to the NOTIFYICONDATA struct, then fill the guidItem field with a GUID that I generate:

let mut guid: GUID = Default::default();
unsafe {
    winapi::um::combaseapi::CoCreateGuid(&mut guid);
}

let mut icon_data = NOTIFYICONDATAW {
    cbSize: std::mem::size_of::<NOTIFYICONDATAW>() as u32,
    uFlags: shellapi::NIF_INFO | shellapi::NIF_GUID,
    szInfo: info,
    szInfoTitle: info_title,
    dwInfoFlags: shellapi::NIIF_NONE,
    guidItem: guid,
    ..Default::default()
};

Finishing up

This was a fun adventure! You can find the full source here on GitHub; it has some abstractions that I didn’t cover around adding/deleting notifications.

  1. Quiet time is a brief period that occurs shortly after a Windows installation, intended “to allow the user to explore and familiarize themselves with the new environment without the distraction of notifications”.