Skip to content

Commit

Permalink
Merge pull request #12 from kevinmehall/macos
Browse files Browse the repository at this point in the history
Implement macOS backend
  • Loading branch information
kevinmehall authored Dec 4, 2023
2 parents 29f092b + 5dfd387 commit 89ee5a9
Show file tree
Hide file tree
Showing 16 changed files with 2,164 additions and 8 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
os: [ubuntu-latest, windows-latest, macos-latest]

runs-on: ${{ matrix.os }}

Expand Down
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,8 @@ rustix = { version = "0.38.17", features = ["fs", "event"] }

[target.'cfg(target_os="windows")'.dependencies]
windows-sys = { version = "0.48.0", features = ["Win32_Devices_Usb", "Win32_Devices_DeviceAndDriverInstallation", "Win32_Foundation", "Win32_Devices_Properties", "Win32_Storage_FileSystem", "Win32_Security", "Win32_System_IO", "Win32_System_Registry", "Win32_System_Com"] }

[target.'cfg(target_os="macos")'.dependencies]
core-foundation = "0.9.3"
core-foundation-sys = "0.8.4"
io-kit-sys = "0.4.0"
4 changes: 4 additions & 0 deletions examples/bulk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ fn main() {
let device = di.open().unwrap();
let interface = device.claim_interface(0).unwrap();

block_on(interface.bulk_out(0x02, Vec::from([1, 2, 3, 4, 5])))
.into_result()
.unwrap();

let mut queue = interface.bulk_in_queue(0x81);

loop {
Expand Down
2 changes: 1 addition & 1 deletion examples/control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ fn main() {
let device = di.open().unwrap();

// Linux can make control transfers without claiming an interface
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
let result = block_on(device.control_out(ControlOut {
control_type: ControlType::Vendor,
Expand Down
6 changes: 3 additions & 3 deletions src/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ impl Device {
///
/// * Not supported on Windows. You must [claim an interface][`Device::claim_interface`]
/// and use the interface handle to submit transfers.
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub fn control_in(&self, data: ControlIn) -> TransferFuture<ControlIn> {
let mut t = self.backend.make_control_transfer();
t.submit::<ControlIn>(data);
Expand Down Expand Up @@ -121,7 +121,7 @@ impl Device {
///
/// * Not supported on Windows. You must [claim an interface][`Device::claim_interface`]
/// and use the interface handle to submit transfers.
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub fn control_out(&self, data: ControlOut) -> TransferFuture<ControlOut> {
let mut t = self.backend.make_control_transfer();
t.submit::<ControlOut>(data);
Expand All @@ -130,7 +130,7 @@ impl Device {
}

/// An opened interface of a USB device.
///
///
/// Obtain an `Interface` with the [`Device::claim_interface`] method.
///
/// This type is reference-counted with an [`Arc`] internally, and can be cloned cheaply for
Expand Down
10 changes: 10 additions & 0 deletions src/enumeration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ pub struct DeviceInfo {
#[cfg(target_os = "windows")]
pub(crate) interfaces: HashMap<u8, OsString>,

#[cfg(target_os = "macos")]
pub(crate) location_id: u32,

pub(crate) bus_number: u8,
pub(crate) device_address: u8,

Expand Down Expand Up @@ -87,6 +90,12 @@ impl DeviceInfo {
self.driver.as_deref()
}

/// *(macOS-only)* IOKit Location ID
#[cfg(target_os = "macos")]
pub fn location_id(&self) -> u32 {
self.location_id
}

/// Number identifying the bus / host controller where the device is connected.
pub fn bus_number(&self) -> u8 {
self.bus_number
Expand Down Expand Up @@ -225,6 +234,7 @@ pub enum Speed {
}

impl Speed {
#[allow(dead_code)] // not used on all platforms
pub(crate) fn from_str(s: &str) -> Option<Self> {
match s {
"low" | "1.5" => Some(Speed::Low),
Expand Down
135 changes: 135 additions & 0 deletions src/platform/macos_iokit/device.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
use std::{collections::BTreeMap, io::ErrorKind, sync::Arc};

use log::{debug, error};

use crate::{
platform::macos_iokit::events::add_event_source,
transfer::{EndpointType, TransferHandle},
DeviceInfo, Error,
};

use super::{
enumeration::service_by_location_id,
events::EventRegistration,
iokit::{call_iokit_function, check_iokit_return},
iokit_usb::{EndpointInfo, IoKitDevice, IoKitInterface},
};

pub(crate) struct MacDevice {
_event_registration: EventRegistration,
pub(super) device: IoKitDevice,
}

impl MacDevice {
pub(crate) fn from_device_info(d: &DeviceInfo) -> Result<Arc<MacDevice>, Error> {
let service = service_by_location_id(d.location_id)?;
let device = IoKitDevice::new(service)?;
let _event_registration = add_event_source(device.create_async_event_source()?);

Ok(Arc::new(MacDevice {
_event_registration,
device,
}))
}

pub(crate) fn set_configuration(&self, configuration: u8) -> Result<(), Error> {
unsafe {
check_iokit_return(call_iokit_function!(
self.device.raw,
SetConfiguration(configuration)
))
}
}

pub(crate) fn reset(&self) -> Result<(), Error> {
unsafe {
check_iokit_return(call_iokit_function!(
self.device.raw,
USBDeviceReEnumerate(0)
))
}
}

pub(crate) fn make_control_transfer(self: &Arc<Self>) -> TransferHandle<super::TransferData> {
TransferHandle::new(super::TransferData::new_control(self.clone()))
}

pub(crate) fn claim_interface(
self: &Arc<Self>,
interface_number: u8,
) -> Result<Arc<MacInterface>, Error> {
let intf_service = self
.device
.create_interface_iterator()?
.nth(interface_number as usize)
.ok_or(Error::new(ErrorKind::NotFound, "interface not found"))?;

let mut interface = IoKitInterface::new(intf_service)?;
let _event_registration = add_event_source(interface.create_async_event_source()?);

interface.open()?;

let endpoints = interface.endpoints()?;
debug!("Found endpoints: {endpoints:?}");

Ok(Arc::new(MacInterface {
device: self.clone(),
interface_number,
interface,
endpoints,
_event_registration,
}))
}
}

pub(crate) struct MacInterface {
pub(crate) interface_number: u8,
_event_registration: EventRegistration,
pub(crate) interface: IoKitInterface,
pub(crate) device: Arc<MacDevice>,

/// Map from address to a structure that contains the `pipe_ref` used by iokit
pub(crate) endpoints: BTreeMap<u8, EndpointInfo>,
}

impl MacInterface {
pub(crate) fn make_transfer(
self: &Arc<Self>,
endpoint: u8,
ep_type: EndpointType,
) -> TransferHandle<super::TransferData> {
if ep_type == EndpointType::Control {
assert!(endpoint == 0);
TransferHandle::new(super::TransferData::new_control(self.device.clone()))
} else {
let endpoint = self.endpoints.get(&endpoint).expect("Endpoint not found");
TransferHandle::new(super::TransferData::new(
self.device.clone(),
self.clone(),
endpoint,
))
}
}

pub fn set_alt_setting(&self, alt_setting: u8) -> Result<(), Error> {
debug!(
"Set interface {} alt setting to {alt_setting}",
self.interface_number
);

unsafe {
check_iokit_return(call_iokit_function!(
self.interface.raw,
SetAlternateInterface(alt_setting)
))
}
}
}

impl Drop for MacInterface {
fn drop(&mut self) {
if let Err(err) = self.interface.close() {
error!("Failed to close interface: {err}")
}
}
}
116 changes: 116 additions & 0 deletions src/platform/macos_iokit/enumeration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
use std::io::ErrorKind;

use core_foundation::{
base::{CFType, TCFType},
number::CFNumber,
string::CFString,
ConcreteCFType,
};
use io_kit_sys::{
kIOMasterPortDefault, kIORegistryIterateParents, kIORegistryIterateRecursively,
keys::kIOServicePlane, ret::kIOReturnSuccess, usb::lib::kIOUSBDeviceClassName,
IORegistryEntrySearchCFProperty, IOServiceGetMatchingServices, IOServiceMatching,
};
use log::{error, info};

use crate::{DeviceInfo, Error, Speed};

use super::iokit::{IoService, IoServiceIterator};

fn usb_service_iter() -> Result<IoServiceIterator, Error> {
unsafe {
let dictionary = IOServiceMatching(kIOUSBDeviceClassName);
if dictionary.is_null() {
return Err(Error::new(ErrorKind::Other, "IOServiceMatching failed"));
}

let mut iterator = 0;
let r = IOServiceGetMatchingServices(kIOMasterPortDefault, dictionary, &mut iterator);
if r != kIOReturnSuccess {
return Err(Error::from_raw_os_error(r));
}

Ok(IoServiceIterator::new(iterator))
}
}

pub fn list_devices() -> Result<impl Iterator<Item = DeviceInfo>, Error> {
Ok(usb_service_iter()?.filter_map(probe_device))
}

pub(crate) fn service_by_location_id(location_id: u32) -> Result<IoService, Error> {
usb_service_iter()?
.find(|dev| get_integer_property(dev, "locationID") == Some(location_id))
.ok_or(Error::new(ErrorKind::NotFound, "not found by locationID"))
}

fn probe_device(device: IoService) -> Option<DeviceInfo> {
// Can run `ioreg -p IOUSB -l` to see all properties
let location_id: u32 = get_integer_property(&device, "locationID")?;
log::info!("Probing device {location_id}");

Some(DeviceInfo {
location_id,
bus_number: 0, // TODO: does this exist on macOS?
device_address: get_integer_property(&device, "USB Address")?,
vendor_id: get_integer_property(&device, "idVendor")?,
product_id: get_integer_property(&device, "idProduct")?,
device_version: get_integer_property(&device, "bcdDevice")?,
class: get_integer_property(&device, "bDeviceClass")?,
subclass: get_integer_property(&device, "bDeviceSubClass")?,
protocol: get_integer_property(&device, "bDeviceProtocol")?,
speed: get_integer_property(&device, "Device Speed").and_then(map_speed),
manufacturer_string: get_string_property(&device, "USB Vendor Name"),
product_string: get_string_property(&device, "USB Product Name"),
serial_number: get_string_property(&device, "USB Serial Number"),
})
}

fn get_property<T: ConcreteCFType>(device: &IoService, property: &'static str) -> Option<T> {
unsafe {
let cf_property = CFString::from_static_string(property);

let raw = IORegistryEntrySearchCFProperty(
device.get(),
kIOServicePlane as *mut i8,
cf_property.as_CFTypeRef() as *const _,
std::ptr::null(),
kIORegistryIterateRecursively | kIORegistryIterateParents,
);

if raw.is_null() {
info!("Device does not have property `{property}`");
return None;
}

let res = CFType::wrap_under_create_rule(raw).downcast_into();

if res.is_none() {
error!("Failed to convert device property `{property}`");
}

res
}
}

fn get_string_property(device: &IoService, property: &'static str) -> Option<String> {
get_property::<CFString>(device, property).map(|s| s.to_string())
}

fn get_integer_property<T: TryFrom<i64>>(device: &IoService, property: &'static str) -> Option<T> {
get_property::<CFNumber>(device, property)
.and_then(|n| n.to_i64())
.and_then(|n| n.try_into().ok())
}

fn map_speed(speed: u32) -> Option<Speed> {
// https://developer.apple.com/documentation/iokit/1425357-usbdevicespeed
match speed {
0 => Some(Speed::Low),
1 => Some(Speed::Full),
2 => Some(Speed::High),
3 => Some(Speed::Super),
4 | 5 => Some(Speed::SuperPlus),
_ => None,
}
}
Loading

0 comments on commit 89ee5a9

Please sign in to comment.