Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Windows: Reliably handle all COM init scenarios on the UI thread #66

Merged
merged 2 commits into from
Nov 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 0 additions & 13 deletions platforms/windows/examples/hello_world.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ use windows::{
Win32::{
Foundation::*,
Graphics::Gdi::ValidateRect,
System::Com::*,
System::LibraryLoader::GetModuleHandleW,
UI::{Input::KeyboardAndMouse::*, WindowsAndMessaging::*},
},
Expand Down Expand Up @@ -229,18 +228,6 @@ fn create_window(title: &str, initial_state: TreeUpdate, initial_focus: NodeId)
}

fn main() -> Result<()> {
// There isn't a single correct answer to the question of how, or whether,
// to initialize COM. It's not clear whether this should even matter
// for a UI Automation provider that, like AccessKit, doesn't use
// COM threading. However, as discussed in #37, it apparently does matter,
// at least on some machines. If a program depends on legacy code
// that requires a single-threaded apartment (STA), then it should
// initialize COM that way. But that's not the case for AccessKit's
// examples and tests, and the multi-threaded apartment (MTA)
// is apparently more reliable for our use case, so we choose that.
unsafe { CoInitializeEx(std::ptr::null_mut(), COINIT_MULTITHREADED) }?;
let _com_guard = scopeguard::guard((), |_| unsafe { CoUninitialize() });

let window = create_window(WINDOW_TITLE, get_initial_state(), INITIAL_FOCUS)?;
unsafe { ShowWindow(window, SW_SHOW) };

Expand Down
18 changes: 18 additions & 0 deletions platforms/windows/src/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ pub struct Manager {

impl Manager {
pub fn new(hwnd: HWND, initial_state: TreeUpdate) -> Self {
// It's unfortunate that we have to force UIA to initialize early;
// it would be more optimal to let UIA lazily initialize itself
// when we receive the first `WM_GETOBJECT`. But if we don't do this,
// then on a thread that's using a COM STA, we can get a race condition
// that leads to nested WM_GETOBJECT messages and, in some cases,
// ATs not realizing that our window natively implements UIA. See #37.
force_init_uia();

Self {
hwnd,
tree: Tree::new(initial_state),
Expand Down Expand Up @@ -58,3 +66,13 @@ impl Manager {
unsafe { UiaReturnRawElementProvider(self.hwnd, wparam, lparam, el) }
}
}

fn force_init_uia() {
// `UiaLookupId` is a cheap way of forcing UIA to initialize itself.
unsafe {
UiaLookupId(
AutomationIdentifierType_Property,
&ControlType_Property_GUID,
)
};
}
32 changes: 24 additions & 8 deletions platforms/windows/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,19 +191,17 @@ where
{
let _lock_guard = MUTEX.lock();

unsafe { CoInitializeEx(std::ptr::null_mut(), COINIT_MULTITHREADED) }.unwrap();
let _com_guard = scopeguard::guard((), |_| unsafe { CoUninitialize() });

let uia: IUIAutomation =
unsafe { CoCreateInstance(&CUIAutomation8, None, CLSCTX_INPROC_SERVER) }?;

let window_mutex: Mutex<Option<HWND>> = Mutex::new(None);
let window_cv = Condvar::new();

crossbeam_utils::thread::scope(|thread_scope| {
thread_scope.spawn(|_| {
unsafe { CoInitializeEx(std::ptr::null_mut(), COINIT_MULTITHREADED) }.unwrap();
let _com_guard = scopeguard::guard((), |_| unsafe { CoUninitialize() });
// We explicitly don't want to initialize COM on the provider thread,
// because we want to make sure that the provider side of UIA works
// even if COM is never initialized on the provider thread
// (as is the case if the window is never shown), or if COM is
// initialized after the window is shown (as is the case,
// at least on some Windows 10 machines, due to IME support).

let window = create_window(window_title, initial_state, initial_focus).unwrap();

Expand Down Expand Up @@ -232,6 +230,24 @@ where
unsafe { PostMessageW(window, WM_CLOSE, WPARAM(0), LPARAM(0)) }.unwrap()
});

// We must initialize COM before creating the UIA client. The MTA option
// is cleaner by far, especially when we want to wait for a UIA event
// handler to be called, and there's no reason not to use it here.
// Note that we don't initialize COM this way on the provider thread,
// as explained above. It's also important that we let the provider
// thread do its forced initialization of UIA, in an environment
// where COM has not been initialized, before we create the UIA client,
// which also triggers UIA initialization, in a thread where COM
// _has_ been initialized. This way, we ensure that the provider side
// of UIA works even if it is set up in an environment where COM
// has not been initialized, and that this sequence of events
// doesn't prevent the UIA client from working.
unsafe { CoInitializeEx(std::ptr::null_mut(), COINIT_MULTITHREADED) }.unwrap();
let _com_guard = scopeguard::guard((), |_| unsafe { CoUninitialize() });

let uia: IUIAutomation =
unsafe { CoCreateInstance(&CUIAutomation8, None, CLSCTX_INPROC_SERVER) }?;

let s = Scope { uia, window };
f(&s)
})
Expand Down