-
Notifications
You must be signed in to change notification settings - Fork 40
Redesign endpoint and transfer API #117
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
Conversation
Great job, and thanks for your efforts and hard work on this Kevin. |
Any chance that you have some demo code to play with for the new API, Kevin? I was messing with interrupt in and out and queues today so this update is quite timely. I'll be back onto it tomorrow. Thanks. |
54a5764
to
25b99e5
Compare
Great stuff! I am happy rebasing :) I will have a look through this PR tomorrow and then try and make a new webusb branch based on it :) P.S. I am using the webusb branch successfully for developing https://inspect.probe.rs/ since a while :) |
Great stuff. This looks like the right way forward to me. |
@kevinmehall I run into the problem where WebUSB needs to be able to report transfer errors. The |
The interface between the platform code and the common code is: I think the implementation between |
I had a go at porting Packetry to the new API. The biggest problem I encountered is that This is a lot less useful to the caller, because with a That particular case is quite important, because a Similarly, |
Good point. Not sure why I changed that, so I'll change it back to TransferError. |
Thanks! I have it working now. I've put those changes in greatscottgadgets/packetry#222 with some CI coverage so that I can keep an eye on things as this branch evolves. One thing I'm still a bit unsure about from an ergonomics point of view is getting rid of the standalone buffer types. Previously, it was possible to take an inbound That type can't just be a This would require either making As it stands it makes things quite weird: if completion.status().is_ok() {
println!("Received {} bytes", completion.len()); // Huh? What's the length of a completion mean?
println!("First byte was {}", completion[0]"); // Why is completion indexable? This looks weird.
// ...but if I name `completion` as `data` instead, then that obscures the fact it also contains a
/// result that still needs to be checked.
// Also now all my downstream processing has to deal with this `Completion<Bulk, In>`
// type, until I'm either done with the data or willing to incur the cost of copying it.
channel.send(completion);
} My suggestion would be something like: if completion.status().is_ok() {
let data = completion.data(); // data() returns &Buffer
println!("Received {} bytes", data.len());
println!("First byte was {}", data[0]");
channel.send(completion.take_data()); // take_data() returns Buffer and consumes the Completion
} |
A much more minor issue I encountered is that |
Thanks for testing it out!
The endpoint type is in the type signature because I want to be able to add different methods for isochronous. Though one option would be to use one set of
Agreed that
To be fully zero-allocation, it would want to keep the non-buffer parts of the transfer, and have a way to re-combine the buffer into a One possibility is exposing only On that subject, I noticed that you're not re-using transfers/buffers in Packetry, which is probably fine now with the standard allocator, and it would be annoying to send consumed buffers back to the thread owning the Did you consider combining those threads? Instead of blocking on the channel receive, block on the select between |
I think having the single parameterised type for
Yes, that would make the intent clearer, but it doesn't really address the fundamental issue; see below.
It's less of an issue for Whereas But if the caller wants to treat the As it stands, there's no way to distinguish those two things in the type system! That's why I'd like to be able to discard the
If This could actually be achieved by just renaming the new struct Completion<EpType, Dir> {
status: Result<(), TransferError>,
data: Buffer<EpType, Dir>,
} Because having that discardable wrapper was important!
There hasn't been any attempt to make Packetry zero-copy ready yet. We could arrange to send those consumed buffers back for reuse, and obviously if nusb were doing that zero-copy mmap allocation then it'd be a higher priority to do so.
The issue there is that we can't guarantee that the decoder will always run fast enough, i.e. not block for long enough that the transfer queue is emptied. Having a dedicated thread that just keeps up with the USB transfers, queuing up all the data for later decoding, means that we ensure we always drain the buffer on the device as fast it's possible for the host to achieve. That's important because both Cynthion and iCE40-usbtrace are devices that offload data to the host at the same USB speed they capture at. So they can only capture a saturated bus for a limited period that's constrained by their internal buffer size. |
I've just added a second commit to that Packetry branch which sends each It doesn't really add much complication, and actually works very neatly, as it means the number of allocated transfers grows to fit the worst-case backlog length of the decoder thread, whilst keeping the same queue depth of transfers submitted to the OS. |
A related buffer management issue encountered while porting a (yet-to-be-published) project to this is that it's annoying that allocating a I'm wondering if exposing the transfer object as
One counterpoint that became apparent when implementing a There are plenty of cases where you don't care about partial data if there is an error though, and I get the point that it'd be too easy to not check the status at all. |
Yeah, interoperability with But in the case where you have the choice of what storage to use, allocating a zero-copy buffer from nusb makes sense. So perhaps what we need is separate
I don't personally have strong feelings either way on this. One possible argument for keeping it strictly zero-alloc would be the possibility of extending/porting the nusb API to support bare-metal embedded systems with USB host support, where people may want a completely no-alloc solution. But that's way outside the current scope of the project. Even in an OS environment though, there might perhaps be advantages to avoiding allocation in situations where real-time performance is important - e.g. SDR, USB audio, etc.
I don't think that ordering between the data and the error is an argument for bundling them into the same object. Even if I want to respect that ordering I may still want to separate the two. For instance, right now in my ported version of Packetry I have a |
Went back to something more like v0.1 Queue with The implementation relies on internal allocations for the transfer state, but it caches the last completed transfer so as to not need to allocate when re-submitting after a transfer completes. |
Seems like a good compromise. I've updated greatscottgadgets/packetry#222 to the new version and everything seems to be working OK. |
OVERLAPPED.Internal contains NTSTATUS instead of WIN32_ERROR, and while it seems very unlikely to change, is documented as subject to change.
This is a redesign of the main APIs for transfers in nusb v0.2:
bulk_in
/bulk_out
/interrupt_in
/interrupt_out
APIs returningTransferFuture
have been removed. The cancel-on-drop semantics have proven to be unhelpful, and I increasingly think that a cancel-safe API is a must here. USB transfer semantics are fundamentally incompatible with the synchronous drop imposed by Rust futures.interface.endpoint::<EpType, Dir>(addr)
. This checks that the endpoint exists and the type and direction match the descriptor, moving those errors up-front instead of when performing a transfer. It also requires that the endpoint is not already claimed.Interface
, along withu8
addresses for one or more endpoints. Instead, you'd hold one or moreEndpoint
s to perform transfers on.Endpoint
s of an interface before changing alternate setting.Endpoint
API is similar to the v0.1Queue
API.However instead of hiding the transfers and handling only buffers, this introducesRequest
andCompletion
types that wrap both the buffer and platform-specific transfer data structures (e.g. URB on Linux).You obtain aRequest<EpType, Dir>
fromendpoint.allocate(capacity)
, pass it toendpoint.submit(request)
, and de-queue a completed transfer withendpoint.next_complete()
returningCompletion<EpType, Dir>
.Request<Bulk | Interrupt, Out>
has Vec-like methods for filling the buffer, andCompletion<Bulk | Interrupt, In>
derefs to&[u8]
for accessing the received data. This will enable alternative allocators for zero-copy and allow the API to adapt to the additional requirements for isochronous transfers.Currently the buffer capacity is fixed once allocated, and the buffer cannot be converted from or into aVec
. Those would be possible to add, but would complicate the story around zero-copy buffers.crate::transfer::internal
completely, and implement the internalInterface
andEndpoint
methods on top ofJsFuture
, hopefully without any need forunsafe
.Future
s, because a queue-based API would be very inconvenient for most control transfer use cases, and they are expected to complete quickly so cancellation is less of an issue.MaybeFuture
trait. This also avoids the locking pitfalls of Linux's blocking control transfer ioctl by implementing all control transfers with async internals.timeout
parameter. Unfortunately, on Windows, the timeout is set via an endpoint "policy" rather than per-request, and like with RAW_IO, it is unclear whether SetPipePolicy is allowed with transfers pending. For now, the passed timeout is ignored and WinUSB has a default control transfer timeout of 5s.Endpoint
/Request
/Completion
API is a little complicated for simple use cases that might have used libusb's synchronous API, or nusb v0.1's TransferFuture APIs to send and receive small transfers. I think a set of convenience APIs implemented on top ofEndpoint
's public APIs could fill this need.Endpoint
wrappers that implementRead
+ReadBuf
+AsyncRead
+AsyncReadBuf
andWrite
+AsyncWrite
traits. It would accept parameters for transfer size, and number of concurrent transfers to maintain, giving control over buffering, but also abstracting away the entire concept of "transfers" for the code written on top. For writing to an OUT endpoint, aWrite
implementation along with methods for forcing a short or zero-length packet seems like it could serve a lot of these needs. For IN endpoints,Read
is a little less direct, but I'm experimenting with a mode where short packets are handled specially.Feedback welcome!