Search code examples
macosipadosdriverkit

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?


Solution

  • 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:

    1. Instantiates an IOUserService class (as mandated by the IOClass property) in the kernel to facilitate the communication with the DEXT residing in userspace
    2. Spawns the userspace process for the DEXT.
    3. Instantiates the required provider class in that process (here: an IOPCIDevice as per the IOProviderClass property).
    4. Instantiates the main class (here: 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.