iohidmanager 0.6.0

Safe Rust bindings for Apple's IOKit HID — enumerate, inspect, and subscribe to HID devices on macOS
Documentation
use core::ffi::c_void;
use core::ptr;

#[allow(clippy::wildcard_imports)]
use super::*;
use crate::ffi_impl as ffi;

#[allow(clippy::type_complexity)]
struct DeviceRemovalContext {
    callback: *mut Box<dyn Fn() + Send + Sync + 'static>,
}

unsafe extern "C" fn device_removal_trampoline(
    context: *mut c_void,
    result: ffi::IOReturn,
    _sender: *mut c_void,
) {
    if context.is_null() || result != ffi::kIOReturnSuccess {
        return;
    }
    let callback = unsafe { &*(*context.cast::<DeviceRemovalContext>()).callback };
    callback();
}

fn build_element_array(elements: &[HidElement]) -> Result<ffi::CFArrayRef, HidError> {
    let capacity = ffi::CFIndex::try_from(elements.len())
        .map_err(|_| HidError::InvalidArgument("array size does not fit CFIndex".to_owned()))?;
    let array = unsafe {
        ffi::CFArrayCreateMutable(
            ffi::kCFAllocatorDefault,
            capacity,
            &raw const ffi::kCFTypeArrayCallBacks,
        )
    };
    if array.is_null() {
        return Err(HidError::OperationFailed("CFArrayCreateMutable"));
    }
    for element in elements {
        unsafe {
            ffi::CFArrayAppendValue(array, element.raw.cast());
        }
    }
    Ok(array.cast())
}

fn build_element_value_dictionary(values: &[(HidElement, HidValue)]) -> Result<ffi::CFDictionaryRef, HidError> {
    let capacity = ffi::CFIndex::try_from(values.len()).map_err(|_| {
        HidError::InvalidArgument("dictionary size does not fit CFIndex".to_owned())
    })?;
    let dict = unsafe {
        ffi::CFDictionaryCreateMutable(
            ffi::kCFAllocatorDefault,
            capacity,
            &raw const ffi::kCFTypeDictionaryKeyCallBacks,
            &raw const ffi::kCFTypeDictionaryValueCallBacks,
        )
    };
    if dict.is_null() {
        return Err(HidError::OperationFailed("CFDictionaryCreateMutable"));
    }
    for (element, value) in values {
        unsafe {
            ffi::CFDictionarySetValue(dict, element.raw.cast(), value.raw.cast());
        }
    }
    Ok(dict.cast())
}

#[allow(clippy::filter_map_bool_then)]
fn read_element_value_dictionary(dict: ffi::CFDictionaryRef) -> Vec<(HidElement, HidValue)> {
    if dict.is_null() {
        return Vec::new();
    }
    let count = usize::try_from(unsafe { ffi::CFDictionaryGetCount(dict) }).unwrap_or(0);
    let mut keys = vec![ptr::null(); count];
    let mut values = vec![ptr::null(); count];
    unsafe {
        ffi::CFDictionaryGetKeysAndValues(dict, keys.as_mut_ptr(), values.as_mut_ptr());
    }
    keys.into_iter()
        .zip(values)
        .filter_map(|(key, value)| {
            (!key.is_null() && !value.is_null()).then(|| {
                clone_value_ref(value.cast_mut()).map(|value| (HidElement { raw: key.cast_mut() }, value))
            })
        })
        .flatten()
        .collect()
}

pub struct DeviceRemovalSubscription {
    device: ffi::IOHIDDeviceRef,
    run_loop: ffi::CFRunLoopRef,
    context: *mut DeviceRemovalContext,
}

unsafe impl Send for DeviceRemovalSubscription {}

impl Drop for DeviceRemovalSubscription {
    fn drop(&mut self) {
        if self.device.is_null() || self.context.is_null() {
            return;
        }
        unsafe {
            ffi::IOHIDDeviceRegisterRemovalCallback(self.device, None, ptr::null_mut());
            close_and_unschedule_device(self.device, self.run_loop);
            ffi::CFRelease(self.device.cast_const());
            let context = Box::from_raw(self.context);
            let _ = Box::from_raw(context.callback);
        }
        self.device = ptr::null_mut();
        self.context = ptr::null_mut();
    }
}

