-
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 & 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 the same method appears on multiple version of the same interface but is only documented for one of them. 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 in C or from 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 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 object are referred to by interfaces, which are basically pointers to a instance memory. At offset 0, the instance memory contains a pointer to a table with all the interface methods. This 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, iterator object must be created and configured to enumerate all interfaces and then query each interface's number. It's completely over-engineered and requires many lines of codes.
The IOKit USB functions support time-outs for bulk endpoints (ReadPipeTO() and WritePipeTO). But it does not for interrupt endpoints.
For interrupt endpoints, this library instead schedules a timer in a separate thread and aborts the interrupt transfer if the timeout expires. If the transfer completes first, the timer is cancelled (see net.codecrete.usb.macos.TransferTimeout).