How to setup DriverKit Timer Event with OSAction Callback Binding

Hello Everyone,

I'm encountering an issue while setting up a timer event in DriverKit and would appreciate any guidance.

Here's my current implementation:

void DRV_MAIN_CLASS_NAME::SetupEventTimer() 
{
    // 1. Create dispatch queue
    kern_return_t ret = IODispatchQueue::Create("TimerQueue", 0, 0, &ivars->dispatchQueue);
    if (ret != kIOReturnSuccess) {
        LogErr("Failed to create dispatch queue: 0x%x", ret);
        return;
    }

    // 2. Create timer source
    ret = IOTimerDispatchSource::Create(ivars->dispatchQueue, &ivars->dispatchSource);
    if (ret != kIOReturnSuccess) {
        LogErr("Failed to create timer: 0x%x", ret);
        OSSafeReleaseNULL(ivars->dispatchQueue);
        return;
    }
    /*!
     * @brief       Create an instance of OSAction.
     * @discussion  Methods to allocate an OSAction instance are generated for each method defined in a class with
     *              a TYPE attribute, so there should not be any need to directly call OSAction::Create().
     * @param       target OSObject to receive the callback. This object will be retained until the OSAction is
     *              canceled or freed.
     * @param       targetmsgid Generated message ID for the target method.
     * @param       msgid Generated message ID for the method invoked by the receiver of the OSAction
     *              to generate the callback.
     * @param       referenceSize Size of additional state structure available to the creator of the OSAction
     *              with GetReference.
     * @param       action Created OSAction with +1 retain count to be released by the caller.
     * @return      kIOReturnSuccess on success. See IOReturn.h for error codes.
     */
    // 3: Create an OSAction for the TimerOccurred method
    // THIS IS WHERE I NEED HELP 
    OSAction* timerAction = nullptr;
    ret = OSAction::Create(this, 0, 0, 0, &timerAction);
    if (ret != kIOReturnSuccess) {
        LogErr("Failed to create OSAction: 0x%x", ret);
        goto cleanup;
    }

    // 4. Set handler
    ret = ivars->dispatchSource->SetHandler(timerAction);
    if (ret != kIOReturnSuccess) {
        LogErr("Failed to set handler: 0x%x", ret);
        goto cleanup;
    }

    // 5. Schedule timer (1 second)
    uint64_t deadline = mach_absolute_time() + NSEC_PER_SEC;
    ivars->dispatchSource->WakeAtTime(0, deadline, 0);

cleanup:
    if (ret != kIOReturnSuccess) {
        OSSafeReleaseNULL(timerAction);
        OSSafeReleaseNULL(ivars->dispatchSource);
        OSSafeReleaseNULL(ivars->dispatchQueue);
    }
}

Problem:

The code runs but the OSAction callback binding seems incorrect (Step 3).

According to the OSAction documentation, I need to use the TYPE macro to properly bind the callback method. But I try to use

TYPE(DRV_MAIN_CLASS_NAME::TimerOccurred)
kern_return_t TimerOccurred() LOCALONLY;

TYPE(TimerOccurred)
kern_return_t TimerOccurred() LOCALONLY;

kern_return_t TimerOccurred() TYPE(DRV_MAIN_CLASS_NAME::TimerOccurred) LOCALONLY;

All results in Out-of-line definition of 'TimerOccurred' does not match any declaration in 'DRV_MAIN_CLASS_NAME'

Questions:

  1. What is the correct way to declare a timer callback method using TYPE?
  2. How to get the values targetmsgid & msgid generated by Xcode?

Any help would be greatly appreciated!

Best Regards, Charles

Answered by DTS Engineer in 832421022

According to the OSAction documentation, I need to use the TYPE macro to properly bind the callback method.

So, fair warning, DriverKit is not well documented and it can often be quite difficult to find the correct syntax based on the class reference. This issues are then complicated by the fact that some of that parts that are documented are ACTUALLY internal components that we never actually intended you to call.

That's what the discussion here is referring to:

Use the custom constructor method generated by the TYPE macro, instead of this method. For more information on custom constructor methods for action objects, see TYPE.

It's not just that you're not "supposed" to call "Create", it's that Create only really exists so that the TYPE method can call it.

As a side note, I would strongly encourage you to file bugs anytime you run into this kind of issue. Improving all of this is likely to be a long term project, but every bug you file makes that process more likely. Please post the bug number back here if you file one on this issue.

Moving to the specific issue:

  1. What is the correct way to declare a timer callback method using TYPE?

In my experience the best tool you've got is looking through the sample code we have provided (and yes, that's not great either) to find an example of whatever you're trying to do. In this case, the sample "Creating an audio device driver" from AudioDriverKit shows how you use IOTimerDispatchSource. Search that sample for "ZtsTimerOccurred" and you'll see what you need to do.

  1. How to get the values targetmsgid & msgid generated by Xcode?

