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
El
and passedEl
have the same memory address so I believe the IUIAutomationElement
is being invalidated when the non-main thread stops.
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.
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.