Skip to content
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
88 changes: 88 additions & 0 deletions docs/UIA_DISCOVERY.md
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 7 additions & 0 deletions tools/survey-uia/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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()
163 changes: 163 additions & 0 deletions tools/survey-uia/survey_uia.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
#include <UIAutomation.h>
#include <iostream>
#include <map>
#include <string>
#include <vector>
#include <windows.h>

// 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<CheckResult> 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;
}
11 changes: 11 additions & 0 deletions tools/wine-uia-extension/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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()
Loading