You don't need those, this is all sorted out by the "TYPE(...)" macro.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

According to the OSAction documentation, I need to use the TYPE macro to properly bind the callback method.

So, fair warning, DriverKit is not well documented and it can often be quite difficult to find the correct syntax based on the class reference. This issues are then complicated by the fact that some of that parts that are documented are ACTUALLY internal components that we never actually intended you to call.

That's what the discussion here is referring to:

Use the custom constructor method generated by the TYPE macro, instead of this method. For more information on custom constructor methods for action objects, see TYPE.

It's not just that you're not "supposed" to call "Create", it's that Create only really exists so that the TYPE method can call it.

As a side note, I would strongly encourage you to file bugs anytime you run into this kind of issue. Improving all of this is likely to be a long term project, but every bug you file makes that process more likely. Please post the bug number back here if you file one on this issue.

Moving to the specific issue:

  1. What is the correct way to declare a timer callback method using TYPE?

In my experience the best tool you've got is looking through the sample code we have provided (and yes, that's not great either) to find an example of whatever you're trying to do. In this case, the sample "Creating an audio device driver" from AudioDriverKit shows how you use IOTimerDispatchSource. Search that sample for "ZtsTimerOccurred" and you'll see what you need to do.

  1. How to get the values targetmsgid & msgid generated by Xcode?

You don't need those, this is all sorted out by the "TYPE(...)" macro.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Dear Kevin,

After tracing through the code, here’s my understanding of the steps involved:

Initialization:

In the init method, assign a work queue to m_work_queue using GetWorkQueue(). Then create a timer dispatch source (IOTimerDispatchSource) link with ZtsTimerOccurred_Impl, and associate it with the work queue.

ivars->m_work_queue = GetWorkQueue();
error = IOTimerDispatchSource::Create(ivars->m_work_queue.get(), &zts_timer_event_source);
ivars->m_zts_timer_event_source = OSSharedPtr(zts_timer_event_source, OSNoRetain);

Next, create an action (OSAction) for handling the timer:

error = CreateActionZtsTimerOccurred(sizeof(void*), &zts_timer_occurred_action); // How can this be done?
ivars->m_zts_timer_occurred_action = OSSharedPtr(zts_timer_occurred_action, OSNoRetain);
ivars->m_zts_timer_event_source->SetHandler(ivars->m_zts_timer_occurred_action.get());

Start I/O

In the StartIO method, use DispatchSync to schedule tasks and call StartTimers().

ivars->m_work_queue->DispatchSync(^(){
    ...
    StartTimers();
});

Start Timers

Set up and enable the timer in StartTimers() to schedule an initial wake-up.

ivars->m_zts_timer_event_source->WakeAtTime(kIOTimerClockMachAbsoluteTime, current_time + ivars->m_zts_host_ticks_per_buffer, 0);
ivars->m_zts_timer_event_source->SetEnable(true);

Timer Event Handler

Handle the timer event in ZtsTimerOccurred_Impl and set the next wake-up.

void SimpleAudioDevice::ZtsTimerOccurred_Impl(OSAction* action, uint64_t time)
{
    ...
    ivars->m_zts_timer_event_source->WakeAtTime(kIOTimerClockMachAbsoluteTime, current_host_time + host_ticks_per_buffer, 0);
    ...
}

Question:

  1. The GetWorkQueue() & CreateActionZtsTimerOccurred methods seem to belong to the framework. How can I do if there’s no explicit API to call?

  2. In StartIO, is it necessary to use DispatchSync, or can another approach be used?

  3. Why are there two WakeAtTime calls—one in StartTimers() and another in ZtsTimerOccurred_Impl? What’s the purpose of this design?

Best Regards, Charles

First of, just so this is clear, I pointed you to the AudioDriverKit sample because it was the best example I could find, not because all of the details of it's implementation are critical or need to be directly copied.

The GetWorkQueue() & CreateActionZtsTimerOccurred methods seem to belong to the framework. How can I do if there’s no explicit API to call?

Starting with the easy case, GetWorkQueue() is a "convenience" method in AudioDriverKit that return one of the IODispatchQueue that's part of AudioDriverKit's internal architecture. In your own code, you use whatever IODispatchQueue you were actually going to use.

The more interesting case is here:

CreateActionZtsTimerOccurred methods seem to belong to the framework.

That is NOT the case. Indeed, I think reason "ZtsTimerOccurred" was chose was partly Hungarian notation but mostly so that it was clearer that it WASN'T part of the underlying framework.

I won't try and untangle how the macro's did it but if you look in your build folder, you can dig into the path:

.../Intermediates.noindex/SimpleAudioDriverExtension.build/Debug-driverkit/SimpleAudioDriver.build/DerivedSources/<dext bundle id>/

