[Haskell-cafe] Need comments on a libusb asynchronous select/poll loop

Bas van Dijk v.dijk.bas at gmail.com
Tue Apr 19 22:36:50 CEST 2011


On 19 April 2011 15:06, John Obbele <john.obbele at gmail.com> wrote:
> I'm trying to implement an asynchronous interface to libusb, re-using
> the raw bindings-usb (by Maurício C. Antunes) and partially copying what
> can be found in the (great but synchronous-only) usb package (from Bas
> van Dijk).

Great! I have wished for an asynchronous implementation since version
0.1 of usb but didn't really had a need for it nor the time to
implement it. However recently at work I have begun using my usb
library in an application which should run on a resource constraint
device. So any CPU cycles saved by using an asynchronous
implementation is a win. Since we both have a need for it maybe we can
work together?

> The issues are:
>
> 1) I don't know how to expose the asynchronous API to Haskell. (How would
> you 'functionalize' it ?)

I also started with trying to come up with an asynchronous _interface_
but realized that's probably not what users need. An asynchronous
interface is difficult to program against. However an asynchronous
_implementation_ is desirable over a synchronous one because of
efficiency.

Note that the synchronous interface of the underlying libusb C library
is also implemented using the lower level and much more complicated
asynchronous interface. See the libusb_control_transfer function for
example: http://bit.ly/fboG9C However as you would expect the
implementation uses a busy loop to check when the transfer completes.

What we actually want is a synchronous interface with the same API as
we have now but with an implementation that uses the asynchronous
interface from libusb and integrate that with the new GHC event
manager.

Let me give a sketch of how I think we should design an asynchronous
implementation. For simplicity lets take the usb control function:

http://hackage.haskell.org/packages/archive/usb/0.8/doc/html/System-USB-IO-Synchronous.html#v:control

and give that an asynchronous implementation. Note that this is still
a sketch some parts need to be filled in:

-- First of all, note that the synchronous API remains the same:
control ∷ DeviceHandle → ControlAction (Timeout → IO ())
control devHndl reqType reqRecipient request value index timeout =

  -- Asynchronous IO consists of 5 steps. See:
  -- http://libusb.sourceforge.net/api-1.0/group__asyncio.html#asynctrf
  -- The following takes care of the first and last step:
  -- allocating and deallocating the transfer structure:
  -- (See allocTransfer below)
  bracket allocTransfer c'libusb_free_transfer $ \transPtr →

    -- Step 2 is filling the just allocated transfer structure with
    -- the right information. The most important piece of information
    -- is the buffer of data which you want to send to the device.
    -- The following allocates this buffer and since we're doing a
    -- control transfer we fill it with a control setup structure:
    alloca $ \bufferPtr →
      poke bufferPtr $ C'libusb_control_setup
                         (marshalRequestType reqType reqRecipient)
                         request
                         value
                         index
                         0

      -- The next piece of information in the transfer structure is
      -- the callback function. This function is called when the
      -- transfer terminates. This happens when either the transfer
      -- completes, timeouts, errors or when it is cancelled.
      --
      -- Because we want to provide a synchronous API the idea is to
      -- create a lock (a 'MVar ()'), submit the transfer, acquire the
      -- lock (which will block) and let the callback function release
      -- the lock (which causes the thread to continue when the
      -- transfer terminates). This way we don't need a busy loop and
      -- we save CPU cycles for other Haskell threads.

      lock ← newEmptyMVar
      let acquire lck = takeMVar lck
          release lck = putMVar lck ()

      let callback _ = release lock

      -- The 'callback' needs to be called from libusb so we have to
      -- turn it into a FunPtr: (See the FFI wrapper mkCallback below)
      bracket (mkCallback callback) freeHaskellFunPtr $ \cbPtr → do

        -- Now it's time to actually fill the transfer structure with
        -- the right information:
        c'libusb_fill_control_transfer transPtr
                                       (getDevHndlPtr devHndl)
                                       bufferPtr
                                       cbPtr
                                       nullPtr -- unused user data
                                       (fromIntegral timeout)

        -- Step 3 is the most important step. Submitting the transfer:
        handleUSBException $ c'libusb_submit_transfer transPtr

        -- TODO: Now we need to do the complicated stuff described in:
        -- http://libusb.sourceforge.net/api-1.0/group__poll.html
        --
        -- First we need the function:
        -- getPollFds ∷ Ctx → IO [C'libusb_pollfd]
        --
        -- A C'libusb_pollfd:
        -- http://libusb.sourceforge.net/api-1.0/structlibusb__pollfd.html
        -- is a structure containing a file descriptor which should be
        -- polled by the GHC event manager and an abstract integer
        -- describing the event flags to be polled.
        --
        -- The idea is to call getPollFds and register the returned
        -- file descriptors and associated events with the GHC event
        -- manager using registerFd:
        -- http://hackage.haskell.org/packages/archive/base/4.3.1.0/doc/html/System-Event.html#v:registerFd
        --
        -- As the callback we use libusb_handle_events_timeout.
        --
        -- But here we run into a problem: We need to turn our
        -- concrete event integer into a value of the _abstract_ type
        -- Event. But the only way to create Events is by evtRead or
        -- evtWrite!
        --
        -- I would really like a solution for this.
        -- Bryan, Johan any ideas?

        -- But suppose we can somehow register the events...

        -- Now we just wait for the lock. When we get interrupted by
        -- an asynchronous exception we just cancel the running
        -- transfer (TODO: what if the transfer just terminated???):

        acquire lock `onException` libusb_cancel_transfer transPtr

        -- TODO: Finally we have to see why the transfer terminated
        -- and turn that into a correct Haskell response:
        --
        -- * transfer completed → return ()
        --
        -- * transfer timed out or failed due to error →
        --     throwIO (converErrorCodeToException errorCode)
        --
        -- * transfer cancelled → the only way a transfer can be
        --      cancelled is by throwing an asynchronous exception to
        --      the thread executing this function. When that happens
        --      the transfer is cancelled and the exception is
        --      re-thrown. So this case should never be reached!

    where
      allocTransfer = do
        transPtr ← c'libusb_alloc_transfer nrOfIsoPackets
        when (transPtr ≡ nullPtr) (throwIO NoMemException)
        return transPtr

      -- We are doing a control transfer so we don't need space for
      -- isochronous packet descriptors:
      nrOfIsoPackets = 0

foreign import ccall "wrapper" mkCallback ∷ (Ptr C'libusb_transfer → IO ())
                                          → IO C'libusb_transfer_cb_fn


> 2) I am messing with lots of FFI calls around epoll and libusb and would
> appreciate if someone could sanitize my approach.

I haven't had time to look at your implementation. I will try to do
that tomorrow if I find some time.

Note that I just moved the usb source repository to github:

https://github.com/basvandijk/usb

(I you want to make patches against it and have trouble with the
UnicodeSyntax don't worry I accept ASCII-only patches too ;-) )

Regards,

Bas



More information about the Haskell-Cafe mailing list