iohidmanager 0.10.4

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::{bridge, ffi_impl as ffi};
use doom_fish_utils::panic_safe::catch_user_panic;

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

unsafe extern "C" fn queue_value_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::<QueueValueContext>()).callback };
    catch_user_panic("queue_value_trampoline", || {
        callback();
    });
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
/// Wraps the option bits consumed by `IOHIDQueueCreate`.
pub struct HidQueueOptions(u32);

impl HidQueueOptions {
    /// Mirrors `kIOHIDQueueOptionsTypeNone`.
    pub const NONE: Self = Self(ffi::kIOHIDQueueOptionsTypeNone);
    /// Mirrors `kIOHIDQueueOptionsTypeEnqueueAll`.
    pub const ENQUEUE_ALL: Self = Self(ffi::kIOHIDQueueOptionsTypeEnqueueAll);

    #[must_use]
    /// Returns the raw option bits passed to `IOHIDQueueCreate`.
    pub const fn bits(self) -> u32 {
        self.0
    }
}

/// Owns an `IOHIDQueueRef` returned by `IOHIDQueueCreate`.
pub struct HidQueue {
    raw: ffi::IOHIDQueueRef,
}

unsafe impl Send for HidQueue {}
unsafe impl Sync for HidQueue {}

impl Clone for HidQueue {
    fn clone(&self) -> Self {
        unsafe { ffi::CFRetain(self.raw.cast_const()) };
        Self { raw: self.raw }
    }
}

impl Drop for HidQueue {
    fn drop(&mut self) {
        if !self.raw.is_null() {
            unsafe { ffi::CFRelease(self.raw.cast_const()) };
            self.raw = ptr::null_mut();
        }
    }
}

/// Owns a registration from `IOHIDQueueRegisterValueAvailableCallback`.
pub struct QueueValueAvailableSubscription {
    queue: ffi::IOHIDQueueRef,
    run_loop: ffi::CFRunLoopRef,
    context: *mut QueueValueContext,
}

unsafe impl Send for QueueValueAvailableSubscription {}

impl Drop for QueueValueAvailableSubscription {
    fn drop(&mut self) {
        if self.queue.is_null() || self.context.is_null() {
            return;
        }
        unsafe {
            ffi::IOHIDQueueRegisterValueAvailableCallback(self.queue, None, ptr::null_mut());
            ffi::IOHIDQueueUnscheduleFromRunLoop(
                self.queue,
                self.run_loop,
                ffi::kCFRunLoopDefaultMode,
            );
            ffi::CFRelease(self.queue.cast_const());
            let context = Box::from_raw(self.context);
            let _ = Box::from_raw(context.callback);
        }
        self.queue = ptr::null_mut();
        self.context = ptr::null_mut();
    }
}

#[allow(clippy::missing_errors_doc)]
impl HidQueue {
    #[must_use]
    /// Wraps `IOHIDQueueGetTypeID`.
    pub fn type_id() -> ffi::CFTypeID {
        unsafe { bridge::iohidmanager_swift_queue_type_id() }
    }

    /// Wraps `IOHIDQueueCreate` with `kIOHIDQueueOptionsTypeNone`.
    pub fn new(device: &HidDevice, depth: usize) -> Result<Self, HidError> {
        Self::with_options(device, depth, HidQueueOptions::NONE)
    }

    /// Wraps `IOHIDQueueCreate`.
    pub fn with_options(
        device: &HidDevice,
        depth: usize,
        options: HidQueueOptions,
    ) -> Result<Self, HidError> {
        let depth = ffi::CFIndex::try_from(depth)
            .map_err(|_| HidError::InvalidArgument("depth does not fit CFIndex".to_owned()))?;
        let raw = unsafe {
            ffi::IOHIDQueueCreate(ffi::kCFAllocatorDefault, device.raw, depth, options.bits())
        };
        if raw.is_null() {
            Err(HidError::OperationFailed("IOHIDQueueCreate"))
        } else {
            Ok(Self { raw })
        }
    }

    #[must_use]
    /// Wraps `IOHIDQueueGetDevice`.
    pub fn device(&self) -> Option<HidDevice> {
        let device = unsafe { ffi::IOHIDQueueGetDevice(self.raw) };
        if device.is_null() {
            None
        } else {
            unsafe { ffi::CFRetain(device) };
            Some(HidDevice { raw: device })
        }
    }

