Poking the macOS IO Kit with Rust

This is a first post of the “Digging up the system details in a cross-platform way” series, intended to gather together pieces of knowledge about the notebook batteries info fetching, based on the crate of mine, the battery.

Why I am doing this instead of just pointing to the code sources? Mostly because I’m practicing in a written English and also because while all the information needed is available on the internet, it is uncoordinated, incomplete and often suggests to follow the worse path of all possible (I’m looking at you, StackOverflow).

All code examples below will be in a pseudish-Rust, will not handle any possible errors (these are examples after all) and provided as is. Also, I had never developed for macOS before, so some terms and definitions might be slightly incorrect and I would love to hear from you if there is something wrong (I will try my best though).

Doing it the wrong way

It is commonly suggested to call the external command (such as ioreg -n AppleSmartBattery, system_profileror even the syslog) and parse its output, but there exists a little bit harder, but much more efficient way to do this. Leave that stdout parsing for a bash scripts.

I/O Kit

Among the many things under the macOS hood, there exists the I/O Kit framework, which provides access to hardware devices and drivers from the user-space. For our needs we will need to reach for an entities (subclasses of the IOService) list, which are represents the power sources (read: batteries) and can provide the required information.

First thing first, we need to link our program with an I/O Kit. With Rust it is as simple as creating the build.rs file at the package root with a following content:

fn main() {
    println!("cargo:rustc-link-lib=framework=IOKit");
}

We need to communicate with an I/O Kit somehow, and this can be done by establishing a connection via Mach port (a communication channel between the client and the server which provides the requested service) with an IOMasterPort function:

extern "C" {
    pub static kIOMasterPortDefault: mach_port_t;
    
    pub fn IOMasterPort(
        bootstrapPort: mach_port_t,
        masterPort: *mut mach_port_t
    ) -> kern_return_t;
}

let mut master_port: mach_port_t = MACH_PORT_NULL;
unsafe {
    // TODO: Handle the possible error
    let _result = IOMasterPort(kIOMasterPortDefault, &mut master_port);
}

Unfortunately, at the moment of writing this post, both1 existing2 Rust I/O Kit FFI bindings are kinda abandoned, so this one and the following examples will use the foreign functions directly. On the other hand, there are mach and core-foundation crates exists, providing the Mach kernel API and macOS types and functions, so at least some definitions are already provided for us (such a mach_port_t type from above).

Obtained port should be deallocated manually later when there will be no need in it anymore:

mach_port_deallocate(mach_task_self(), master_port);

Searching for services

Assuming that we had received the communication port successfully, it is time to do the “device matching” now — a process of searching the I/O Registry for objects representing the battery devices.

At the lowest level, it can be done by creating the matching dictionary, which describes the properties of the objects to search, but in our case the only property needed is a class name of these objects (IOPMPowerSource) and I/O Kit provides the IOServiceMatching function, which will create this dictionary for us from the class name.

This function returns a reference to a matching dictionary object on success and we immediately pass it into the IOServiceGetMatchingServices function, which in its own turn creates the iterator over found objects:

type io_object_t = mach_port_t;
type io_iterator_t = io_object_t;

extern "C" {
    pub fn IOServiceMatching(name: *const c_char) -> CFMutableDictionaryRef;
    
    pub fn IOServiceGetMatchingServices(
        masterPort: mach_port_t,
        matching: CFDictionaryRef,
        existing: *mut io_iterator_t,
    ) -> kern_return_t;

    pub fn IOObjectRelease(object: io_object_t) -> kern_return_t;
}

unsafe {
    let match_dict = IOServiceMatching(
        b"IOPMPowerSource\0".as_ptr() as *const c_char
    );
    let mut iterator: io_iterator_t = mem::uninitialized();

    // TODO: Handle the possible error
    let _result = IOServiceGetMatchingServices(
        master_port,
        match_dict,
        &mut iterator,
    );
}

IOServiceMatching documentation says that the returned match_dict reference will be automatically freed for us by the IOServiceGetMatchingServices, so there is no need to clean it up manually in that case.

Contrary to that, received iterator object should be freed with a IOObjectRelease function manually later.

Iterating over results

Iterators as a concept should be more than familiar to any Rust developer, so all we need is to advance the obtained iterator till its exhaustion with the help of IOIteratorNext function:

extern "C" {
    pub fn IOIteratorNext(iterator: io_iterator_t) -> io_object_t;
}

unsafe {
    // TODO: Handle the possible error
    let battery_obj = IOIteratorNext(iterator);
}

As you can see, each iteration yields the io_object_t instance, and same to io_iterator_t lifecycle it should be released with a IOObjectRelease too.

