How to communicate between USBDriverKit driver and Client app?

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:

  1. 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 utilised USBDriverKit::IOUSBHostPipe to send/receive data from our device. We print data from device in logs for now.
  2. Studied Communicating Between a DriverKit Extension and a Client App
  3. Configured and implemented a driver that based on 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:

  1. First instance is automatically run by macOS when device is plugged
  2. 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?

Answered by in 721855022

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.

Accepted Answer

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.

Hello guys,

We also intend to develop a similar driver for one of our use cases. Would it be possible to talk sometime? I'd really appreciate it. My linkedIn profile is - https://www.linkedin.com/in/mohd-irfan-85906b78/ Please feel free to send a message there, or if you could provide me with your contact information, I can do so.

How to communicate between USBDriverKit driver and Client app?
 
 
Q