Recently I wanted to execute code1 when my MacBook’s battery charge reached a certain percentage remaining. I was using the rust-battery crate to check the status of the battery of my MacBook. The crate comes with an example program called simple.rs which shows how to poll for the battery state. However the problem with this example program is the main loop.
On my laptop simple.rs prints out the following:
Each iteration of the loop results in the same output as the previous run, since the polling interval is more frequent than the battery data updates from macOS. A better program would only refresh the battery data once the OS has signalled some new information is available.
Fortunately on macOS it’s possible to be signalled when the battery state has been updated using the notify.h API in the macOS SDK. The rest of this post will show how to use notify.h safely from Rust and integrate it with Tokio so battery data can be refreshed asynchronously.
The man page for this API is called notify(3). To quote from the man page:
These routines allow processes to exchange stateless notification events. Processes post notifications to a single system-wide notification server, which then distributes notifications to client processes that have registered to receive those notifications, including processes run by other users.
This API allows processes to subscribe or publish messages to a macOS wide event bus2. Each event is given a unique string identifier such as com.apple.system.config.network_change3. The event is published from a single process and arbitrary processes can subscribe to them. The typical use that I have seen for these APIs is subscribing to OS configuration changes such as network, timezone, hostname, power source and other changes4.
For subscribing to battery charge state or power source changes, the event name is com.apple.system.powersources.timeremaining. This event will be published if the machine switches between battery power and AC Power, and if there are any changes to the battery charge level.
The entire API is exposed in the notify.h header, and there are only a few functions.The entire lifecycle of subscribing to, receiving and unsubscribing from an event is covered by the following functions5.
From the comments of the header it should be possible to do the following:
Use notify_register_file_descriptor to get a file descriptor that will be readable every time an event fires.
The value readable should be the same as the token returned with the file descriptor.
To close the file descriptor and stop getting events use notify_cancel.
The file descriptor doesn’t represent a file on disk, so we should be careful to never write to it or to accidentally close it.
The comments in notify.h hint on what a safe Rust API could look like. First a file descriptor and token are returned from notify_register_file_descriptor and this file descriptor can only be closed with notify_cancel. That tells us that a constructor in Rust would need to call notify_register_file_descriptor and return a struct with a Drop implementation that calls notify_cancel.
Second, the file descriptor returned is not backed by a file. Passing this file descriptor to File would be not appropriate since that offers methods that could write to the file descriptor. Instead, we likely need to implement the Read trait on our struct.
With out two hints, using the notify.h from safe Rust looks like something below.
On my laptop I get the following output after sometime on battery power.
Observe that every line has a different value, indicating the program is not polling for battery state too frequently. The only drawback with this approach is that using the Read trait blocks the main until the next event arrives, preventing concurrency.
To prevent blocking while waiting for the next event, we can integrate the above NotifyFd struct with Tokio. Tokio, unlike other Rust async runtimes, allows for arbitrary file descriptors to be added to it’s reactor with AsyncFd. The documentation for AsyncFd says:
Associates an IO object backed by a Unix file descriptor with the tokio reactor, allowing for readiness to be polled. The file descriptor must be of a type that can be used with the OS polling facilities (ie, poll, epoll, kqueue, etc), such as a network socket or pipe, and the file descriptor must have the nonblocking mode set to true.
This implies so long as we correctly change the file descriptor to non blocking mode and pass it to Tokio, it should be possible to asynchronously wait for events. Based on the documentation of the trait the code needs to do the following:
Implement the AsRawFd trait on a struct so Tokio can extract the underlying file descriptor to pass to kqueue.
Before passing the struct to Tokio, the code should put the file descriptor in non blocking mode and drain it6 of all existing data so it can be safely passed to kqueue.
Implement the AsyncRead to allow us to call .read in an async manner.
A full implementation based on the code above is below. The primary additions is adding the AsRawFd trait to the NotifyFd struct, as well as a new struct called AsyncNotifyFd which implements the integration with Tokio. The main function is now async.
The above code produces the same output as the blocking implementation, with the added advantage that we can concurrently do other work while waiting on the file descriptor to be readable. For example, this code can select! on other futures while waiting for the next event.
The notify.h API in macOS allows programs to be signalled when events occur over a file descriptor. It’s easy to safely use notify.h in Rust and integrate with Tokio for asynchronous handling of events. If you ever want to poll for battery state on macOS consider using notify.h to signal when the battery state should be refreshed instead of polling very frequently.