Inside that directory are the fully preprocessed sources for your DEXT. That will show that this line:

	virtual void				ZtsTimerOccurred(OSAction* action,
												 uint64_t time) TYPE(IOTimerDispatchSource::TimerOccurred);

Ended up declaring (among other things) these:

\
    kern_return_t\
    CreateActionZtsTimerOccurred(size_t referenceSize, OSAction ** action);\
\
...
\
protected:\
    /* _Impl methods */\
\
    void\
    ZtsTimerOccurred_Impl(SimpleAudioDevice_ZtsTimerOccurred_Args);\
\

Similarly, these are actually two ways of making exactly the same declaration:

1)
void IMPL(SimpleAudioDevice, ZtsTimerOccurred)

2)
void	SimpleAudioDevice::ZtsTimerOccurred_Impl(OSAction* action, uint64_t time)

The "IMPL" format was the original syntax and people started using the second later, I think because it was "decided" that the second syntax was easier to read.

In StartIO, is it necessary to use DispatchSync, or can another approach be used?

No, not really. WakeAtTime() does need to be called on the target dispatch queue, but you could also use DispatchAsync() or call it directly if you "knew" you were already on the right queue.

Why are there two WakeAtTime calls—one in StartTimers() and another in ZtsTimerOccurred_Impl? What’s the purpose of this design?

WakeAtTime only schedules a single call, so the call in "StartTimers()" gets the process started and the call in ZtsTimerOccurred_Impl() is what actually causes the timer to repeatedly fire.

FYI, I believe the reason DriverKit works this way (instead of having simple repeated timers) is that there is an inherent ambiguity in timer scheduling about when EXACTLY a timer should fire. In large scale terms, if a timer is scheduled to fire every "1s" and my handler takes 0.5s to execute, should my timer fire at "1, 2, 3, 4..." or "1, 2.5, 4, 5.5..."? That issue is basically ignored by app developers* but it really does matter in driver development where the timing requirement and execution environments are all much tighter. In any case, the DriverKit pattern makes sure you driver alway "gets what it wants" by forcing your driver to explicitly schedule exactly what it wants.

*What can I say, life is just easier in user space.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Accepted Answer

After encountering issues with the timer firing incorrectly, I refactored the code for setting up a 1-second timer in macOS DriverKit. Below is the implementation:

// Use the Mach Absolute Time clock
uint64_t clockType = kIOTimerClockMachAbsoluteTime;

// Convert Mach Absolute Time to nanoseconds
mach_timebase_info_data_t timebaseInfo;
mach_timebase_info(&timebaseInfo); // Get the timebase info

// Get the current Mach Absolute Time
uint64_t currentTime = mach_absolute_time();
    
// Conversion: Mach Absolute Time * (timebase.numer / timebase.denom)
uint64_t currentTimeInNanoseconds = (currentTime * timebaseInfo.numer) / timebaseInfo.denom;

// Calculate the deadline: 1 second from now in nanoseconds
uint64_t oneSecondInNanoseconds = NSEC_PER_SEC; // 1 second
uint64_t deadlineInNanoseconds = currentTimeInNanoseconds + oneSecondInNanoseconds;
    
// Convert back to Mach Absolute Time units
uint64_t deadline = (deadlineInNanoseconds * timebaseInfo.denom) / timebaseInfo.numer;

// Leeway: Set to 0 for precise timing (can adjust for power efficiency)
uint64_t leeway = 0;

kern_return_t ret = ivars->m_zts_timer_event_source->WakeAtTime(clockType, deadline, leeway);

if (ret != kIOReturnSuccess) {
    LogErr("Failed to schedule WakeAtTime: 0x%x", ret);
    return ret;
}

Log("Timer successfully scheduled to wake in 1 second");

Deadline Calculation: By calculating the deadline with the proper Mach time conversion, the timer behaves as expected.

How can help that anyone who comes across TYPE or OSAction also discovers a good, working example that demonstrates how to use OSAction with a timer event?

FYI, the WakeAtTime entry doesn't do a very good job of describing how the timer options actually work. The key point here is that you're not just selecting a time unit you're also describing HOW the clock will increment, with the critical difference being how sleep is accounted for. In your case, kIOTimerClockUptimeRaw would be the natural alternative, since that avoids dealing with the tick conversion.

As an aside here, the IOTimerDispatchSource entry says:

"There are several timebases you can use to configure the timer, but the most common one is kIOTimerClockMachAbsoluteTime."

I have NO idea why it says that. It's true in the sense that the mach_absolute_time() behavior of:

"Returns current value of a clock that increments monotonically in tick units (starting at an arbitrary point), this clock does not increment while the system is asleep."

...is what most drivers want.

However, the most common time base in our source is in fact kIOTimerClockUptimeRaw, since what most driver ACTUALLY want is "mach_absolute_time() in a straightforward time base like nanoseconds".

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

How to setup DriverKit Timer Event with OSAction Callback Binding
 
 
Q