-
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
Async #48
Async #48
Conversation
Could you show an example of how you might use this API? It doesn't provide any way of waiting for transfers to complete or waking up an async task when the result is ready. When does the user know to call What happens when you drop the You can't assume that the callback will run on the same thread. For example, all the libusb sync APIs call into the libusb event loop while waiting, so your callback could be called by any thread the that is waiting for a synchronous transfer. What happens when you're reading from the VecDeque while another thread modifies it in the callback? |
Right now I'm testing it on a project I'm working on that includes a lot of code that's specific to the device. I'll try to separate out the USB-specific stuff into an example. Good point in the second paragraph. I think I just need to call I think the libusb docs say that it will run on the same thread. Isn't that what this says (from http://libusb.sourceforge.net/api-1.0/group__libusb__asyncio.html)? """ If this doesn't mean what I think it means, then we could always protect that Edit: |
The callback is still called asynchronously with LIBUSB_ERROR_CANCELLED sometime after
Go read the implementation of the libusb synchronous API. It calls
Be careful with race conditions here. If the transfer completes and callback runs on another thread between checking the VecDeque and sleeping, you can lose the event and never wake up. That's why Fix those things, and add an abstraction for waiting for multiple different transfers, and you're basically reinventing the AsyncGroup API that I implemented a while ago. That was reverted because of the "leakpocalypse" issue -- if you I think |
That makes sense. It sounds like the risk comes mostly from using the sync and async API at the same time. I wonder if there's some way to use to type system to force you to use one or the other at a time for a given device handle. I'll look at your |
In theory, you could make a new version of |
I was also thinking maybe you could have some kind of struct that takes ownership of |
pub struct AsyncTransfer { | ||
pub ptr: *mut libusb_transfer, | ||
pub buff: Vec<u8>, | ||
pub recv: Pin<Box<VecDeque<TransferStatus>>>, |
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 not sure AsyncTransfer
or specifically recv
is prevented from moving here. Neither the VecDeque or TransferStatus
is !Unpin
, so neither is AsyncTransfer
. If this is true, it means that when an AsyncTransfer
is moved, we get a dangling reference inside of ptr
.
BUT I'm very unfamiliar with unsafe rust, and self-referential structs. Am I correct in this? Just going off of the std::pin
documentation.
|
||
fn drop(&mut self) { | ||
unsafe{ | ||
libusb1_sys::libusb_cancel_transfer(self.ptr); |
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 function is asynchronous, and doesn't block until the transfer is cancelled. From here:
Asynchronously cancel a previously submitted transfer. This function returns immediately, but this does not indicate cancellation is complete. Your callback function will be invoked at some later time with a transfer status of LIBUSB_TRANSFER_CANCELLED.
Hence, calling free on the transfer will still be invalid, in most cases
I've written an alternative implementation in #64. I think that PR resolves the soundness issues in this one. Let me know what you think! |
Closed in favor of #143 |
This is an attempt at making the async API work. It's not complete yet and only includes bulk receive right now, but if nobody sees anything wrong with the basic idea, it'll be pretty straightforward to fill in the other types of transfers. The whole thing revolves around a struct called
AsyncTransfer
. It contains a raw pointer to alibusb_transfer
from libusb, a buffer for receiving data, and a buffer for completed transfers.TODO: I made all the struct members public for debugging, but I just realized these should probably all be private. We could create a
pop_front
function on this struct that takes&mut self
and just callspop_front
onrecv
. That's the only function the owning scope should need after the struct is created.For this to work, all three of these need to have the same lifetime and making them all owned by the same struct accomplishes this. We also use Pin for the VecDeque that collects the results because a pointer to this is passed to the libusb transfer struct as the user data, so it needs to maintain the same location in memory for as long as it lives.
The
buff
vector is the vector where libusb puts the data.ptr
is the transfer struct provided by libusb.recv
is where we accumulate the results.TransferStatus
looks like this:The callback function is a private function inside
impl AsynTransfer
and looks like this:Basically, it puts the result into a
VecDeque
and the scope that owns the struct can process these whenever it wants. This is not threadsafe, but doesn't pretend to be. If you try to make this structSend
orSync
, the compiler gives an error. The callback function runs in the same thread that created the struct when you calllibusb1_sys::libusb_handle_events
.