Skip to content

Commit dd69d91

Browse files
asklarCopilot
andauthored
Add plugin system for runtime-loaded framework providers (#3)
Add a generic plugin architecture that allows external DLLs to extend lvt with additional UI framework support. Plugins are discovered at startup from %USERPROFILE%/.lvt/plugins/ and loaded dynamically via a C ABI. Core changes: - src/plugin.h: C ABI plugin interface with versioning (LvtPluginInfo, LvtFrameworkDetection, LvtEnrichTreeFn, etc.) - src/plugin_loader.h/cpp: Plugin discovery, loading, framework detection delegation, and JSON-based tree grafting. Plugins return JSON tree data which is grafted into the Win32 element tree by matching target_hwnd. - framework_detector.h/cpp: Added Framework::Plugin enum value with string name field. Plugin-detected frameworks are appended after built-in ones. - tree_builder.cpp: Calls enrich_with_plugin() for plugin-detected frameworks. - main.cpp: Plugin load/unload lifecycle, framework_display_name() for output. - CMakeLists.txt: Added plugin_loader.cpp to lvt and test targets. TAP DLL cleanup: - WPF TAP DLL now calls FreeLibraryAndExitThread to unload itself after collection, matching the convention for all injected DLLs. Removed the MonitorThread/trigger event mechanism (no longer needed since each run is a fresh injection). Tests: - 8 new PluginGraft tests: HWND matching, root fallback, multiple roots, nested children, properties, invalid JSON, deep matching. - 3 new FrameworkDisplayName tests. - All 44 tests pass. Co-authored-by: Alexander Sklar <AskLar@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 21f6036 commit dd69d91

14 files changed

Lines changed: 674 additions & 95 deletions

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
run: cmake --preset default
2828

2929
- name: Build managed WPF assembly
30-
run: msbuild src/tap_wpf/LvtWpfTap.csproj /p:Configuration=Release /restore /v:minimal
30+
run: msbuild src/tap_wpf/LvtWpfTap.csproj /p:Configuration=Release /p:Platform=AnyCPU /restore /v:minimal
3131

3232
- name: Build
3333
run: cmake --build build

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ jobs:
5353
run: cmake --preset ${{ matrix.preset }} -DCMAKE_BUILD_TYPE=Release
5454

5555
- name: Build managed WPF assembly
56-
run: msbuild src/tap_wpf/LvtWpfTap.csproj /p:Configuration=Release /restore /v:minimal
56+
run: msbuild src/tap_wpf/LvtWpfTap.csproj /p:Configuration=Release /p:Platform=AnyCPU /restore /v:minimal
5757

5858
- name: Build
5959
run: cmake --build ${{ matrix.build_dir }}

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Build output
22
build/
3+
build-x86/
4+
build-arm64/
35

46
# IDE files
57
.vs/

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ add_executable(lvt
1414
src/tree_builder.cpp
1515
src/json_serializer.cpp
1616
src/screenshot.cpp
17+
src/plugin_loader.cpp
1718
src/providers/win32_provider.cpp
1819
src/providers/comctl_provider.cpp
1920
src/providers/xaml_provider.cpp
@@ -104,6 +105,7 @@ add_executable(lvt_unit_tests
104105
src/json_serializer.cpp
105106
src/framework_detector.cpp
106107
src/target.cpp
108+
src/plugin_loader.cpp
107109
src/providers/win32_provider.cpp
108110
src/providers/comctl_provider.cpp
109111
src/providers/xaml_provider.cpp

src/framework_detector.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include "framework_detector.h"
2+
#include "plugin_loader.h"
23
#include <wil/resource.h>
34
#include <Psapi.h>
45

@@ -13,10 +14,16 @@ std::string framework_to_string(Framework f) {
1314
case Framework::Xaml: return "xaml";
1415
case Framework::WinUI3: return "winui3";
1516
case Framework::Wpf: return "wpf";
17+
case Framework::Plugin: return "plugin";
1618
}
1719
return "unknown";
1820
}
1921

22+
std::string framework_display_name(const FrameworkInfo& fi) {
23+
if (!fi.name.empty()) return fi.name;
24+
return framework_to_string(fi.type);
25+
}
26+
2027
static const wchar_t* comctl_classes[] = {
2128
L"SysListView32", L"SysTreeView32", L"SysTabControl32",
2229
L"msctls_statusbar32", L"ToolbarWindow32", L"msctls_trackbar32",
@@ -191,6 +198,12 @@ std::vector<FrameworkInfo> detect_frameworks(HWND hwnd, DWORD pid) {
191198
if (!detectedWpf && data.hasWpf)
192199
result.push_back({Framework::Wpf, {}});
193200

201+
// Plugin-provided framework detection
202+
auto pluginFws = detect_plugin_frameworks(hwnd, pid);
203+
for (auto& pf : pluginFws) {
204+
result.push_back({Framework::Plugin, pf.version, pf.name});
205+
}
206+
194207
return result;
195208
}
196209

src/framework_detector.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,20 @@ enum class Framework {
1111
Xaml,
1212
WinUI3,
1313
Wpf,
14+
Plugin, // Plugin-provided framework (name in FrameworkInfo::name)
1415
};
1516

1617
struct FrameworkInfo {
1718
Framework type;
1819
std::string version; // e.g. "3.1.7.2602" for WinUI3, "6.10" for comctl
20+
std::string name; // Plugin-provided name (empty for built-in frameworks)
1921
};
2022

2123
std::string framework_to_string(Framework f);
2224

25+
// Returns the display name for a FrameworkInfo (uses name field if set).
26+
std::string framework_display_name(const FrameworkInfo& fi);
27+
2328
// Detect which UI frameworks are in use for the given window/process.
2429
std::vector<FrameworkInfo> detect_frameworks(HWND hwnd, DWORD pid);
2530

src/main.cpp

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include "tree_builder.h"
44
#include "json_serializer.h"
55
#include "screenshot.h"
6+
#include "plugin_loader.h"
67
#include "debug.h"
78

89
#include <cstdio>
@@ -111,6 +112,9 @@ int main(int argc, char* argv[]) {
111112

112113
auto args = parse_args(argc, argv);
113114

115+
// Load plugins from %USERPROFILE%/.lvt/plugins/
116+
lvt::load_plugins();
117+
114118
// --dump is default unless --screenshot is specified without --dump
115119
if (!args.dumpSet)
116120
args.dump = args.screenshotFile.empty();
@@ -190,12 +194,13 @@ int main(int argc, char* argv[]) {
190194
if (args.frameworksOnly) {
191195
// Just print detected frameworks
192196
for (auto& fi : frameworks) {
197+
auto name = lvt::framework_display_name(fi);
193198
if (fi.version.empty())
194-
printf("%s\n", lvt::framework_to_string(fi.type).c_str());
199+
printf("%s\n", name.c_str());
195200
else
196-
printf("%s %s\n", lvt::framework_to_string(fi.type).c_str(),
197-
fi.version.c_str());
201+
printf("%s %s\n", name.c_str(), fi.version.c_str());
198202
}
203+
lvt::unload_plugins();
199204
return 0;
200205
}
201206

@@ -221,10 +226,11 @@ int main(int argc, char* argv[]) {
221226
if (args.dump) {
222227
std::vector<std::string> frameworkNames;
223228
for (auto& fi : frameworks) {
229+
auto name = lvt::framework_display_name(fi);
224230
if (fi.version.empty())
225-
frameworkNames.push_back(lvt::framework_to_string(fi.type));
231+
frameworkNames.push_back(name);
226232
else
227-
frameworkNames.push_back(lvt::framework_to_string(fi.type) + " " + fi.version);
233+
frameworkNames.push_back(name + " " + fi.version);
228234
}
229235

230236
std::string serialized;
@@ -259,5 +265,6 @@ int main(int argc, char* argv[]) {
259265
}
260266
}
261267

268+
lvt::unload_plugins();
262269
return 0;
263270
}

src/plugin.h

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#pragma once
2+
3+
// lvt plugin interface — C ABI for runtime-loaded framework provider plugins.
4+
// Plugins are DLLs placed in %USERPROFILE%/.lvt/plugins/ and discovered at startup.
5+
// This header is the ONLY dependency between lvt core and any plugin.
6+
7+
#include <stdint.h>
8+
#include <Windows.h>
9+
10+
#ifdef __cplusplus
11+
extern "C" {
12+
#endif
13+
14+
#define LVT_PLUGIN_API_VERSION 1
15+
16+
// ---------- Plugin metadata ----------
17+
18+
struct LvtPluginInfo {
19+
uint32_t struct_size; // sizeof(LvtPluginInfo), for versioning
20+
uint32_t api_version; // must be LVT_PLUGIN_API_VERSION
21+
const char* name; // short identifier, e.g. "myframework"
22+
const char* description; // human-readable, e.g. "Custom framework support"
23+
};
24+
25+
// ---------- Framework detection ----------
26+
27+
struct LvtFrameworkDetection {
28+
uint32_t struct_size;
29+
const char* name; // framework name reported by plugin
30+
const char* version; // version string or NULL
31+
};
32+
33+
// ---------- Element data (C ABI mirror of lvt::Element) ----------
34+
35+
struct LvtBounds {
36+
int32_t x, y, width, height;
37+
};
38+
39+
struct LvtProperty {
40+
const char* key;
41+
const char* value;
42+
};
43+
44+
struct LvtElementData {
45+
uint32_t struct_size;
46+
const char* type;
47+
const char* framework;
48+
const char* class_name;
49+
const char* text;
50+
LvtBounds bounds;
51+
const LvtProperty* properties;
52+
uint32_t property_count;
53+
struct LvtElementData* children;
54+
uint32_t child_count;
55+
uintptr_t native_handle; // e.g. HWND
56+
};
57+
58+
// ---------- Plugin entry points ----------
59+
// Plugins must export these functions by name.
60+
61+
// Returns static plugin metadata. Called once at load time.
62+
typedef LvtPluginInfo* (*LvtPluginInfoFn)(void);
63+
64+
// Detect if this plugin's framework is present in the target process.
65+
// Returns nonzero if detected, fills `out` with framework info.
66+
// `out` is caller-allocated. Plugin should set name and version fields.
67+
typedef int (*LvtDetectFrameworkFn)(DWORD pid, HWND hwnd, LvtFrameworkDetection* out);
68+
69+
// Enrich the element tree with this plugin's framework data.
70+
// `json_out` receives a malloc'd JSON string (caller frees with lvt_plugin_free).
71+
// The JSON follows the same schema as the XAML TAP DLL output:
72+
// [{"type":"...", "name":"...", "children":[...], "width":..., "height":..., "offsetX":..., "offsetY":...}]
73+
// `hwnd_filter` is the HWND of a specific host window to scope enrichment to,
74+
// or NULL for all.
75+
// Returns nonzero on success.
76+
typedef int (*LvtEnrichTreeFn)(HWND hwnd, DWORD pid, const char* element_class_filter, char** json_out);
77+
78+
// Free memory allocated by the plugin (e.g. json_out from LvtEnrichTreeFn).
79+
typedef void (*LvtPluginFreeFn)(void* ptr);
80+
81+
// Exported function names (for GetProcAddress)
82+
#define LVT_PLUGIN_INFO_FUNC "lvt_plugin_info"
83+
#define LVT_PLUGIN_DETECT_FUNC "lvt_detect_framework"
84+
#define LVT_PLUGIN_ENRICH_FUNC "lvt_enrich_tree"
85+
#define LVT_PLUGIN_FREE_FUNC "lvt_plugin_free"
86+
87+
#ifdef __cplusplus
88+
}
89+
#endif

0 commit comments

Comments
 (0)