Now, yielded io_object_t refers to a some in-kernel object, representing the IOPMPowerSource subclass driver. According to the documentation, each driver should populate a bunch of the required keys into the I/O Registry, such as amperage, voltage, manufacturer details and such — and this is an exact what we need.

We can fetch this information by requesting the properties of this object with a help of IORegistryEntryCreateCFProperties function, which will create the Core Foundation dictionary with all keys and values provided by this object.

unsafe {
    // `CFMutableDictionaryRef` type is provided by the `core-foundation` crate
    let mut props: CFMutableDictionaryRef = mem::uninitialized();

    let _result = IORegistryEntryCreateCFProperties(
        battery_obj,
        &mut props,
        kCFAllocatorDefault,
        0
    );

    let properties: CFDictionary<CFString, CFType> = CFMutableDictionary::wrap_under_create_rule(props).to_immutable();
}

Core Foundation functions are following the naming convention with a “Create” and “Get” rules used to determine the memory ownership, which is pretty much the same as Rust does with “owned” and “borrowed” memory, except you might need to handle the memory release manually. Since IORegistryEntryCreateCFProperties function is a “Create” function, we own the created dictionary reference and with a help of core-foundation crate it can be wrapped into a Rust CFDictionary type, which will also manage the memory release during the value destruction.

In case when called function adheres to the “Get” rule, wrap_under_get_rule can be used instead — it will increase the reference counter for the returned object and decrease it while dropping the type instance.

Debugging like a pro

Cool and dirty debugging hint: Core Foundation has the CFShow function, which prints the contents of the received object into the stderr and it’s literally the same thing as Rust’ dbg! macro. Each type of the core-foundation crate, that implements the TCFType trait, has the show method, which calls the CFShow onto self, helping us to take a look into object contents:

use core_foundation::base::TCFType;

properties.show();

It will print something like this into a stderr, except it would not be pretty-formatted:

CFBasicHash 0x7fbb8840d850 [0x7fff8a67caf0]>{
    type = mutable dict,
    count = 17,
    entries =>
        0 : <CFString 0x7fbb8840d970 [0x7fff8a67caf0]>{
                contents = "ExternalConnected"
            } = <CFBoolean 0x7fff8a67d3f8 [0x7fff8a67caf0]>{
                value = false
            }
        1 : <CFString 0x7fbb8840cf30 [0x7fff8a67caf0]>{
                contents = "Model"
            } = <CFString 0x7fff8a644f38 [0x7fff8a67caf0]>{
            	contents = "bq20z421"
            }
        2 : <CFString 0x7fbb8840d9b0 [0x7fff8a67caf0]>{
            contents = "IsCharging"
        } = <CFBoolean 0x7fff8a67d3f8 [0x7fff8a67caf0]>{
            value = false
        }
…
}

Digging the properties

Finally, we have the information we need, but it is stored in a CFDictionary<CFString, CFType> type, which looks similar to Rust’ own std::collections::HashMap with a CFString type as a key type, but with a base CFType as a value type. Please, note that you should not expect the key type to be the CFString all the time, but in case of power source properties all keys are strings, so we can take a shortcut here.

With the IOPMPowerSource documentation and the debug info from above we know now, which keys are present and what type their values has; what’s left is to fetch the value from the dictionary and “convert” it into an expected type:

let key = CFString::from_static_string("IsCharging");

let is_charging: bool = properties.find(&key)
	.and_then(|value_ref| value_ref.downcast::<CFBoolean>())
	.map(Into::into)
	.expect("Unable to find the `IsCharging` key with a `CFBoolean` value")

Here we are making an assumption that the value for the IsCharging key has the CFBoolean type and downcasting the found reference into it, later converting it into a Rust’ bool type.

Other types can be “converted” similarly:

let key = CFString::from_static_string("Model");

let model: String = properties.find(&key)
    .and_then(|value_ref| value_ref.downcast::<CFString>())
    .map(|value| value.to_string())
    .expect("Unable to find the `Model` key with a `CFString` value")

This is it!

Using Rust for macOS and I/O Kit development is not very convenient at the moment due to lack of the idiomatic bindings, but it is not impossible and from my point of view, not so hard, all you need is the following three steps:

  1. Create the matching dictionary to fetch the devices
  2. Iterate over them
  3. Fetch their properties

My battery crate does exactly this to fetch the batteries information for macOS and so far it works flawlessly, you can check out the sources if you want to take a deeper look into errors handling and working with a mach and core-foundation crates.

Or, if you are using macOS, check out the battop — terminal viewer for notebook batteries.