#[allow(clippy::missing_errors_doc)]
impl HidDevice {
    pub fn create(service: ffi::io_service_t) -> Result<Self, HidError> {
        let raw = unsafe { ffi::IOHIDDeviceCreate(ffi::kCFAllocatorDefault, service) };
        if raw.is_null() {
            Err(HidError::OperationFailed("IOHIDDeviceCreate"))
        } else {
            Ok(Self { raw })
        }
    }

    pub fn open_with_options(&self, options: ffi::IOOptionBits) -> Result<(), HidError> {
        let status = unsafe { ffi::IOHIDDeviceOpen(self.raw, options) };
        if status == ffi::kIOReturnSuccess {
            Ok(())
        } else {
            Err(HidError::IoReturn("IOHIDDeviceOpen", status))
        }
    }

    pub fn close_with_options(&self, options: ffi::IOOptionBits) -> Result<(), HidError> {
        let status = unsafe { ffi::IOHIDDeviceClose(self.raw, options) };
        if status == ffi::kIOReturnSuccess {
            Ok(())
        } else {
            Err(HidError::IoReturn("IOHIDDeviceClose", status))
        }
    }

    pub fn activate(&self) {
        unsafe { ffi::IOHIDDeviceActivate(self.raw) };
    }

    pub fn cancel(&self) {
        unsafe { ffi::IOHIDDeviceCancel(self.raw) };
    }

    pub fn on_removal<F>(&self, callback: F) -> Result<DeviceRemovalSubscription, HidError>
    where
        F: Fn() + Send + Sync + 'static,
    {
        let run_loop = open_and_schedule_device(self.raw)?;
        let callback: Box<dyn Fn() + Send + Sync + 'static> = Box::new(callback);
        let callback_ptr = Box::into_raw(Box::new(callback));
        let context_ptr = Box::into_raw(Box::new(DeviceRemovalContext {
            callback: callback_ptr,
        }));
        unsafe {
            ffi::IOHIDDeviceRegisterRemovalCallback(
                self.raw,
                Some(device_removal_trampoline),
                context_ptr.cast(),
            );
            ffi::CFRetain(self.raw.cast_const());
        }
        Ok(DeviceRemovalSubscription {
            device: self.raw,
            run_loop,
            context: context_ptr,
        })
    }

    pub fn set_value_multiple(&self, values: &[(HidElement, HidValue)]) -> Result<(), HidError> {
        let dict = build_element_value_dictionary(values)?;
        let status = unsafe { ffi::IOHIDDeviceSetValueMultiple(self.raw, dict) };
        unsafe { ffi::CFRelease(dict.cast()) };
        if status == ffi::kIOReturnSuccess {
            Ok(())
        } else {
            Err(HidError::IoReturn("IOHIDDeviceSetValueMultiple", status))
        }
    }

    pub fn set_value_multiple_with_timeout(
        &self,
        values: &[(HidElement, HidValue)],
        timeout_ms: f64,
    ) -> Result<(), HidError> {
        let dict = build_element_value_dictionary(values)?;
        let status = unsafe {
            ffi::IOHIDDeviceSetValueMultipleWithCallback(
                self.raw,
                dict,
                timeout_ms,
                None,
                ptr::null_mut(),
            )
        };
        unsafe { ffi::CFRelease(dict.cast()) };
        if status == ffi::kIOReturnSuccess {
            Ok(())
        } else {
            Err(HidError::IoReturn(
                "IOHIDDeviceSetValueMultipleWithCallback",
                status,
            ))
        }
    }