    #[must_use]
    /// Wraps `IOHIDQueueGetDepth`.
    pub fn depth(&self) -> usize {
        usize::try_from(unsafe { ffi::IOHIDQueueGetDepth(self.raw) }).unwrap_or(0)
    }

    /// Wraps `IOHIDQueueSetDepth`.
    pub fn set_depth(&self, depth: usize) -> Result<(), HidError> {
        let depth = ffi::CFIndex::try_from(depth)
            .map_err(|_| HidError::InvalidArgument("depth does not fit CFIndex".to_owned()))?;
        unsafe { ffi::IOHIDQueueSetDepth(self.raw, depth) };
        Ok(())
    }

    /// Wraps `IOHIDQueueAddElement`.
    pub fn add_element(&self, element: &HidElement) {
        unsafe { ffi::IOHIDQueueAddElement(self.raw, element.raw) };
    }

    /// Wraps `IOHIDQueueRemoveElement`.
    pub fn remove_element(&self, element: &HidElement) {
        unsafe { ffi::IOHIDQueueRemoveElement(self.raw, element.raw) };
    }

    #[must_use]
    /// Wraps `IOHIDQueueContainsElement`.
    pub fn contains_element(&self, element: &HidElement) -> bool {
        unsafe { ffi::IOHIDQueueContainsElement(self.raw, element.raw) }
    }

    /// Wraps `IOHIDQueueStart`.
    pub fn start(&self) {
        unsafe { ffi::IOHIDQueueStart(self.raw) };
    }

    /// Wraps `IOHIDQueueStop`.
    pub fn stop(&self) {
        unsafe { ffi::IOHIDQueueStop(self.raw) };
    }

    /// Wraps `IOHIDQueueActivate`.
    pub fn activate(&self) {
        unsafe { ffi::IOHIDQueueActivate(self.raw) };
    }

    /// Wraps `IOHIDQueueCancel`.
    pub fn cancel(&self) {
        unsafe { ffi::IOHIDQueueCancel(self.raw) };
    }

    #[must_use]
    /// Wraps `IOHIDQueueScheduleWithRunLoop` for the current run loop.
    pub fn schedule_current_run_loop(&self) -> ffi::CFRunLoopRef {
        let run_loop = unsafe { ffi::CFRunLoopGetCurrent() };
        unsafe {
            ffi::IOHIDQueueScheduleWithRunLoop(self.raw, run_loop, ffi::kCFRunLoopDefaultMode);
        }
        run_loop
    }

    #[allow(clippy::not_unsafe_ptr_arg_deref)]
    /// Wraps `IOHIDQueueUnscheduleFromRunLoop`.
    pub fn unschedule_from_run_loop(&self, run_loop: ffi::CFRunLoopRef) {
        unsafe {
            ffi::IOHIDQueueUnscheduleFromRunLoop(self.raw, run_loop, ffi::kCFRunLoopDefaultMode);
        }
    }

    #[must_use]
    /// Wraps `IOHIDQueueCopyNextValue`.
    pub fn copy_next_value(&self) -> Option<HidValue> {
        clone_value_ref(unsafe { ffi::IOHIDQueueCopyNextValue(self.raw) })
    }

    #[must_use]
    /// Wraps `IOHIDQueueCopyNextValueWithTimeout`.
    pub fn copy_next_value_with_timeout(&self, timeout_ms: f64) -> Option<HidValue> {
        clone_value_ref(unsafe { ffi::IOHIDQueueCopyNextValueWithTimeout(self.raw, timeout_ms) })
    }

    /// Wraps `IOHIDQueueRegisterValueAvailableCallback`.
    pub fn on_value_available<F>(
        &self,
        callback: F,
    ) -> Result<QueueValueAvailableSubscription, HidError>
    where
        F: Fn() + Send + Sync + 'static,
    {
        let run_loop = self.schedule_current_run_loop();
        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(QueueValueContext {
            callback: callback_ptr,
        }));
        unsafe {
            ffi::IOHIDQueueRegisterValueAvailableCallback(
                self.raw,
                Some(queue_value_trampoline),
                context_ptr.cast(),
            );
            ffi::CFRetain(self.raw.cast_const());
        }
        Ok(QueueValueAvailableSubscription {
            queue: self.raw,
            run_loop,
            context: context_ptr,
        })
    }

    #[must_use]
    /// Mirrors `IOHIDQueueRef`.
    pub const fn as_ptr(&self) -> ffi::IOHIDQueueRef {
        self.raw
    }
}