Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement macOS backend #12

Merged
merged 7 commits into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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