-
Notifications
You must be signed in to change notification settings - Fork 11
MacOS Implementation
MacOS has good support for USB devices and appropriate APIs. Using the APIs from with the Foreign Function And Memory API unfortunately is somewhat more involved for several reasons:
-
Many APIs follow the design of the Component Object Model (COM). It probably seemed like a good idea at the time. But COM didn't prevail as it is too complex to use. It's even too complex for Apple's documentation as the same method appears in multiple versions of the same interface but only one of them is documented. Each one has a documentation page. But it's very difficult to find the one with the description.
-
Some APIs use Core Foundation data types. They are quite easy to use in Objective-C or Swift but far more complex from C or Java.
Also see macOS reference code.
The IO Registry keeps track of devices and services on macOS. It can also send notifications about changes to registered receivers. On initial registration, the IO Registry notification function also provides a list of already connected devices in the same way. This is a nice design as enumeration and monitoring work the same and as it is not possible to miss any events in the time between the initial enumeration and the start of the monitoring.
The main challenge with IO registry functions is that they are very generic as they work with all kinds of devices and services. So it can be difficult to figure out how USB devices will be presented. The documentation is not specific either. But sufficient sample codes can be found.
Notifications are sent to a Mach port. The port is connected to the run loop of the background process. A run loop is a main loop waiting for the next event and then handling the event. The callback handler for each type of notification can be specified when the notification is set up. Once everything is correctly configured, the run loop is run.
The callback makes use of function upcalls (of the Foreign Function and Memory API).
For the communication with a USB device, the IOKit USB functions are used. Converting an IO registry entry to an IOKit interface is quite obscure. It's the gateway into the COM world.
The COM objects are referred to by interfaces, which are basically pointers to an object instance. At offset 0, the instance memory contains a pointer to a table with all the interface methods. It is basically the layout of C++ objects, which also have a pointer to the so called vtable at the start of the object.
The confusing part is that the C structs like IOUSBDeviceInterface
define the vtable layout and not the object layout. This was probably chosen because the object layout is private, while the vtable layout must be public. The result is that the typical data type is not IOUSBDeviceInterface *
(a pointer to IOUSBDeviceInterface
) but IOUSBDeviceInterface * *
(a pointer to a pointer to a struct). So there are a lot of indirections involved.
jextract is capable of generating the Java code for structs like IOUSBDeviceInterface
consisting of function pointers only. That code includes methods to call these functions. However, it does not include code for dereferencing the instance pointer to get at the vtable. Thus, a wrapper is needed for each function (see net.codecrete.usb.macos.IoKitUsb).
A minor annoyance of IOKit USB is that it neither uses endpoint numbers nor endpoint addresses to refer to endpoints. Instead, it assigns each endpoint an index. This index must first be queried. In the code, this index is called pipe index.
A further annoyance is that – given an interface number – there is no function to open a USB interface. Instead, an iterator object must be created and configured to enumerate all interfaces and then query each interface's number. It's over-engineered and requires unnecessarily many lines of codes.
Asynchronous transfers are straight-forward. In particular, the completion handler background thread is simple. It just executes a run loop and maintains a mapping between transfer request IDs and the actual Transfer
instance. Additionally, it creates an upcall stub suitable for the callback
argument in the asynchronous USB functions.
Each USB device will be registered as an event source for the run loop. This can happen at any time without interacting with the background thread. Submitting asynchronous requests doesn't require any interaction with the background thread either.
For bulk endpoints, macOS handles timeouts. For other endpoint types, this library handles the timeout by using Object.wait()
and Object.notify()
on the Transfer
instance.