Sending Notifications in Windows with Rust
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:
szInfo
: A pointer to a 256-codepoint UTF-16 string that will be used as the body of the message.szInfoTitle
: A pointer to a 64-codepoint UTF-16 string that will be used as the title of the notification.dwInfoFlags
: A bitfield describing the appearance of the notification. This customizes behavior like sound, the icon used, and whether this notification respects “quiet time”1.
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:
- By a combination of HWND and HWND-unique ID
- By a GUID
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.
-
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”. ↩