Initialize COM for application-owned Wine accessibility objects

This commit is contained in:
Storm Dragon committed 2026-10-10 12:41:45 -04:00
1 parent 25e38907d3
commit 6976c83294
4 files changed
+84 -3

No files matched your search

+13
View File
@@ -801,6 +801,18 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
return 1;
}
// Application-owned MSAA objects (including Windows Forms menus) are
// marshaled across processes. Initialize their callback/message-loop thread;
// Wine's built-in control proxies can otherwise hide this missing setup.
const HRESULT comResult = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
if (FAILED(comResult)) {
std::fprintf(stderr, "wine-access: COM initialization failed (error %lx)\n",
static_cast<unsigned long>(comResult));
g_object_unref(connection);
CloseHandle(singleton);
return 1;
}
HWND window = nullptr;
bool controllerActive = false;
if (controllerEnabled &&
@@ -881,6 +893,7 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
}
if (window != nullptr)
DestroyWindow(window);
CoUninitialize();
g_object_unref(connection);
CloseHandle(singleton);
return 0;
+35
View File
@@ -16,6 +16,12 @@ items; escaped ampersands; duplicate menu selections and reopening; restored
button focus; and exiting with both reader capabilities disabled. Additional
`WM_MENUSELECT` messages exercise duplicate notifications deterministically.
The menu fixture also returns an application-owned MSAA object through
`WM_GETOBJECT`/`LresultFromObject`. The separate helper must retrieve its
`client` role through COM; native text fallback cannot satisfy the check.
This catches missing COM initialization that ordinary Wine control proxies
and the native menu hook did not expose.
The fixture window takes focus and closes automatically. Cleanup stops only
the Wine server for the temporary prefix. The recorded events never reach the
running reader's speech or braille service.
@@ -32,6 +38,35 @@ live listening or physical braille acceptance.
32-bit hook support, entirely custom menus without `WM_MENUSELECT`, and full
object review are outside the current implementation.
## BARD Express managed menus
The October 10 investigation found two independent blockers. The helper's
message-loop thread did not initialize COM, so application-owned MSAA queries
failed with `CO_E_NOTINITIALIZED` and used native window text such as `MenuBar`.
The helper now initializes a single-threaded COM apartment before registering
callbacks and uninitializes it after removing them. The new marshaled-provider
check passes with the fix and fails with the previous helper.
BARD's prefix also registered `IAccessible` against UIA type library
`{930299CE-9965-4DEC-B0F4-A54848D4B667}`, version `1.0`, while its installed
`uiautomationcore.dll` lacked the registered resource `3`.
An initialized read-only query failed with `TYPE_E_CANTLOADLIBRARY`.
After backing up the prefix registry, the 32-bit and 64-bit
`HKLM\Software\Classes\Interface\{618736E0-3C3D-11CF-810C-00AA00389B71}\TypeLib`
registrations were pointed to the loadable standard accessibility type library
`{1EA4DBF0-3C3B-11CF-810C-00AA00389B71}`, version `1.1`.
Read-only inspection then retrieved Account/View/Help and their command labels,
shortcuts and unavailable states from the open application. This repair is
specific to a verified prefix mismatch; the helper does not change registries.
After installing the helper, restart Cthulhu to load it. In BARD, press Alt,
move Left/Right among Account, View and Help, then Down/Up among commands.
Check unavailable items while working offline and Escape back to the main
screen. Recheck Tab/Shift+Tab through buttons, preferences and login fields,
including password exclusion, and ordinary Wine Notepad menus/editing.
Verify audible speech and physical braille separately; querying menu labels
alone does not establish either output gate.
## Editor and actual Notepad checks
The CMake build also includes a small MSAA state decoder check. Run it with
+32 -1
View File
@@ -1,13 +1,34 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
#define COBJMACROS
#include <windows.h>
#include <initguid.h>
#include <oleacc.h>
static HMENU bar, file, edit, nested, context;
static int step;
static HWND marshaledControl;
static HWND button;
static LRESULT CALLBACK provider_proc(HWND window, UINT message, WPARAM wp, LPARAM lp) {
if (message == WM_GETOBJECT && (LONG)lp == OBJID_CLIENT) {
IAccessible *accessible = NULL;
if (SUCCEEDED(CreateStdAccessibleObject(window, OBJID_CLIENT, &IID_IAccessible,
(void **)&accessible))) {
// Returning a marshaled object forces the separate helper to use COM,
// rather than Wine's directly constructed native-control proxy.
LRESULT result = LresultFromObject(&IID_IAccessible, wp, (IUnknown *)accessible);
IAccessible_Release(accessible);
return result;
}
}
return DefWindowProcW(window, message, wp, lp);
}
static LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wp, LPARAM lp) {
if (message == WM_TIMER) {
switch (++step) {
case 1:
SetFocus(button);
// Enter File through the application's actual native menu loop.
PostMessageW(window, WM_SYSCOMMAND, SC_KEYMENU, 'f');
break;
@@ -73,6 +94,7 @@ static LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wp, LPARAM
int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR args, int show) {
(void)previous; (void)args; (void)show;
if (FAILED(CoInitializeEx(NULL, COINIT_APARTMENTTHREADED))) return 1;
WNDCLASSW cls = {0};
cls.hInstance = instance;
cls.lpszClassName = L"WineAccessMenuFixture";
@@ -92,15 +114,24 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR args, int show)
HWND window = CreateWindowW(cls.lpszClassName, L"Wine menu fixture", WS_OVERLAPPEDWINDOW,
120, 120, 400, 240, NULL, bar, instance, NULL);
ShowWindow(window, SW_SHOW);
HWND button = CreateWindowW(L"Button", L"Return to control", WS_CHILD | WS_VISIBLE | WS_TABSTOP,
button = CreateWindowW(L"Button", L"Return to control", WS_CHILD | WS_VISIBLE | WS_TABSTOP,
10, 10, 200, 30, window, NULL, instance, NULL);
cls.lpszClassName = L"WineAccessMarshaledFixture";
cls.lpfnWndProc = provider_proc;
RegisterClassW(&cls);
marshaledControl = CreateWindowW(cls.lpszClassName, L"Application-owned accessibility",
WS_CHILD | WS_VISIBLE | WS_TABSTOP,
10, 60, 250, 30, window, NULL, instance, NULL);
SetForegroundWindow(window);
SetFocus(button);
SetFocus(marshaledControl);
NotifyWinEvent(EVENT_OBJECT_FOCUS, marshaledControl, OBJID_CLIENT, CHILDID_SELF);
SetTimer(window, 1, 400, NULL);
MSG message;
while (GetMessageW(&message, NULL, 0, 0) > 0) {
TranslateMessage(&message); DispatchMessageW(&message);
}
DestroyMenu(context);
CoUninitialize();
return 0;
}
+4 -2
View File
@@ -65,7 +65,7 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
subprocess.run([compiler, "-Wall", "-Wextra", "-mwindows",
*(["-DWINE_ACCESS_WINFORMS_FIXTURE"] if args.fixture == "winforms" else []),
"-o", str(fixture), str(Path(__file__).with_name(sourceName + "_fixture.c")),
"-luser32", "-lcomctl32"], check=True)
"-luser32", "-lcomctl32", "-loleacc", "-lole32"], check=True)
helper = None
app = None
try:
@@ -190,11 +190,13 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
assert len(saved) == 2, f"duplicate suppression/menu reset failed: {len(saved)}"
assert all(e[4] == "Ctrl+P" and e[6] == "checked, unavailable" for e in saved)
assert any(e[2] == "Return to control" and e[3] == "button" for e in events), "control focus missing"
assert any(e[2] == "Application-owned accessibility" and e[3] == "client"
for e in events), "marshaled application MSAA object unavailable"
subprocess.run([str(build / "cthulhu-wine-access.exe"),
"--no-dialog-reader", "--no-controller"],
env=env, check=True, timeout=5)
print("PASS: native and nested menus, context menus, shortcuts, states, "
"duplicate suppression, reopening, control focus, and disable options")
"duplicate suppression, reopening, marshaled MSAA, control focus, and disable options")
finally:
# Only this test's newly created Wine prefix is affected.
subprocess.run(["wineserver", "-k"], env=env, timeout=10, check=False)