-
Notifications
You must be signed in to change notification settings - Fork 84
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
feat: Native macOS backend #131
base: main
Are you sure you want to change the base?
Conversation
Hi, thank you for your work! I will hopefully have some time to review it in the coming week. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having written something similar myself, this design is really strange. Rust's standard library has functionality which could make the implementation significantly simpler.
It looks like this attempts to decouple IOHIDDevice
s from the IOHIDManager
which created it, in order to allow "opening by path" (which is nonsensical on macOS). It would be better to attach that to the HidApiBackend
.
While this may be more of a comment on the state of hidapi
's interface, the device matching rules available are severely lacking (only single VID+PID), which is a problem in modern versions of macOS, which put free-for-all access behind an "input monitoring" permission. There's also no way to monitor device insertion/removal events, so you're forced to continually prod HidApiBackend::enumerate()
.
// | ||
// It is recommended that changing the configuration a run loop should be done from the thread that owns the run loop whenever possible. | ||
// In the use here, we only use the wrapper to wake up the run loop, which is safe. | ||
unsafe impl Send for WrappedCFRunLoop {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should no longer be needed as of servo/core-foundation-rs#610
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correction: still needed, the PR made it into v0.8.5, but it was yanked due to servo/core-foundation-rs#619.
fn enumerate(vendor_id: Option<u16>, product_id: Option<u16>) -> HidResult<Vec<DeviceInfo>> { | ||
let manager = IOHIDManager::create(); | ||
|
||
if vendor_id.is_some() || product_id.is_some() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Allow matching by Usage/Usage Page, and multiple VIDs/PIDs.
Without any matching rules, macOS demands "Input Monitoring" permission, which is quite bad.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would require changes to the other backends as well, or additional Mac-specific functions.
|
||
// Condition variable linked to input_reports | ||
condition: std::sync::Condvar, | ||
input_reports: Mutex<VecDeque<Vec<u8>>>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use an mpsc::channel
rather than a VecDeque
. That way you get queue size limits for free, you don't need to wrap everything in a Mutex
.
You also get helpers like recv_timeout
...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right now, the oldest received input report is dropped when the limit is reached. I don't think this is possible with the mpsc::sync_channel
, the size limit there could only be used stop sending new input reports, but it doesn't seem possible to remove elements from the channel.
Using a channel would be nice, but it would change the behaviour compared to the C
hidapi library.
{ | ||
let device = shared_state.device.lock().unwrap(); | ||
|
||
// unregister the input report callback |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't necessary - IOHIDDevice
automatically unregisters these on close: https://github.com/apple-oss-distributions/IOKitUser/blob/b0b3f822b7507c265aa8a1e37c3100c03ca82039/hid.subproj/IOHIDDevice.c#L483-L486
&& (!shared_state.disconnected.load(Ordering::Relaxed)) | ||
{ | ||
// TODO: Verify timeout value | ||
let code = unsafe { CFRunLoopRunInMode(run_loop_mode.as_concrete_TypeRef(), 1000.0, 0) }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm struggling to understand the design here, but why not just CFRunLoopRun()
? It does basically the same thing as your loop: https://github.com/apple-oss-distributions/CF/blob/dc54c6bb1c1e5e0b9486c1d26dd5bef110b20bf3/CFRunLoop.c#L2676-L2682
On disconnect/Drop
, you can issue a CFRunLoopStop()
from the main thread – HidDevice::drop
already has a reference to the device's CFRunLoop
.
I don't think it matters in this loop whether the device was closed by the application (terminating the run loop) or there was an explicit disconnect (terminating the run loop); a disconnected device is a disconnected device. :)
mpsc::channel
also saves you here too, because Drop
ing the Sender
will unblock any pending Receiver::recv
calls on another thread, and make future calls fail.
If you've only associated a single IOHIDDevice
with the CFRunLoop
, the run loop will stop on disconnect anyway. hid_removal_callback
appears to only set a flag that it was disconnected, but this could also be detected by setting your own (not-mutex-guarded) flag on close()
.
|
||
pub fn copy_devices(&self) -> Vec<IOHIDDevice> { | ||
let set: CFSet<IOHIDDeviceRef> = unsafe { | ||
let set = IOHIDManagerCopyDevices(self.as_concrete_TypeRef()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't documented, but IOHIDManagerCopyDevices
can return nullptr
if there are no matching devices.
.get_i32_property("MaxInputReportSize") | ||
.unwrap_or_default(); | ||
|
||
let run_loop_mode = format!("HIDAPI_{:p}", device.as_concrete_TypeRef()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't right, but the IOHIDDeviceUnscheduleFromRunLoop
runLoopMode
parameter is unused anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you explain what you mean by not right? Using custom run modes seems to be possible according to the documentation at https://developer.apple.com/documentation/corefoundation/cfrunloop?language=objc.
Or do you mean that a custom run mode shouldn't be used?
Thanks for looking at this! It's good to get some feedback from somebody who knows these APIs.
As mentioned in the description, this is more or less a direct port of the hidapi C library, with only minor changes to work around the problem mentioned in #127. I'm not sure what the requirements regarding compatibilty with the existing implementation are. If this is only an optional backend, and the hidapi library is kept as an alternative, then it should probably be compatible, which limits the amount of changes that can be done. On the other hand, if compatibility is not a concern, then some of the improvements like using a @ruabmbua What are your thoughts regarding compatibility with the C library? |
Thanks for the review and the work done! I already started with it myself, but could not motivate myself to start learning macos API`s ^^. Regarding compatibility to the current public API of the library: I would keep it as is for now (with non breaking changes allowed). This means I agree that the new optional pure rust macos backend should try to mimic the old one as close as it can. I might revisit the public API in the future (v3.0.0), and do mostly some fixes that make it more idomatic rust. |
The main functionality I would want out of async in this library is being able to have async read and write from/to a device. Some might not be possible to actually do async and some might make more sense to use something else (like updating the list of relevant devices). But I don't see a way around async read/write without helf from the library.
Unfortunately you cannot always replicate async. The device I'm most interested in driving is interactive, so while I can (and do) have a thread that takes care of talking to the device, it needs to choose whether to read or write. I want to react to the user's interaction quickly, so it's waiting on a read. But I have to be able to write to the device based on what might be happening on the computer. Right now the only way this works is by providing a short-but-not-too-short timeout to the read. If the timeout is too long, the interaction becomes too sluggish. If it's too short, we're looping and wasting cycles and power on reading nothing. There's two ways of making this better that I can see: async or cancellable timeouts, and both would need some support from the library, and async might be the easier one overall. The library does currently implement Which is why ISTM that at least having async read and write would help quite a bit and it would allow users of this library not to have to implement workarounds for each platform. And the native Linux and Windows as well as a libusb implementation should all be able to do some form of asynchornous IO (IIRC what the C hidapi does is force the libusb event stuff to become blocking). I've been playing around with what an async interface would look like but I haven't quite figured out what it should look like. A different |
This is a port of the hidapi code for macOS to pure Rust, with some small modifications to make it more thread safe.
I've tested it with probe-rs, and it seems to work fine so far.
The error handling could be improved, but that might be better to do in coordination with the other native backends.
Remaining tasks