We are experimenting with DriverKit on macOS while DriverKit is still in beta on iPadOS. We want to build a Driver for iPad that will allow to communicate our iPad App with USB device.
What we did:
USBDriverKit::IOUSBHostInterface
as provider. This driver is automatically matched/started by macOS when we plug our device into USB port. Next we utilised USBDriverKit::IOUSBHostPipe
to send/receive data from our device. We print data from device in logs for now.IOUserClient
and allows to open communication channel by macOs App using IOServiceOpen
API. Driver has callback to pass data to macOS Client App.Currently we want to combine 2 drivers and pass data received from USB device to our client App using callback. Unfortunately, we stuck since now we have 2 instances of driver:
virtual kern_return_t NewUserClient(uint32_t type, IOUserClient** userClient)
method is called.So we can't use second instance to do USB device communication since it has wrong provider(IOUserClient) in kern_return_t Start(IOService * provider)
but we need IOUSBHostInterface
to start:
ivars->interface = OSDynamicCast(IOUSBHostInterface, provider);
if(ivars->interface == NULL) {
ret = kIOReturnNoDevice;
goto Exit;
}
Are we doing it wrong? Maybe instead of automatic matching for IOUSBHostInterface
we should do it manually from UserClient driver or use another approach?
As we learned we have to create a new service instance in NewUserClient
method and can't return driver that was run by OS:
kern_return_t IMPL(MyDriver, NewUserClient)
{
kern_return_t ret = kIOReturnSuccess;
IOService* client = nullptr;
ret = Create(this, "UserClientProperties", &client);
if (ret != kIOReturnSuccess)
{
goto Exit;
}
*userClient = OSDynamicCast(IOUserClient, client);
if (*userClient == NULL)
{
client->release();
ret = kIOReturnError;
goto Exit;
}
Exit:
return ret;
}
BTW, maybe there is much easier way to forward data from USB device to iPadOS App?
I don't know about USBDriverKit and iPadOS, but perhaps my limited experience with PCIDriverKit on macOS can server as a useful analogue.
I have implemented a DEXT for a custom PCI device, and a user space application that communicates with the driver to do things with the device.
The DEXT consists of two classes (two pairs of .iig
and .cpp
files):
class MyDriver: public IOService
class MyDriverClient: public IOUserClient
with the following Info.plist
:
<dict>
<key>IOKitPersonalities</key>
<dict>
<key>MyDriver</key>
<dict>
<key>CFBundleIdentifierKernel</key <string>com.apple.kpi.iokit</string>
<key>IOClass</key> <string>IOUserService</string>
<key>IOPCIMatch</key> <string>0xcafebabe</string>
<key>IOPCITunnelCompatible</key> <true/>
<key>IOProviderClass</key> <string>IOPCIDevice</string>
<key>IOUserClass</key> <string>MyDriver</string>
<key>IOUserServerName</key> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>MyDriverClientProperties</key>
<dict>
<key>IOClass</key>
<string>IOUserUserClient</string>
<key>IOUserClass</key>
<string>MyDriverClient</string>
</dict>
</dict>
</dict>
</dict>
When the system discovers a PCI device with matching vendor/device ID as per the IOPCIMatch
property value, it:
IOUserService
class (as mandated by the IOClass
property) in the kernel to facilitate the communication with the DEXT residing in userspaceIOPCIDevice
as per the IOProviderClass
property).MyDriver
as per the IOUserClass
property), giving it a pointer to the provider
created above.As part of its Start
implementation, MyDriver
invokes RegisterService()
to make itself appear in the I/O Registry (e.g. ioreg -l
).
When a userspace application (with appropriate entitlements) later finds the registered MyDriver
service (e.g. using IOServiceGetMatchingService()
), and opens it ( IOServiceOpen()
), on the DEXT side that ends up invoking MyDriver::NewUserClient()
(docs), which looks like this:
kern_return_t IMPL(MyDriver, NewUserClient)
{
kern_return_t ret = kIOReturnSuccess;
IOService* client = nullptr;
ret = Create(this, "MyDriverClientProperties", &client);
// ... Error handling
*userClient = OSDynamicCast(IOUserClient, client);
// ... Error handling
// If you need to remember/keep track of the client, you could stash it in the `ivars`, already casted to its true type:
ivars->client = OSDynamicCast(MyDriverClient, client);
// ...
}
What exactly Create
will do is determined by the provided property dictionary (under the arbitrarily named MyDriverClientProperties
key in the Info.plist
). In this case, an IOUserUserClient
(the double-User
is not a typo) object is created in the kernel (IOClass
property), and a MyDriverClient
object is created in userspace (IOUserClass
property).
Your description in the question seems to suggest otherwise, but at least in my case, the client object is fed the MyDriver
instance as its provider
, and I am able to obtain a reference to it:
kern_return_t IMPL(MyDriverClient, Start)
{
kern_return_t ret;
ret = Start(provider, SUPERDISPATCH);
// ... Error handling
ivars->driver = OSDynamicCast(MyDriver, provider);
// ... Error handling
// ...
}
Later, when for example MyDriverClient::ExternalMethod
(docs) is invoked, I can use ivars->driver
to invoke functionalities of the MyDriver
class and interact with the underlying device.