    pub fn copy_value_multiple(
        &self,
        elements: &[HidElement],
    ) -> Result<Vec<(HidElement, HidValue)>, HidError> {
        if elements.is_empty() {
            return Ok(Vec::new());
        }
        let array = build_element_array(elements)?;
        let mut multiple = ptr::null();
        let status = unsafe { ffi::IOHIDDeviceCopyValueMultiple(self.raw, array, &mut multiple) };
        unsafe { ffi::CFRelease(array.cast()) };
        if status != ffi::kIOReturnSuccess {
            return Err(HidError::IoReturn("IOHIDDeviceCopyValueMultiple", status));
        }
        let pairs = read_element_value_dictionary(multiple);
        if !multiple.is_null() {
            unsafe { ffi::CFRelease(multiple.cast()) };
        }
        Ok(pairs)
    }

    pub fn copy_value_multiple_with_timeout(
        &self,
        elements: &[HidElement],
        timeout_ms: f64,
    ) -> Result<Vec<(HidElement, HidValue)>, HidError> {
        if elements.is_empty() {
            return Ok(Vec::new());
        }
        let array = build_element_array(elements)?;
        let mut multiple = ptr::null();
        let status = unsafe {
            ffi::IOHIDDeviceCopyValueMultipleWithCallback(
                self.raw,
                array,
                &mut multiple,
                timeout_ms,
                None,
                ptr::null_mut(),
            )
        };
        unsafe { ffi::CFRelease(array.cast()) };
        if status != ffi::kIOReturnSuccess {
            return Err(HidError::IoReturn(
                "IOHIDDeviceCopyValueMultipleWithCallback",
                status,
            ));
        }
        let pairs = read_element_value_dictionary(multiple);
        if !multiple.is_null() {
            unsafe { ffi::CFRelease(multiple.cast()) };
        }
        Ok(pairs)
    }

    pub fn set_value_with_timeout(
        &self,
        element: &HidElement,
        value: &HidValue,
        timeout_ms: f64,
    ) -> Result<(), HidError> {
        let status = unsafe {
            ffi::IOHIDDeviceSetValueWithCallback(
                self.raw,
                element.raw,
                value.raw,
                timeout_ms,
                None,
                ptr::null_mut(),
            )
        };
        if status == ffi::kIOReturnSuccess {
            Ok(())
        } else {
            Err(HidError::IoReturn("IOHIDDeviceSetValueWithCallback", status))
        }
    }

    pub fn get_value_with_timeout(
        &self,
        element: &HidElement,
        timeout_ms: f64,
    ) -> Result<HidValue, HidError> {
        let mut value = ptr::null_mut();
        let status = unsafe {
            ffi::IOHIDDeviceGetValueWithCallback(
                self.raw,
                element.raw,
                &mut value,
                timeout_ms,
                None,
                ptr::null_mut(),
            )
        };
        if status != ffi::kIOReturnSuccess {
            return Err(HidError::IoReturn("IOHIDDeviceGetValueWithCallback", status));
        }
        clone_value_ref(value).ok_or(HidError::OperationFailed("IOHIDDeviceGetValueWithCallback"))
    }

    pub fn set_report_with_timeout(
        &self,
        report_type: HidReportType,
        report_id: u32,
        report: &[u8],
        timeout_ms: f64,
    ) -> Result<(), HidError> {
        let report_id = ffi::CFIndex::try_from(report_id)
            .map_err(|_| HidError::InvalidArgument("report_id does not fit CFIndex".to_owned()))?;
        let report_length = ffi::CFIndex::try_from(report.len()).map_err(|_| {
            HidError::InvalidArgument("report length does not fit CFIndex".to_owned())
        })?;
        let status = unsafe {
            ffi::IOHIDDeviceSetReportWithCallback(
                self.raw,
                report_type.as_raw(),
                report_id,
                report.as_ptr(),
                report_length,
                timeout_ms,
                None,
                ptr::null_mut(),
            )
        };
        if status == ffi::kIOReturnSuccess {
            Ok(())
        } else {
            Err(HidError::IoReturn("IOHIDDeviceSetReportWithCallback", status))
        }
    }
}