DriverKit: limitations of AsyncCallback in IOConnectCallAsyncStructMethod?

We implemented communication between Swift App and DriverKit driver using IOConnectCallAsyncStructMethod. So in Swift App we have following code to setup AsyncDataCallback:

func AsyncDataCallback(refcon: UnsafeMutableRawPointer?, result: IOReturn, args: UnsafeMutablePointer<UnsafeMutableRawPointer?>?, numArgs: UInt32) -> Void {
// handle args
}

var asyncRef: [io_user_reference_t] = .init(repeating: 0, count: 8)

asyncRef[kIOAsyncCalloutFuncIndex] = unsafeBitCast(AsyncDataCallback as IOAsyncCallback, to: UInt64.self)
asyncRef[kIOAsyncCalloutRefconIndex] = unsafeBitCast(self, to: UInt64.self)

......

let notificationPort = IONotificationPortCreate(kIOMasterPortDefault)
let machNotificationPort = IONotificationPortGetMachPort(notificationPort)
let runLoopSource = IONotificationPortGetRunLoopSource(notificationPort)!

CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource.takeUnretainedValue(), CFRunLoopMode.defaultMode)

var input = "someinput".map { UInt8($0.asciiValue!) }
let inputSize = input.count

IOConnectCallAsyncStructMethod(
            connection,
            123,
            machNotificationPort,
            &asyncRef,
            UInt32(kIOAsyncCalloutCount),
            &input,
            inputSize,
            nil,
            nil
)

In the driver's ExternalMethod we save callback in ivars:

ivars->callbackAction = arguments->completion;
ivars->callbackAction->retain();

Next we start communication with our device in a separate thread and execute callback when we get data from device:

SendDataAsync(void *data, uint32_t dataLength) {
const int asyncDataCount = 16;

if (ivars->callbackAction != nullptr) {
        Log("SendDataAsync() - got %u bytes", dataLength);

        uint64_t asyncData[asyncDataCount] = { };
        asyncData[0] = dataLength;
        memcpy(asyncData + 1, data, dataLength);

        AsyncCompletion(ivars->callbackAction, kIOReturnSuccess, asyncData, asyncDataCount);
    }

Limitation 1: Our device produces data packet every 10 ms. So driver gets 100 data packets per second. Receive rate is good and we verified it by logs. However we see some delay in AsyncCallback in Swift App. It looks like async calls are queued since we see that Swift App gets callbacks for a few seconds when we stopped to send data from Driver. We measured receive rate in Swift App and it is about 50 data packets per second.

So what's a minimum call rate for AsyncCompletion? Is it higher than 10 ms?

Maybe there is other more efficient way to asynchronously pass data from Driver to Swift App?

Limitation 2: We thought we can buffer data packets and decrease AsyncCompletion call rate. However asyncData could be only 16 of uint64_t by declaration typedef uint64_t IOUserClientAsyncArgumentsArray[16];. Size of our data packer is 112 bytes that perfectly fits to max args size(8*16 = 128 bytes). So we can't cache and send 2 data packets.

How we can avoid this limitation and send more data via AsyncCompletion?
Is there any other API for asynchronous communication that allows send more data back?

Post not yet marked as solved Up vote post of myurik2 Down vote post of myurik2
1.4k views
  • That size limitation listed as "Limitation 2" is only for scalar values. Since you are using IOConnectCallAsyncStructMethod, there is no such limitation. In fact, the "Communicating Between a DriverKit Extension and a Client App" sample shows how to transfer a struct with >512 uint64_ts. Why can't you make a struct will fit multiple packets?

Add a Comment

Replies

@Drewbadour "Communicating Between a DriverKit Extension and a Client App" shows how to transfer struct synchronously via arguments->structureOutput. However in our case we want to save arguments->completion and transfer data asynchronously when it's provided by USB device. So we have to call AsyncCompletion that has following signature

void AsyncCompletion(OSAction *action, IOReturn status, const IOUserClientAsyncArgumentsArray asyncData, uint32_t asyncDataCount, OSDispatchMethod supermethod);

where

typedef uint64_t IOUserClientAsyncArgumentsArray[16];

For larger data transfers like this, please use a shared memory ring buffer (via IODataQueueDispatchSource for example). Then use AsyncCompletion to provide alerts that there is new data in the ring.