use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_void};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError};
use std::thread;
use core_foundation_sys::array::{
CFArrayGetCount, CFArrayGetTypeID, CFArrayGetValueAtIndex, CFArrayRef,
};
use core_foundation_sys::base::{
kCFAllocatorDefault, CFAllocatorRef, CFGetTypeID, CFIndex, CFRelease, CFTypeRef,
};
use core_foundation_sys::data::{CFDataGetBytePtr, CFDataGetLength, CFDataGetTypeID, CFDataRef};
use core_foundation_sys::dictionary::{
CFDictionaryGetTypeID, CFDictionaryGetValue, CFDictionaryRef, CFMutableDictionaryRef,
};
use core_foundation_sys::number::{
kCFNumberSInt32Type, CFNumberGetTypeID, CFNumberGetValue, CFNumberRef,
};
use core_foundation_sys::runloop::{
kCFRunLoopDefaultMode, kCFRunLoopRunFinished, kCFRunLoopRunStopped, CFRunLoopAddSource,
CFRunLoopGetCurrent, CFRunLoopGetMain, CFRunLoopRef, CFRunLoopRemoveSource, CFRunLoopRunInMode,
CFRunLoopSourceContext, CFRunLoopSourceCreate, CFRunLoopStop, CFRunLoopWakeUp,
};
use core_foundation_sys::set::{CFSetGetCount, CFSetGetValues, CFSetRef};
use core_foundation_sys::string::{
kCFStringEncodingUTF8, CFStringCreateWithCString, CFStringGetCString, CFStringGetCStringPtr,
CFStringGetLength, CFStringGetMaximumSizeForEncoding, CFStringGetTypeID, CFStringRef,
};
use super::queue::ReportQueue;
use super::{payload_after_report_id, HidBackend, HidDeviceBackend};
use crate::error::{HidError, HidResult};
use crate::{BusType, DeviceInfo};
#[allow(non_camel_case_types)]
type io_object_t = u32; #[allow(non_camel_case_types)]
type io_service_t = io_object_t;
type IOOptionBits = u32;
type IOReturn = i32; type IOHIDManagerRef = *mut c_void;
type IOHIDDeviceRef = *mut c_void;
type IOHIDReportType = u32;
type IOHIDReportCallback = unsafe extern "C" fn(
context: *mut c_void,
result: IOReturn,
sender: *mut c_void,
report_type: IOHIDReportType,
report_id: u32,
report: *mut u8,
report_length: CFIndex,
);
type IOHIDCallback =
unsafe extern "C" fn(context: *mut c_void, result: IOReturn, sender: *mut c_void);
#[link(name = "IOKit", kind = "framework")]
extern "C" {
fn IOHIDManagerCreate(allocator: CFAllocatorRef, options: IOOptionBits) -> IOHIDManagerRef;
fn IOHIDManagerSetDeviceMatching(manager: IOHIDManagerRef, matching: CFDictionaryRef);
fn IOHIDManagerCopyDevices(manager: IOHIDManagerRef) -> CFSetRef;
fn IOHIDDeviceCreate(allocator: CFAllocatorRef, service: io_service_t) -> IOHIDDeviceRef;
fn IOHIDDeviceOpen(device: IOHIDDeviceRef, options: IOOptionBits) -> IOReturn;
fn IOHIDDeviceClose(device: IOHIDDeviceRef, options: IOOptionBits) -> IOReturn;
fn IOHIDDeviceGetProperty(device: IOHIDDeviceRef, key: CFStringRef) -> CFTypeRef;
fn IOHIDDeviceSetReport(
device: IOHIDDeviceRef,
report_type: IOHIDReportType,
report_id: CFIndex,
report: *const u8,
report_length: CFIndex,
) -> IOReturn;
fn IOHIDDeviceGetReport(
device: IOHIDDeviceRef,
report_type: IOHIDReportType,
report_id: CFIndex,
report: *mut u8,
report_length: *mut CFIndex,
) -> IOReturn;
fn IOHIDDeviceRegisterInputReportCallback(
device: IOHIDDeviceRef,
report: *mut u8,
report_length: CFIndex,
callback: Option<IOHIDReportCallback>,
context: *mut c_void,
);
fn IOHIDDeviceRegisterRemovalCallback(
device: IOHIDDeviceRef,
callback: Option<IOHIDCallback>,
context: *mut c_void,
);
fn IOHIDDeviceScheduleWithRunLoop(
device: IOHIDDeviceRef,
run_loop: CFRunLoopRef,
run_loop_mode: CFStringRef,
);
fn IOHIDDeviceUnscheduleFromRunLoop(
device: IOHIDDeviceRef,
run_loop: CFRunLoopRef,
run_loop_mode: CFStringRef,
);
fn IOHIDDeviceGetService(device: IOHIDDeviceRef) -> io_service_t;
fn IORegistryEntryGetRegistryEntryID(entry: io_object_t, entry_id: *mut u64) -> IOReturn;
fn IORegistryEntryIDMatching(entry_id: u64) -> CFMutableDictionaryRef;
fn IORegistryEntrySearchCFProperty(
entry: io_service_t,
plane: *const c_char,
key: CFStringRef,
allocator: CFAllocatorRef,
options: u32,
) -> CFTypeRef;
fn IOServiceGetMatchingService(
main_port: io_object_t,
matching: CFDictionaryRef,
) -> io_service_t;
fn IOObjectRelease(object: io_object_t) -> IOReturn;
}
const IO_RETURN_SUCCESS: IOReturn = 0;
const IO_REGISTRY_ITERATE_PARENTS_RECURSIVELY: u32 = 0x1 | 0x2;
const IO_MASTER_PORT_DEFAULT: io_object_t = 0;
const IOHID_OPTIONS_TYPE_NONE: IOOptionBits = 0;
const IOHID_OPTIONS_TYPE_SEIZE_DEVICE: IOOptionBits = 1;
const IOHID_MANAGER_OPTION_NONE: IOOptionBits = 0;
const IOHID_REPORT_TYPE_INPUT: IOHIDReportType = 0;
const IOHID_REPORT_TYPE_OUTPUT: IOHIDReportType = 1;
const IOHID_REPORT_TYPE_FEATURE: IOHIDReportType = 2;
const KEY_VENDOR_ID: &str = "VendorID"; const KEY_PRODUCT_ID: &str = "ProductID"; const KEY_SERIAL_NUMBER: &str = "SerialNumber"; const KEY_MANUFACTURER: &str = "Manufacturer"; const KEY_PRODUCT: &str = "Product"; const KEY_VERSION_NUMBER: &str = "VersionNumber"; const KEY_PRIMARY_USAGE_PAGE: &str = "PrimaryUsagePage"; const KEY_PRIMARY_USAGE: &str = "PrimaryUsage"; const KEY_DEVICE_USAGE_PAIRS: &str = "DeviceUsagePairs"; const KEY_DEVICE_USAGE_PAGE: &str = "DeviceUsagePage"; const KEY_DEVICE_USAGE: &str = "DeviceUsage"; const KEY_TRANSPORT: &str = "Transport"; const KEY_MAX_INPUT_REPORT_SIZE: &str = "MaxInputReportSize"; const KEY_REPORT_DESCRIPTOR: &str = "ReportDescriptor";
const PATH_PREFIX: &str = "DevSrvsID:";
fn parse_dev_srvs_id(path: &str) -> Option<u64> {
path.strip_prefix(PATH_PREFIX)?.parse().ok()
}
fn format_dev_srvs_id(entry_id: u64) -> String {
format!("{PATH_PREFIX}{entry_id}")
}
fn bus_type_from_transport(transport: &str) -> BusType {
match transport {
"USB" => BusType::Usb,
"Bluetooth" | "BluetoothLowEnergy" | "Bluetooth Low Energy" => BusType::Bluetooth,
"I2C" => BusType::I2c,
"SPI" => BusType::Spi,
_ => BusType::Unknown,
}
}
struct CfRef(CFTypeRef);
impl CfRef {
unsafe fn from_create(value: CFTypeRef) -> Option<Self> {
if value.is_null() {
None
} else {
Some(CfRef(value))
}
}
fn as_type_ref(&self) -> CFTypeRef {
self.0
}
}
impl Drop for CfRef {
fn drop(&mut self) {
unsafe { CFRelease(self.0) };
}
}
struct CfString(CFStringRef);
impl CfString {
fn new(s: &str) -> Self {
let c = CString::new(s).expect("CF key contains NUL");
CfString(unsafe {
CFStringCreateWithCString(kCFAllocatorDefault, c.as_ptr(), kCFStringEncodingUTF8)
})
}
fn as_ref(&self) -> CFStringRef {
self.0
}
}
impl Drop for CfString {
fn drop(&mut self) {
if self.0.is_null() {
return;
}
unsafe { CFRelease(self.0 as CFTypeRef) };
}
}
unsafe fn cfstring_to_string(value: CFTypeRef) -> Option<String> {
if value.is_null() || CFGetTypeID(value) != CFStringGetTypeID() {
return None;
}
let s = value as CFStringRef;
let ptr = CFStringGetCStringPtr(s, kCFStringEncodingUTF8);
if !ptr.is_null() {
return Some(CStr::from_ptr(ptr).to_string_lossy().into_owned());
}
let max = CFStringGetMaximumSizeForEncoding(CFStringGetLength(s), kCFStringEncodingUTF8) + 1;
let mut buf = vec![0u8; max as usize];
if CFStringGetCString(
s,
buf.as_mut_ptr() as *mut c_char,
max,
kCFStringEncodingUTF8,
) == 0
{
return None;
}
let nul = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
buf.truncate(nul);
String::from_utf8(buf).ok()
}
unsafe fn cfnumber_to_i32(value: CFTypeRef) -> Option<i32> {
if value.is_null() || CFGetTypeID(value) != CFNumberGetTypeID() {
return None;
}
let mut out: i32 = 0;
CFNumberGetValue(
value as CFNumberRef,
kCFNumberSInt32Type,
&mut out as *mut i32 as *mut c_void,
)
.then_some(out)
}
unsafe fn device_property(device: IOHIDDeviceRef, key: &str) -> CFTypeRef {
IOHIDDeviceGetProperty(device, CfString::new(key).as_ref())
}
unsafe fn string_property(device: IOHIDDeviceRef, key: &str) -> Option<String> {
cfstring_to_string(device_property(device, key))
}
unsafe fn i32_property(device: IOHIDDeviceRef, key: &str) -> Option<i32> {
cfnumber_to_i32(device_property(device, key))
}
unsafe fn registry_entry_id(device: IOHIDDeviceRef) -> Option<u64> {
let service = IOHIDDeviceGetService(device);
if service == 0 {
return None;
}
let mut entry_id = 0u64;
(IORegistryEntryGetRegistryEntryID(service, &mut entry_id) == IO_RETURN_SUCCESS)
.then_some(entry_id)
}
unsafe fn usb_interface_number(device: IOHIDDeviceRef) -> i32 {
let service = IOHIDDeviceGetService(device);
if service == 0 {
return -1;
}
let plane = b"IOService\0";
let value = CfRef::from_create(IORegistryEntrySearchCFProperty(
service,
plane.as_ptr() as *const c_char,
CfString::new("bInterfaceNumber").as_ref(),
kCFAllocatorDefault,
IO_REGISTRY_ITERATE_PARENTS_RECURSIVELY,
));
value
.and_then(|v| cfnumber_to_i32(v.as_type_ref()))
.unwrap_or(-1)
}
unsafe fn device_infos(device: IOHIDDeviceRef) -> Vec<DeviceInfo> {
let info = DeviceInfo {
path: registry_entry_id(device)
.map(format_dev_srvs_id)
.unwrap_or_default(),
vendor_id: i32_property(device, KEY_VENDOR_ID).unwrap_or(0) as u16,
product_id: i32_property(device, KEY_PRODUCT_ID).unwrap_or(0) as u16,
serial_number: string_property(device, KEY_SERIAL_NUMBER),
release_number: i32_property(device, KEY_VERSION_NUMBER).unwrap_or(0) as u16,
manufacturer_string: string_property(device, KEY_MANUFACTURER),
product_string: string_property(device, KEY_PRODUCT),
usage_page: i32_property(device, KEY_PRIMARY_USAGE_PAGE).unwrap_or(0) as u16,
usage: i32_property(device, KEY_PRIMARY_USAGE).unwrap_or(0) as u16,
interface_number: usb_interface_number(device),
bus_type: string_property(device, KEY_TRANSPORT)
.as_deref()
.map(bus_type_from_transport)
.unwrap_or(BusType::Unknown),
};
let mut usages = vec![(info.usage_page, info.usage)];
let pairs = device_property(device, KEY_DEVICE_USAGE_PAIRS);
if !pairs.is_null() && CFGetTypeID(pairs) == CFArrayGetTypeID() {
let pairs = pairs as CFArrayRef;
let page_key = CfString::new(KEY_DEVICE_USAGE_PAGE);
let usage_key = CfString::new(KEY_DEVICE_USAGE);
for i in 0..CFArrayGetCount(pairs) {
let dict = CFArrayGetValueAtIndex(pairs, i) as CFTypeRef;
if dict.is_null() || CFGetTypeID(dict) != CFDictionaryGetTypeID() {
continue;
}
let dict = dict as CFDictionaryRef;
let page = cfnumber_to_i32(CFDictionaryGetValue(
dict,
page_key.as_ref() as *const c_void,
));
let usage = cfnumber_to_i32(CFDictionaryGetValue(
dict,
usage_key.as_ref() as *const c_void,
));
let (Some(page), Some(usage)) = (page, usage) else {
continue;
};
let pair = (page as u16, usage as u16);
if !usages.contains(&pair) {
usages.push(pair);
}
}
}
info.per_usage(&usages)
}
pub struct MacApi {
open_exclusive: AtomicBool,
}
impl MacApi {
pub fn set_open_exclusive(&self, exclusive: bool) {
self.open_exclusive.store(exclusive, Ordering::Relaxed);
}
pub fn open_exclusive(&self) -> bool {
self.open_exclusive.load(Ordering::Relaxed)
}
}
impl HidBackend for MacApi {
type Device = MacDevice;
fn new() -> HidResult<Self> {
Ok(MacApi {
open_exclusive: AtomicBool::new(false),
})
}
fn enumerate(&self, vendor_id: u16, product_id: u16) -> HidResult<Vec<DeviceInfo>> {
let mut result = Vec::new();
unsafe {
let manager = IOHIDManagerCreate(kCFAllocatorDefault, IOHID_MANAGER_OPTION_NONE);
if manager.is_null() {
return Err(HidError::Initialization {
message: "IOHIDManagerCreate returned NULL".into(),
});
}
IOHIDManagerSetDeviceMatching(manager, std::ptr::null());
let set = IOHIDManagerCopyDevices(manager);
if set.is_null() {
CFRelease(manager as CFTypeRef);
return Ok(result);
}
let count = CFSetGetCount(set) as usize;
let mut devices: Vec<*const c_void> = vec![std::ptr::null(); count];
CFSetGetValues(set, devices.as_mut_ptr());
for device in devices {
if device.is_null() {
continue;
}
result.extend(
device_infos(device as IOHIDDeviceRef)
.into_iter()
.filter(|info| info.matches(vendor_id, product_id)),
);
}
CFRelease(set as CFTypeRef);
CFRelease(manager as CFTypeRef);
}
Ok(result)
}
fn open_path(&self, path: &str) -> HidResult<MacDevice> {
let entry_id = parse_dev_srvs_id(path).ok_or_else(|| HidError::InvalidData {
message: format!("not a macOS device path (expected \"DevSrvsID:<id>\"): {path}"),
})?;
let options = if self.open_exclusive() {
IOHID_OPTIONS_TYPE_SEIZE_DEVICE
} else {
IOHID_OPTIONS_TYPE_NONE
};
MacDevice::open(entry_id, options)
}
}
#[derive(Default)]
struct Shared {
queue: ReportQueue,
thread: Mutex<ThreadState>,
ready_cond: Condvar,
}
#[derive(Default)]
struct ThreadState {
run_loop: usize,
ready: bool,
}
impl Shared {
fn new() -> Self {
Shared {
queue: ReportQueue::new("device read thread is shut down"),
..Default::default()
}
}
fn lock_thread(&self) -> MutexGuard<'_, ThreadState> {
self.thread.lock().unwrap_or_else(PoisonError::into_inner)
}
}
unsafe extern "C" fn input_report_callback(
context: *mut c_void,
_result: IOReturn,
_sender: *mut c_void,
_report_type: IOHIDReportType,
_report_id: u32,
report: *mut u8,
report_length: CFIndex,
) {
if report.is_null() || report_length <= 0 {
return;
}
let shared = &*(context as *const Shared);
let data = std::slice::from_raw_parts(report, report_length as usize).to_vec();
shared.queue.push(data);
}
unsafe extern "C" fn removal_callback(
context: *mut c_void,
_result: IOReturn,
_sender: *mut c_void,
) {
let shared = &*(context as *const Shared);
shared.queue.set_disconnected();
let thread = shared.lock_thread();
if thread.run_loop != 0 {
CFRunLoopStop(thread.run_loop as CFRunLoopRef);
}
}
extern "C" fn keepalive_perform(_info: *const c_void) {}
#[derive(Clone, Copy)]
struct SendRef<T>(*mut T);
unsafe impl<T> Send for SendRef<T> {}
fn read_thread(device: SendRef<c_void>, mode: SendRef<c_void>, shared: Arc<Shared>) {
let device: IOHIDDeviceRef = device.0;
let mode = mode.0 as CFStringRef;
unsafe {
let run_loop = CFRunLoopGetCurrent();
IOHIDDeviceScheduleWithRunLoop(device, run_loop, mode);
let mut source_context = CFRunLoopSourceContext {
version: 0,
info: std::ptr::null_mut(),
retain: None,
release: None,
copyDescription: None,
equal: None,
hash: None,
schedule: None,
cancel: None,
perform: keepalive_perform,
};
let keepalive = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &mut source_context);
CFRunLoopAddSource(run_loop, keepalive, mode);
{
let mut thread = shared.lock_thread();
thread.run_loop = run_loop as usize;
thread.ready = true;
shared.ready_cond.notify_all();
}
loop {
if shared.queue.is_shutdown() || shared.queue.is_disconnected() {
break;
}
let code = CFRunLoopRunInMode(mode, 1000.0, 0);
if code == kCFRunLoopRunFinished || code == kCFRunLoopRunStopped {
if !shared.queue.is_shutdown() {
shared.queue.set_disconnected();
}
break;
}
}
CFRunLoopRemoveSource(run_loop, keepalive, mode);
CFRelease(keepalive as CFTypeRef);
shared.lock_thread().run_loop = 0;
shared.queue.wake_parked();
}
}
pub struct MacDevice {
device: IOHIDDeviceRef,
open_options: IOOptionBits,
run_loop_mode: CFStringRef,
input_buf: *mut u8,
input_buf_len: usize,
shared: Arc<Shared>,
read_thread: Option<thread::JoinHandle<()>>,
}
unsafe impl Send for MacDevice {}
unsafe impl Sync for MacDevice {}
impl MacDevice {
fn open(entry_id: u64, options: IOOptionBits) -> HidResult<Self> {
unsafe {
let matching = IORegistryEntryIDMatching(entry_id);
if matching.is_null() {
return Err(HidError::backend("IORegistryEntryIDMatching failed"));
}
let service = IOServiceGetMatchingService(IO_MASTER_PORT_DEFAULT, matching);
if service == 0 {
return Err(HidError::DeviceNotFound);
}
let device = IOHIDDeviceCreate(kCFAllocatorDefault, service);
IOObjectRelease(service);
if device.is_null() {
return Err(HidError::backend("IOHIDDeviceCreate failed"));
}
let ret = IOHIDDeviceOpen(device, options);
if ret != IO_RETURN_SUCCESS {
CFRelease(device as CFTypeRef);
return Err(HidError::OpenFailed {
message: format!("IOHIDDeviceOpen failed: IOReturn 0x{:08x}", ret as u32),
});
}
let input_buf_len = i32_property(device, KEY_MAX_INPUT_REPORT_SIZE)
.filter(|&len| len > 0)
.unwrap_or(64) as usize;
let input_buf = Box::into_raw(vec![0u8; input_buf_len].into_boxed_slice()) as *mut u8;
let mode_name =
CString::new(format!("HIDRA_{device:p}")).expect("no NUL in pointer format");
let run_loop_mode = CFStringCreateWithCString(
kCFAllocatorDefault,
mode_name.as_ptr(),
kCFStringEncodingUTF8,
);
let shared = Arc::new(Shared::new());
let context = Arc::as_ptr(&shared) as *mut c_void;
IOHIDDeviceRegisterInputReportCallback(
device,
input_buf,
input_buf_len as CFIndex,
Some(input_report_callback),
context,
);
IOHIDDeviceRegisterRemovalCallback(device, Some(removal_callback), context);
let thread = thread::Builder::new().name("hidra-hid-read".into()).spawn({
let shared = Arc::clone(&shared);
let device = SendRef(device);
let mode = SendRef(run_loop_mode as *mut c_void);
move || read_thread(device, mode, shared)
});
let thread = match thread {
Ok(thread) => thread,
Err(err) => {
IOHIDDeviceRegisterInputReportCallback(
device,
input_buf,
input_buf_len as CFIndex,
None,
context,
);
IOHIDDeviceRegisterRemovalCallback(device, None, context);
IOHIDDeviceClose(device, options);
CFRelease(device as CFTypeRef);
CFRelease(run_loop_mode as CFTypeRef);
drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(
input_buf,
input_buf_len,
)));
return Err(HidError::io("spawning HID read thread", err));
}
};
{
let mut thread = shared.lock_thread();
while !thread.ready {
thread = shared
.ready_cond
.wait(thread)
.unwrap_or_else(PoisonError::into_inner);
}
}
Ok(MacDevice {
device,
open_options: options,
run_loop_mode,
input_buf,
input_buf_len,
shared,
read_thread: Some(thread),
})
}
}
fn disconnected(&self) -> bool {
self.shared.queue.is_disconnected()
}
fn set_report(&self, report_type: IOHIDReportType, data: &[u8]) -> HidResult<usize> {
if data.is_empty() {
return Err(HidError::InvalidData {
message: "report data must contain a report ID byte".into(),
});
}
if self.disconnected() {
return Err(HidError::Disconnected);
}
let report_id = data[0];
let payload = payload_after_report_id(data);
let ret = unsafe {
IOHIDDeviceSetReport(
self.device,
report_type,
report_id as CFIndex,
payload.as_ptr(),
payload.len() as CFIndex,
)
};
if ret != IO_RETURN_SUCCESS {
return Err(HidError::backend(format!(
"IOHIDDeviceSetReport failed: IOReturn 0x{:08x}",
ret as u32
)));
}
Ok(data.len())
}
fn get_report(&self, report_type: IOHIDReportType, buf: &mut [u8]) -> HidResult<usize> {
if buf.is_empty() {
return Err(HidError::InvalidData {
message: "buffer must contain a report ID byte".into(),
});
}
if self.disconnected() {
return Err(HidError::Disconnected);
}
let report_id = buf[0];
let body = if report_id == 0 {
&mut buf[1..]
} else {
&mut *buf
};
let mut len = body.len() as CFIndex;
let ret = unsafe {
IOHIDDeviceGetReport(
self.device,
report_type,
report_id as CFIndex,
body.as_mut_ptr(),
&mut len,
)
};
if ret != IO_RETURN_SUCCESS {
return Err(HidError::backend(format!(
"IOHIDDeviceGetReport failed: IOReturn 0x{:08x}",
ret as u32
)));
}
let mut returned = len as usize;
if report_id == 0 {
returned += 1; }
Ok(returned)
}
}
impl HidDeviceBackend for MacDevice {
type Read<'a> = ReadAsync<'a>;
fn write(&self, data: &[u8]) -> HidResult<usize> {
self.set_report(IOHID_REPORT_TYPE_OUTPUT, data)
}
fn read_async<'a>(&'a self, buf: &'a mut [u8]) -> ReadAsync<'a> {
ReadAsync { dev: self, buf }
}
fn send_feature_report(&self, data: &[u8]) -> HidResult<()> {
self.set_report(IOHID_REPORT_TYPE_FEATURE, data).map(|_| ())
}
fn get_feature_report(&self, buf: &mut [u8]) -> HidResult<usize> {
self.get_report(IOHID_REPORT_TYPE_FEATURE, buf)
}
fn get_input_report(&self, buf: &mut [u8]) -> HidResult<usize> {
self.get_report(IOHID_REPORT_TYPE_INPUT, buf)
}
fn get_manufacturer_string(&self) -> HidResult<Option<String>> {
Ok(unsafe { string_property(self.device, KEY_MANUFACTURER) })
}
fn get_product_string(&self) -> HidResult<Option<String>> {
Ok(unsafe { string_property(self.device, KEY_PRODUCT) })
}
fn get_serial_number_string(&self) -> HidResult<Option<String>> {
Ok(unsafe { string_property(self.device, KEY_SERIAL_NUMBER) })
}
fn get_indexed_string(&self, _index: u32) -> HidResult<Option<String>> {
Err(HidError::Unsupported {
message: "indexed strings are not available via IOHIDDevice; use the usb backend"
.into(),
})
}
fn get_report_descriptor(&self, buf: &mut [u8]) -> HidResult<usize> {
unsafe {
let value = device_property(self.device, KEY_REPORT_DESCRIPTOR);
if value.is_null() || CFGetTypeID(value) != CFDataGetTypeID() {
return Err(HidError::backend(
"failed to read the ReportDescriptor property",
));
}
let data = value as CFDataRef;
let len = (CFDataGetLength(data) as usize).min(buf.len());
std::ptr::copy_nonoverlapping(CFDataGetBytePtr(data), buf.as_mut_ptr(), len);
Ok(len)
}
}
fn get_device_info(&self) -> HidResult<DeviceInfo> {
let mut infos = unsafe { device_infos(self.device) };
Ok(infos.remove(0))
}
}
impl Drop for MacDevice {
fn drop(&mut self) {
unsafe {
let disconnected = self.shared.queue.is_disconnected();
let run_loop = self.shared.lock_thread().run_loop;
let context = Arc::as_ptr(&self.shared) as *mut c_void;
if !disconnected {
IOHIDDeviceRegisterInputReportCallback(
self.device,
self.input_buf,
self.input_buf_len as CFIndex,
None,
context,
);
IOHIDDeviceRegisterRemovalCallback(self.device, None, context);
if run_loop != 0 {
IOHIDDeviceUnscheduleFromRunLoop(
self.device,
run_loop as CFRunLoopRef,
self.run_loop_mode,
);
IOHIDDeviceScheduleWithRunLoop(
self.device,
CFRunLoopGetMain(),
kCFRunLoopDefaultMode,
);
}
}
self.shared.queue.set_shutdown();
if run_loop != 0 {
CFRunLoopStop(run_loop as CFRunLoopRef);
CFRunLoopWakeUp(run_loop as CFRunLoopRef);
}
if let Some(thread) = self.read_thread.take() {
let _ = thread.join();
}
if !disconnected {
IOHIDDeviceClose(self.device, self.open_options);
}
CFRelease(self.device as CFTypeRef);
CFRelease(self.run_loop_mode as CFTypeRef);
drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(
self.input_buf,
self.input_buf_len,
)));
}
}
}
pub struct ReadAsync<'a> {
dev: &'a MacDevice,
buf: &'a mut [u8],
}
impl std::future::Future for ReadAsync<'_> {
type Output = HidResult<usize>;
fn poll(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Self::Output> {
let this = self.get_mut();
this.dev.shared.queue.poll_read(this.buf, cx)
}
}
impl core::fmt::Debug for MacApi {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("MacApi").finish_non_exhaustive()
}
}
impl core::fmt::Debug for MacDevice {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("MacDevice").finish_non_exhaustive()
}
}
impl core::fmt::Debug for ReadAsync<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ReadAsync").finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_dev_srvs_id_paths() {
assert_eq!(parse_dev_srvs_id("DevSrvsID:4295123456"), Some(4295123456));
assert_eq!(parse_dev_srvs_id("DevSrvsID:0"), Some(0));
assert_eq!(parse_dev_srvs_id("DevSrvsID:"), None);
assert_eq!(parse_dev_srvs_id("DevSrvsID:abc"), None);
assert_eq!(parse_dev_srvs_id("DevSrvsID:-3"), None);
assert_eq!(parse_dev_srvs_id("/dev/hidraw0"), None);
assert_eq!(parse_dev_srvs_id(""), None);
}
#[test]
fn formats_and_reparses_paths() {
assert_eq!(format_dev_srvs_id(42), "DevSrvsID:42");
let path = format_dev_srvs_id(u64::MAX);
assert_eq!(path, "DevSrvsID:18446744073709551615");
assert_eq!(parse_dev_srvs_id(&path), Some(u64::MAX));
}
#[test]
fn maps_transport_strings() {
assert_eq!(bus_type_from_transport("USB"), BusType::Usb);
assert_eq!(bus_type_from_transport("Bluetooth"), BusType::Bluetooth);
assert_eq!(
bus_type_from_transport("BluetoothLowEnergy"),
BusType::Bluetooth
);
assert_eq!(
bus_type_from_transport("Bluetooth Low Energy"),
BusType::Bluetooth
);
assert_eq!(bus_type_from_transport("I2C"), BusType::I2c);
assert_eq!(bus_type_from_transport("SPI"), BusType::Spi);
assert_eq!(bus_type_from_transport("AirPlay"), BusType::Unknown);
assert_eq!(bus_type_from_transport(""), BusType::Unknown);
}
}