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:
- Configured and implemented a driver that uses
USBDriverKit::IOUSBHostInterface
as provider. This driver is automatically matched/started by macOS when we plug our device into USB port. Next we utilisedUSBDriverKit::IOUSBHostPipe
to send/receive data from our device. We print data from device in logs for now. - Studied Communicating Between a DriverKit Extension and a Client App
- Configured and implemented a driver that based on
IOUserClient
and allows to open communication channel by macOs App usingIOServiceOpen
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:
- First instance is automatically run by macOS when device is plugged
- Second instance is created when we are connecting from Client App and
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?
Instead of making a second instance of your IOUSBHostInterface
-provider class in NewUserClient
, make a new class that subclasses IOUserClient
. The sample is made using the same class for the sake of being able to run without matching to hardware, but this isn't the way you should write your sample. For clarity through the rest of the post, I'll refer to the IOUSBHostInterface
-provider class as your "USB class" and the IOUserClient
subclass as the "User Client class."
Your NewUserClient
implementation in your USB class should look something like this:
kern_return_t USBClass::NewUserClient_Impl(uint32_t type, IOUserClient** userClient)
{
kern_return_t ret = kIOReturnSuccess;
IOService* client = nullptr;
Log("NewUserClient()");
ret = Create(this, "UserClientProperties", &client);
if (ret != kIOReturnSuccess)
{
Log("NewUserClient() - Failed to create UserClientProperties with error: 0x%08x.", ret);
goto Exit;
}
ivars->userClient = OSDynamicCast(MyUserClientClass, client);
if (ivars->userClient == nullptr)
{
Log("NewUserClient() - Failed to cast new client.");
client->release();
ret = kIOReturnError;
goto Exit;
}
*userClient = ivars->userClient;
Log("NewUserClient() - Finished.");
Exit:
return ret;
}
In order to support this, make sure that you update the UserClientProperties
in your IOKitPersonalities
:
<key>IOUserClass</key>
<string>USBClass</string>
<!--...-->
<key>UserClientProperties</key>
<dict>
<key>IOClass</key>
<string>IOUserUserClient</string>
<key>IOUserClass</key>
<string>MyUserClientClass</string>
</dict>
Then, whenever your USB device receives data, you can call into your userclient class with something like:
if (ivars->userClient)
{
if (ivars->prevState != currentState)
{
ivars->userClient->SendState(timestamp, currentState);
ivars->prevState = currentState;
}
}
SendState
will be a function of your own making, declared in your userclient class's iig
with something like:
kern_return_t SendState(uint64_t timestamp, uint64_t state) LOCALONLY;
Then you can send off the data as demonstrated in the previous sample. That should get you pointed in the right direction. Best of luck.