diff --git a/docs/UIA_DISCOVERY.md b/docs/UIA_DISCOVERY.md new file mode 100644 index 0000000..d9def90 --- /dev/null +++ b/docs/UIA_DISCOVERY.md @@ -0,0 +1,88 @@ +# Wine UIA Discovery and Extension + +This document outlines how to identify missing UI Automation (UIA) features in your Wine environment and how to extend Wine's capabilities using standalone artifacts provided in this repository. + +## 1. Discovery: Identifying Missing Features + +The `tools/survey-uia` tool is designed to probe the Wine environment for specific UIA capabilities. + +### Building the Survey Tool + +You must build this tool using a Windows toolchain (MSVC or MinGW). + +```bash +cd tools/survey-uia +mkdir build +cd build +cmake .. +cmake --build . +``` + +### Running the Survey + +Run the executable inside your Wine environment: + +```bash +wine survey_uia.exe > report.json +``` + +### Interpreting the Report + +The tool outputs a JSON report. Key checks include: + +- **`CoCreateInstance(CLSID_CUIAutomation)`**: If this fails, the core UIA system is not initialized or `UIAutomationCore.dll` is missing/broken. +- **`GetRootElement`**: If this fails, the desktop root cannot be accessed. +- **`ElementFromHandle`**: If this fails, mapping HWNDs to UIA elements is broken. +- **Patterns**: Checks for support of common patterns (`LegacyIAccessible`, `Invoke`, `Value`). + +Example failure: +```json +{ + "name": "CoCreateInstance(CLSID_CUIAutomation)", + "passed": false, + "details": "HRESULT: -2147221164" +} +``` + +## 2. Extension: Layering New Capabilities + +If features are missing, you can layer new capabilities into Wine without recompiling Wine itself by creating a **Wine UIA Extension DLL**. + +We provide a prototype for such an extension in `tools/wine-uia-extension`. + +### The Prototype + +The `tools/wine-uia-extension` project contains a scaffold for a COM DLL. This DLL can be used to: + +1. Implement missing COM interfaces (e.g., a custom `IUIAutomation` implementation). +2. Provide a `IRawElementProviderSimple` for specific window classes. +3. Hook into the existing system via registry overrides. + +### Building the Extension + +```bash +cd tools/wine-uia-extension +mkdir build +cd build +cmake .. +cmake --build . +``` + +This produces `wine_uia_extension.dll`. + +### Installing in Wine + +To use this extension, you typically register it as a COM server in the Wine prefix: + +```bash +wine regsvr32 wine_uia_extension.dll +``` + +*Note: You may need to update the `extension.cpp` file to implement the actual `DllRegisterServer` logic to write the correct registry keys for the interfaces you are patching.* + +### Strategy for Implementation + +1. **Identify the missing Interface/CLSID** from the survey report. +2. **Implement the Interface** in `extension.cpp`. +3. **Assign a CLSID** to your implementation. +4. **Register the DLL** so that applications (like `wininspect`) load your DLL instead of (or in addition to) the system default. diff --git a/tools/survey-uia/CMakeLists.txt b/tools/survey-uia/CMakeLists.txt new file mode 100644 index 0000000..dcd7365 --- /dev/null +++ b/tools/survey-uia/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 3.10) +project(survey_uia) + +add_executable(survey_uia survey_uia.cpp) +if(WIN32) + target_link_libraries(survey_uia ole32 oleaut32 uuid) +endif() diff --git a/tools/survey-uia/survey_uia.cpp b/tools/survey-uia/survey_uia.cpp new file mode 100644 index 0000000..d6c0eaa --- /dev/null +++ b/tools/survey-uia/survey_uia.cpp @@ -0,0 +1,163 @@ +#include +#include +#include +#include +#include +#include + +// Helper to escape JSON strings +std::string json_escape(const std::string &s) { + std::string out; + for (char c : s) { + if (c == '"') + out += "\\\""; + else if (c == '\\') + out += "\\\\"; + else if (c == '\b') + out += "\\b"; + else if (c == '\f') + out += "\\f"; + else if (c == '\n') + out += "\\n"; + else if (c == '\r') + out += "\\r"; + else if (c == '\t') + out += "\\t"; + else if ((unsigned char)c < 0x20) { + char buf[7]; + sprintf(buf, "\\u%04x", c); + out += buf; + } else + out += c; + } + return out; +} + +std::string w2u8(const std::wstring &ws) { + if (ws.empty()) + return {}; + int len = WideCharToMultiByte(CP_UTF8, 0, ws.c_str(), (int)ws.size(), nullptr, + 0, nullptr, nullptr); + std::string out(len, '\0'); + WideCharToMultiByte(CP_UTF8, 0, ws.c_str(), (int)ws.size(), out.data(), len, + nullptr, nullptr); + return out; +} + +std::string bstr_to_utf8(BSTR bstr) { + if (!bstr) + return {}; + std::wstring ws(bstr, SysStringLen(bstr)); + return w2u8(ws); +} + +struct CheckResult { + std::string name; + bool passed; + std::string details; +}; + +std::vector results; + +void add_result(const std::string &name, bool passed, + const std::string &details = "") { + results.push_back({name, passed, details}); +} + +int main() { + CoInitializeEx(NULL, COINIT_MULTITHREADED); + + IUIAutomation *pAutomation = NULL; + HRESULT hr = CoCreateInstance(CLSID_CUIAutomation, NULL, CLSCTX_INPROC_SERVER, + IID_IUIAutomation, (void **)&pAutomation); + + add_result("CoCreateInstance(CLSID_CUIAutomation)", SUCCEEDED(hr), + SUCCEEDED(hr) ? "" : "HRESULT: " + std::to_string(hr)); + + if (SUCCEEDED(hr) && pAutomation) { + IUIAutomationElement *pRoot = NULL; + hr = pAutomation->GetRootElement(&pRoot); + add_result("GetRootElement", SUCCEEDED(hr) && pRoot, + SUCCEEDED(hr) ? "" : "HRESULT: " + std::to_string(hr)); + + if (pRoot) { + BSTR name = NULL; + pRoot->get_CurrentName(&name); + add_result("Root.CurrentName", name != NULL, + name ? bstr_to_utf8(name) : "NULL"); + SysFreeString(name); + + // Test FindAll Children + IUIAutomationCondition *pTrueCondition = NULL; + pAutomation->CreateTrueCondition(&pTrueCondition); + if (pTrueCondition) { + IUIAutomationElementArray *pChildren = NULL; + hr = pRoot->FindAll(TreeScope_Children, pTrueCondition, &pChildren); + add_result("Root.FindAll(Children)", SUCCEEDED(hr), + SUCCEEDED(hr) ? "" : "HRESULT: " + std::to_string(hr)); + + if (SUCCEEDED(hr) && pChildren) { + int count = 0; + pChildren->get_Length(&count); + add_result("Root.Children.Count", true, std::to_string(count)); + + if (count > 0) { + IUIAutomationElement *pChild = NULL; + pChildren->GetElement(0, &pChild); + if (pChild) { + BSTR childName = NULL; + pChild->get_CurrentName(&childName); + add_result("Child[0].CurrentName", true, + childName ? bstr_to_utf8(childName) : "(null)"); + SysFreeString(childName); + + // Check Patterns + IUnknown *pPattern = NULL; + hr = pChild->GetCurrentPattern(UIA_LegacyIAccessiblePatternId, + &pPattern); + add_result("Child[0].LegacyIAccessiblePattern", + SUCCEEDED(hr) && pPattern, ""); + if (pPattern) + pPattern->Release(); + + pChild->Release(); + } + } + pChildren->Release(); + } + pTrueCondition->Release(); + } + pRoot->Release(); + } + + // Test ElementFromHandle + HWND hDesktop = GetDesktopWindow(); + IUIAutomationElement *pFromHandle = NULL; + hr = pAutomation->ElementFromHandle(hDesktop, &pFromHandle); + add_result("ElementFromHandle(Desktop)", SUCCEEDED(hr) && pFromHandle, ""); + if (pFromHandle) + pFromHandle->Release(); + + pAutomation->Release(); + } + + CoUninitialize(); + + // Output JSON + std::cout << "{" << std::endl; + std::cout << " \"results\": [" << std::endl; + for (size_t i = 0; i < results.size(); ++i) { + std::cout << " {" << std::endl; + std::cout << " \"name\": \"" << json_escape(results[i].name) << "\"," + << std::endl; + std::cout << " \"passed\": " << (results[i].passed ? "true" : "false") + << "," << std::endl; + std::cout << " \"details\": \"" << json_escape(results[i].details) + << "\"" << std::endl; + std::cout << " }" << (i < results.size() - 1 ? "," : "") << std::endl; + } + std::cout << " ]" << std::endl; + std::cout << "}" << std::endl; + + return 0; +} diff --git a/tools/wine-uia-extension/CMakeLists.txt b/tools/wine-uia-extension/CMakeLists.txt new file mode 100644 index 0000000..ff8102a --- /dev/null +++ b/tools/wine-uia-extension/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.10) +project(wine_uia_extension) + +add_library(wine_uia_extension SHARED extension.cpp) + +if(WIN32) + # Define exports + target_compile_definitions(wine_uia_extension PRIVATE "BUILDING_WINE_UIA_EXTENSION") + # Link against standard libs + target_link_libraries(wine_uia_extension ole32 uuid) +endif() diff --git a/tools/wine-uia-extension/extension.cpp b/tools/wine-uia-extension/extension.cpp new file mode 100644 index 0000000..fd12c42 --- /dev/null +++ b/tools/wine-uia-extension/extension.cpp @@ -0,0 +1,186 @@ +#include +#include +#include +#include +#include + +// Standard COM Globals +std::atomic g_refCount(0); +HINSTANCE g_hInst = NULL; + +// Define a CLSID for our proxy (this would be the CLSID we register to override +// or augment) For example, if we were replacing CUIAutomation, we might use +// that CLSID, but usually we want a unique one and then register it as a +// provider. Here we just use a dummy GUID. +// {12345678-1234-1234-1234-123456789ABC} +static const GUID CLSID_WineUIAExtension = { + 0x12345678, + 0x1234, + 0x1234, + {0x12, 0x34, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC}}; + +// Helper to convert GUID to string +std::wstring GuidToString(const GUID &guid) { + wchar_t buf[39]; + StringFromGUID2(guid, buf, 39); + return std::wstring(buf); +} + +// Registry helpers +bool SetRegistryKey(HKEY hKeyRoot, const std::wstring &subKey, + const std::wstring &valueName, const std::wstring &data) { + HKEY hKey; + LONG lResult = + RegCreateKeyExW(hKeyRoot, subKey.c_str(), 0, NULL, + REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hKey, NULL); + if (lResult != ERROR_SUCCESS) + return false; + + lResult = RegSetValueExW(hKey, valueName.empty() ? NULL : valueName.c_str(), + 0, REG_SZ, (const BYTE *)data.c_str(), + (DWORD)(data.size() + 1) * sizeof(wchar_t)); + + RegCloseKey(hKey); + return lResult == ERROR_SUCCESS; +} + +bool DeleteRegistryKey(HKEY hKeyRoot, const std::wstring &subKey) { + return RegDeleteTreeW(hKeyRoot, subKey.c_str()) == ERROR_SUCCESS; +} + +class CMyUIAProxy : public IUnknown { + std::atomic m_refCount; + +public: + CMyUIAProxy() : m_refCount(1) { g_refCount++; } + virtual ~CMyUIAProxy() { g_refCount--; } + + // IUnknown + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, + void **ppvObject) override { + if (!ppvObject) + return E_POINTER; + if (riid == IID_IUnknown) { + *ppvObject = static_cast(this); + AddRef(); + return S_OK; + } + // TODO: Implement IUIAutomation or IRawElementProviderSimple here + *ppvObject = NULL; + return E_NOINTERFACE; + } + + ULONG STDMETHODCALLTYPE AddRef() override { return ++m_refCount; } + + ULONG STDMETHODCALLTYPE Release() override { + long val = --m_refCount; + if (val == 0) + delete this; + return val; + } +}; + +class CClassFactory : public IClassFactory { + std::atomic m_refCount; + +public: + CClassFactory() : m_refCount(1) { g_refCount++; } + virtual ~CClassFactory() { g_refCount--; } + + // IUnknown + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, + void **ppvObject) override { + if (!ppvObject) + return E_POINTER; + if (riid == IID_IUnknown || riid == IID_IClassFactory) { + *ppvObject = static_cast(this); + AddRef(); + return S_OK; + } + *ppvObject = NULL; + return E_NOINTERFACE; + } + + ULONG STDMETHODCALLTYPE AddRef() override { return ++m_refCount; } + + ULONG STDMETHODCALLTYPE Release() override { + long val = --m_refCount; + if (val == 0) + delete this; + return val; + } + + // IClassFactory + HRESULT STDMETHODCALLTYPE CreateInstance(IUnknown *pUnkOuter, REFIID riid, + void **ppvObject) override { + if (pUnkOuter) + return CLASS_E_NOAGGREGATION; + CMyUIAProxy *pObj = new CMyUIAProxy(); + if (!pObj) + return E_OUTOFMEMORY; + HRESULT hr = pObj->QueryInterface(riid, ppvObject); + pObj->Release(); + return hr; + } + + HRESULT STDMETHODCALLTYPE LockServer(BOOL fLock) override { + if (fLock) + g_refCount++; + else + g_refCount--; + return S_OK; + } +}; + +extern "C" BOOL WINAPI DllMain(HINSTANCE hInst, DWORD reason, LPVOID reserved) { + if (reason == DLL_PROCESS_ATTACH) { + g_hInst = hInst; + DisableThreadLibraryCalls(hInst); + } + return TRUE; +} + +extern "C" __declspec(dllexport) HRESULT WINAPI +DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID *ppv) { + if (rclsid == CLSID_WineUIAExtension) { + CClassFactory *pFactory = new CClassFactory(); + if (!pFactory) + return E_OUTOFMEMORY; + HRESULT hr = pFactory->QueryInterface(riid, ppv); + pFactory->Release(); + return hr; + } + return CLASS_E_CLASSNOTAVAILABLE; +} + +extern "C" __declspec(dllexport) HRESULT WINAPI DllCanUnloadNow() { + return g_refCount == 0 ? S_OK : S_FALSE; +} + +extern "C" __declspec(dllexport) HRESULT WINAPI DllRegisterServer() { + wchar_t modulePath[MAX_PATH]; + if (GetModuleFileNameW(g_hInst, modulePath, MAX_PATH) == 0) { + return E_FAIL; + } + + std::wstring clsidStr = GuidToString(CLSID_WineUIAExtension); + std::wstring key = L"CLSID\\" + clsidStr; + + if (!SetRegistryKey(HKEY_CLASSES_ROOT, key, L"", L"Wine UIA Extension")) + return E_FAIL; + if (!SetRegistryKey(HKEY_CLASSES_ROOT, key + L"\\InProcServer32", L"", + modulePath)) + return E_FAIL; + if (!SetRegistryKey(HKEY_CLASSES_ROOT, key + L"\\InProcServer32", + L"ThreadingModel", L"Both")) + return E_FAIL; + + return S_OK; +} + +extern "C" __declspec(dllexport) HRESULT WINAPI DllUnregisterServer() { + std::wstring clsidStr = GuidToString(CLSID_WineUIAExtension); + std::wstring key = L"CLSID\\" + clsidStr; + DeleteRegistryKey(HKEY_CLASSES_ROOT, key); + return S_OK; +}