Search code examples
c++multithreadingmicrosoft-ui-automationnode-addon-api

How to pass an IUIAutomationElement between threads


I am writing a Node.js native Addon in C++ (using node-addon-api) to interact with Microsofts UIAutomation API. I am trying to listen to Focus Events, wrap the IUIAutomationElement which caused the event and pass the wrapped element to javascript.

I can attach an Event Listener (following this example for Handling Focus Events) which successful receives focus events and the IUIAutomationElement. However all UIAutomation Event Listeners run in a seperate thread

It is safe to make UI Automation calls in a UI Automation event handler, because the event handler is always called on a non-UI thread. (see: https://learn.microsoft.com/en-us/windows/win32/winauto/uiauto-threading).

For example here I pass a lambda function to a wrapper around the IUIAutomation::AddFocusChangedEventHandler method.

this->automation_->SubscribeToFocusChange([callback, this](IUIAutomationElement* el){
    // This code here runs in a non-main thread
    // It gets the correct IUIAutomationElemenet
}

In order to pass the IUIAutomationElement back to Javascript I need to pass it to the main thread. node-addon-api provides Napi::ThreadSafeFunction which is meant to pass variables between threads.

Napi::ThreadSafeFunction callback = Napi::ThreadSafeFunction::New(
    env, 
    info[0].As<Napi::Function>(),
    "Callback",
    0,
    1
);

this->automation_->SubscribeToFocusChange([callback, this](IUIAutomationElement* el){
    // Code running in non-main thread
    // el works here 
    callback.BlockingCall(el, [this](Napi::Env env, Napi::Function jsCallback, IUIAutomationElement* passedEl){
       // Code running in main thread
       // passedEl should be the same as el
    }
}

Note: Here info[0] is a function argument representing a Javascript function.

The problem is that while el works, any functions now run on passedEl throw exceptions.

For example:

BSTR elControlType;
BSTR passedElcontrolType;

// Following works perfectly
HRESULT hr = this->el->get_CurrentLocalizedControlType(&controlType);

// This throws an exception and stops the program
HRESULT hr = this->passedEl->get_CurrentLocalizedControlType(&controlType);

What I've tried

  1. El and passedEl have the same memory address so I believe the IUIAutomationElement is being invalidated when the non-main thread stops.

  2. callback.NonBlockingCall works perfectly with other variables (int, string, custom classes)

My question is what's the correct way of passing an IUIAutomationElement between threads?

From what I've read I need to stop Microsoft from reclaiming the object when the non-main thread stops. I believe to do this I need to get and store a reference to the object but havn't found any documentation for how.


Solution

  • In order for instances from the IUIAutomation API to be passed across threads you need to keep a strong reference. IUIAutomationElement is based off IUnknown so this can be done using IUnknown::AddRef (https://learn.microsoft.com/en-us/windows/win32/api/unknwn/nf-unknwn-iunknown-addref).

    This adds a reference to the reference count meaning the object isn't invalidated once the thread that created it stops and hence stops holding it.

    It's also important to eventually release the object and its memory, this can be done with IUnknown::Release (https://learn.microsoft.com/en-us/windows/win32/api/unknwn/nf-unknwn-iunknown-release).

    This could be generalized by making a wrapper like std::shared_ptr which would help manage the reference, however I havn't been able to figure out how to do this.

    TL;DR: The thread that created the IUIAutomationElement owns that object and its memory. In order to pass it to another thread you need to increment the reference count or else the thread will release the object / memory once it stops.