documentation

This commit is contained in:
2026-03-16 20:04:30 +01:00
parent a7257a3492
commit 7c3b780ae9
14 changed files with 2424 additions and 583 deletions

View File

@@ -1,308 +1,346 @@
#lang scribble/manual
@title{Internal Architecture and Behavior of the QtBacked WebView Bridge}
@title{Structure and behavior of @tt{rktwebview_qt}}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@italic{Technical architecture notes for maintainers}
@section{Overview}
@tt{rktwebview_qt} is a Qt-based backend that provides embedded webviews for
applications written in Racket. The library exposes a small C-compatible API
intended to be used through the Racket FFI. Internally, the implementation is
written in C++ and uses Qt WebEngine.
@;@abstract{
This document explains the internal structure, threading model, command
dispatch mechanism, JavaScript bridge, event flow, timing behavior, and
lifecycle semantics of a Qt WebEnginebacked webview bridge. It focuses on how
a synchronous-looking host API is implemented atop Qts single-threaded GUI
requirements. Public API signatures and usage examples are intentionally
omitted here and should be documented in a separate companion file.
@;}
@section{Design Objectives}
The implementation provides a compact, synchronous façade over Qt WebEngine
while preserving GUI-thread safety, predictable behavior, and a simple, typed
boundary to the host.
The backend provides:
@itemlist[#:style 'compact
@item{All UI work must execute on the Qt GUI thread; the host may call from any thread.}
@item{The host experiences a synchronous API; internally, work is dispatched asynchronously and awaited by pumping the event loop in bounded slices.}
@item{A minimal JSON bridge enables page→host messages without exposing Qt types over the boundary.}
@item{Window lifecycle, geometry, dialogs, and developer tools behave deterministically and are observable via structured events.}
@item{creation of browser windows}
@item{navigation to URLs or HTML content}
@item{execution of JavaScript}
@item{return values from JavaScript}
@item{delivery of browser and window events}
@item{native dialogs such as file choosers and message boxes}
]
@section{Core Building Blocks}
The implementation is intentionally organized so that the external interface
remains simple and language-neutral while the Qt-specific logic is hidden
inside the runtime controller.
@subsection{Command and CommandEvent}
@section{Requirements}
The @tt{rktwebview_qt} backend requires Qt version @tt{6.10.2} or newer.
The backend is built against Qt WebEngine and depends on the runtime
components provided by Qt. Earlier Qt versions are not supported.
Applications using the backend must therefore ensure that a compatible
Qt runtime (≥ @tt{6.10.2}) is available.
In packaged builds the required Qt runtime is normally bundled together
with the backend library.
@subsection{Compatibility}
The backend requires Qt version @tt{6.10.2} or newer.
This requirement is due to the use of the method
@tt{setAdditionalTrustedCertificates} provided by
@tt{QWebEngineProfileBuilder}. This functionality allows the backend to
install additional trusted certificates for a context.
The mechanism is used to support trusted self-signed certificates for
local services. Earlier Qt versions do not provide this functionality
and are therefore not supported.
Packaged builds typically include the required Qt runtime components.
@section{Layers}
The system consists of several layers.
@subsection{C ABI Layer}
The file @tt{rktwebview.h} defines the public API. This layer provides a stable
C-compatible interface that can easily be consumed from Racket through FFI.
Important characteristics of the ABI include:
@itemlist[#:style 'compact
@item{A @tt{Command} encapsulates a request: @emph{code} (enum), @emph{args} (@tt{QVector<QVariant>}), a @emph{result} (@tt{QVariant}), and flags @tt{done} and @tt{js_result_ok}.}
@item{@tt{CommandEvent}: a custom @tt{QEvent} (type @tt{QEvent::User + 1}) that carries a pointer to a @tt{Command} from the callers thread to the GUI thread.}
@item{The GUI thread handles @tt{CommandEvent} in @tt{customEvent}, dispatching to @tt{processCommand} which performs the UI-side operation, sets the result, and flips @tt{done = true}.}
@item{numeric handles represent windows and browser contexts}
@item{result codes indicate success or failure}
@item{data returned from the library is represented as @tt{rkt_data_t}}
@item{event callbacks deliver JSON strings describing events}
]
@subsection{@tt{Rktwebview_qt} Controller}
The C layer is implemented in @tt{rktwebview.cpp}. These functions mainly
forward calls to a singleton instance of the internal runtime class
@tt{Rktwebview_qt}.
@section{Runtime Controller}
The class @tt{Rktwebview_qt} acts as the central runtime controller.
Its responsibilities include:
@itemlist[#:style 'compact
@item{Owns the singleton @tt{QApplication} and initializes the event-pump timers.}
@item{Allocates integer window handles and maintains:
@itemlist[#:style 'compact
@item{@tt{_views}: handle → @tt{WebviewWindow*}}
@item{@tt{_view_js_callbacks}: handle → host event callback}
]
}
@item{Implements the backend of all operations (create/close, navigation, JS run/call, geometry, visibility, devtools, dialogs, title/state, OU token).}
@item{Provides @emph{bounded} event-loop pumping so a non-Qt host can drive the GUI cooperatively.}
@item{Periodically harvests page→host events by invoking JavaScript in each page.}
@item{owning the Qt application instance}
@item{managing https contexts}
@item{managing open windows}
@item{dispatching commands}
@item{forwarding events to the host application}
]
@subsection{@tt{WebviewWindow} (Window) and @tt{WebViewQt} (View)}
Internally, it maintains several registries:
@itemlist[#:style 'compact
@item{@tt{WebviewWindow}: a @tt{QMainWindow} that embeds a @tt{WebViewQt}, handles lifecycle (show/hide/close), debounces geometry changes, opens devtools, processes certificate errors using an OU token, and periodically scrapes JS events from the page.}
@item{@tt{WebViewQt}: a @tt{QWebEngineView} with a stable integer ID and a back-pointer to its parent window; participates in load signal wiring so the controller can inject helper JS and fire a @tt{"page-loaded"} event.}
@item{a map from window handles to @tt{WebviewWindow} instances}
@item{a map from context identifiers to @tt{QWebEngineProfile} objects}
@item{a map from window handles to callback functions}
]
@subsection{Utility Layer}
Each API call that manipulates a window or browser state is forwarded through
this controller.
@section{HTTP(s) Contexts}
A http(s) context represents a shared WebEngine profile.
Contexts are created using @tt{rkt_webview_new_context}. Each context contains:
@itemlist[#:style 'compact
@item{@tt{EventContainer}: a lightweight keyvalue structure for assembling outbound events.}
@item{@tt{mkEventJson}: serializes an @tt{EventContainer} to compact JSON text for the host callback.}
@item{a @tt{QWebEngineProfile}}
@item{optional injected JavaScript code}
@item{optional trusted certificates}
]
@section{Threading, Event Loop, and Timing}
Multiple windows normally share the same context. This allows cookies,
configuration, and injected scripts to be reused.
@subsection{Initialization and Timers}
A context normally represents a https server that can be reached on a certain (local) Url.
On the first entry (or via explicit initialization), the controller constructs
the @tt{QApplication} and sets up two key timers:
@section{Window and View Classes}
Two Qt classes implement the actual browser window.
@subsection{@tt{WebviewWindow}}
@tt{WebviewWindow} derives from @tt{QMainWindow}. It represents the native
top-level window.
Responsibilities include:
@itemlist[#:style 'compact
@item{@bold{JS event harvester} @tt{_process_events}: fires every few milliseconds to call into each page and fetch queued page→host events.}
@item{@bold{Event-loop slice} @tt{_evt_loop_timer}: a single-shot timer used to exit a short run of the Qt event loop; this underpins the bounded pump.}
@item{hosting the browser widget}
@item{reporting window events such as show, hide, move, and resize}
@item{handling close requests}
@item{reporting navigation events}
@item{forwarding JavaScript events}
]
@subsection{Bounded Event-Loop Pumping}
Window geometry changes are slightly delayed through timers so that the host
application receives fewer intermediate events.
Because the host may not yield a classic Qt main loop, the controller exposes a
“pump” that executes Qts event loop briefly, then exits via the single-shot
timer. A host helper can call this repeatedly for a requested duration to:
@subsection{@tt{WebViewQt}}
@tt{WebViewQt} derives from @tt{QWebEngineView}. This class mainly associates
the Qt widget with a numeric handle used by the external API.
@section{Command Dispatch}
Many API functions are implemented using a small command-dispatch mechanism.
A command object represents a pending request. The request is delivered to the
Qt event system using a custom Qt event. The runtime controller receives the
event and executes the corresponding operation.
This design ensures that GUI operations occur inside the Qt event loop while
the external API remains synchronous.
@section{Event Processing}
Qt events are processed through repeated calls to
@tt{rkt_webview_process_events}, which internally calls
@tt{QApplication::processEvents}. This allows the host runtime to control how
frequently GUI events are processed.
@section{Navigation}
Two main navigation operations are provided.
@itemlist[#:style 'compact
@item{Deliver posted @tt{CommandEvent}s to the GUI thread.}
@item{Complete asynchronous WebEngine operations (e.g., JS callbacks, load signals).}
@item{Run the JS harvester that drains page→host events.}
@item{@tt{rkt_webview_set_url} loads a URL}
@item{@tt{rkt_webview_set_html} loads raw HTML}
]
This design preserves GUI responsiveness and enables synchronous host calls
without blocking the GUI thread.
When page loading finishes, a @tt{"page-loaded"} event is emitted. The event
contains a boolean field indicating whether loading succeeded.
@section{Lifecycle and Window Semantics}
@section{JavaScript Execution}
@subsection{Creation and Parenting}
JavaScript execution is provided through two functions.
@subsection{Fire-and-Forget Execution}
@tt{rkt_webview_run_js} executes JavaScript without returning a result.
This is useful for DOM manipulation or triggering page behavior.
@subsection{Synchronous Execution with Result}
@tt{rkt_webview_call_js} evaluates JavaScript and returns a result object.
The JavaScript is wrapped in a small function that captures either the result
value or an exception. The result is returned as a JSON object containing:
@itemlist[#:style 'compact
@item{A “create” command constructs a @tt{WebviewWindow}, optionally modal if a valid parent handle is supplied.}
@item{A @tt{WebViewQt} is created with a fresh handle and set as the central widget.}
@item{The window is shown; the command returns only after the window reports @emph{created}, ensuring safe subsequent operations.}
@item{@tt{oke}}
@item{@tt{result}}
@item{@tt{exn}}
]
@subsection{Closing and the Blocked-Close Pattern}
The native side waits for the asynchronous Qt callback before returning the
value to the host application.
@section{JavaScript Event Bridge}
Communication from JavaScript to the host application uses a small event queue
implemented in injected JavaScript.
When a browser context is created, the runtime injects support code defining
the following objects:
@itemlist[#:style 'compact
@item{If the window is not yet permitted to close, a close request is ignored and a @tt{"can-close?"} event is emitted to the host.}
@item{When permitted, the window closes, a @tt{"closed"} event is emitted, and the handle is removed from controller maps. Any devtools window is also disposed.}
@item{@tt{window.rkt_event_queue}}
@item{@tt{window.rkt_send_event(obj)}}
@item{@tt{window.rkt_get_events()}}
]
@subsection{Visibility and Activation}
The queue stores JavaScript-generated events until the native side retrieves
them.
@itemlist[#:style 'compact
@item{@tt{show}, @tt{hide}, @tt{show-normal}, @tt{minimize}, @tt{maximize}, @tt{present}.}
@item{@tt{present} ensures the window is shown, raised, and activated.}
@item{Window state queries map Qts visibility/activation to a simplified enum, including @emph{active} variants.}
]
@subsection{Event Queue}
@subsection{Geometry, Debounce, and Completion Criteria}
Move/resize semantics are designed to be deterministic:
@itemlist[#:style 'compact
@item{Each @tt{move} or @tt{resize} triggers a 500ms single-shot timer that coalesces rapid changes (e.g., during drags).}
@item{The command completes only after the windows internal move/resize counter increments, guaranteeing a visible state change before success is reported.}
@item{Corresponding @tt{"move"} and @tt{"resize"} events include stable geometry values @tt{x}, @tt{y}, @tt{w}, and @tt{h}.}
]
@section{JavaScript Bridge}
@subsection{Injected Helpers and Event Queue}
On page load, the controller ensures the following helpers exist in the page
context (application world), establishing a minimal event queue:
When page code wants to emit an event, it calls:
@verbatim|{
window.rkt_event_queue = [];
window.rkt_send_event = function(obj) {
window.rkt_event_queue.push(obj);
};
window.rkt_get_events = function() {
let q = window.rkt_event_queue;
window.rkt_event_queue = [];
return JSON.stringify(q);
};
window.rkt_send_event(obj)
}|
Notes:
The object is appended to the queue.
The queue can be retrieved using:
@verbatim|{
window.rkt_get_events()
}|
This function returns a JSON array and clears the queue.
@subsection{Native Notification}
To notify the native side that events are waiting, the injected code creates a
hidden iframe named @tt{rkt-evt-frame}. The iframe is used only as a signalling
mechanism.
After adding an event to the queue, the script attempts to call:
@verbatim|{
frameWindow.print()
}|
Qt receives this through the signal
@tt{QWebEnginePage::printRequestedByFrame}.
The window implementation checks whether the frame name is
@tt{"rkt-evt-frame"}. If so, it calls @tt{processJsEvents} to retrieve the
queued events.
This results in the following sequence:
@itemlist[#:style 'compact
@item{The helpers are injected after page load so they are present for app code regardless of the navigated content.}
@item{A previously prepared @tt{QWebEngineScript} injection path exists on the window, but the explicit insertion call is disabled; the active injection occurs via the controller on page load, which is sufficient in practice.}
@item{JavaScript calls @tt{rkt_send_event}.}
@item{The event is added to the queue.}
@item{The hidden iframe triggers a print request.}
@item{Qt receives the request.}
@item{The native side calls @tt{rkt_get_events}.}
@item{Each event object is delivered to the host.}
]
@subsection{Periodic Harvesting (Page → Host)}
The mechanism is therefore not a simple native polling loop. It is better
described as a queued JavaScript event bridge with a lightweight native
wake-up signal.
A fast timer iterates active windows and evaluates @tt{rkt_get_events()} in each
page. The returned JSON array is parsed; each object is wrapped as an
@tt{EventContainer} of kind @tt{"js-evt"} (embedding the original object under
a field) and emitted to the host callback as a compact JSON string.
@subsection{Delivered Event Format}
@itemlist[#:style 'compact
@item{This keeps the host callback signature simple (C string payload).}
@item{Batching reduces cross-thread chatter; clearing the page-side queue prevents duplicates.}
]
JavaScript-originated events are forwarded to the host as structured JSON.
@subsection{Run vs Call (Host → Page)}
Each event uses a generic envelope:
Two execution modes are differentiated to avoid conflating effects and results:
@itemlist[#:style 'compact
@item{@bold{Run}: fire-and-forget evaluation for side-effects (e.g., DOM mutations). No return value is captured.}
@item{@bold{Call}: user code is wrapped in a @tt{try/catch} scaffold that returns a JSON object:
@verbatim|{
@verbatim|{
{
"oke": <boolean>,
"result": <any-or-false>,
"exn": <string-or-false>
"evt": "js-evt",
"js-evt": { ... }
}
}|
The native side returns this JSON as a newly allocated result object; @tt{js_result_ok} mirrors the @tt{"oke"} field.}
]
@section{Dialogs and Security Controls}
The inner object is the original JavaScript event payload.
@subsection{Native Dialogs}
This design keeps the native API simple while allowing arbitrary JavaScript
objects to be delivered to the host application.
@section{Window Lifecycle}
Windows are created using @tt{rkt_webview_create}. Each window receives a
callback function used to deliver events.
Important lifecycle events include:
@itemlist[#:style 'compact
@item{@tt{choose-dir}: returns a compact JSON with either @tt{"choosen"} and a directory path, or @tt{"canceled"} with the base directory.}
@item{@tt{file-open}, @tt{file-save}: similarly return @tt{"choosen"} with a file path and the filter used, or @tt{"canceled"} with the last-selected filter.}
@item{The host must free returned result objects; see @secref{memory}.}
@item{@tt{"show"}}
@item{@tt{"hide"}}
@item{@tt{"move"}}
@item{@tt{"resize"}}
@item{@tt{"closed"}}
]
@subsection{Certificate Handling with OU Token}
If the user attempts to close a window directly, the close request is converted
into a @tt{"can-close?"} event so that the host application can decide whether
the window should actually close.
When a certificate error arises, the window inspects the chain; if it encounters
a self-signed certificate whose Organizational Unit (OU) equals the configured
token, it accepts the certificate.
@section{Native Dialogs}
The backend also exposes native dialogs including:
@itemlist[#:style 'compact
@item{This provides a controlled trust mechanism for private/dev deployments.}
@item{Ordinary sites remain subject to standard verification.}
@item{directory selection}
@item{file open}
@item{file save}
@item{message boxes}
]
@section{Event Model (Delivered to Host)}
The results of these dialogs are delivered as events so that the host
application remains in control of the user interface flow.
All events are delivered as compact JSON strings to the hosts callback.
@section{Certificates}
@itemlist[#:style 'compact
@item{@tt{"page-loaded"}: emitted after helper JS installation; includes an @tt{"oke"} boolean indicating load success.}
@item{@tt{"show"} and @tt{"hide"}: visibility transitions (hide is suppressed on final close).}
@item{@tt{"move"}: includes @tt{"x"}, @tt{"y"} after debounce.}
@item{@tt{"resize"}: includes @tt{"w"}, @tt{"h"} after debounce.}
@item{@tt{"can-close?"}: emitted when a close was requested but not yet permitted.}
@item{@tt{"closed"}: emitted when the window is actually destroyed and unregistered.}
@item{@tt{"js-evt"}: wrapper events for each page-originated object collected via the event queue.}
]
Contexts may optionally install trusted certificates. Additionally, a window
may specify a special Organizational Unit token used to automatically trust
certain self-signed certificates.
@section{Command Set and Internal Behavior}
This mechanism is useful when communicating with local development services.
This section summarizes each commands core behavior and completion rule.
@section{Memory Ownership}
@itemlist[#:style 'compact
@item{@bold{CREATE}: build @tt{WebviewWindow} (+ optional modal parent), create @tt{WebViewQt}, assign handle, @tt{show}, wait until @emph{created} flag is true, then return handle.}
@item{@bold{CLOSE}: mark as allowed to close, call @tt{close()}, emit @tt{"closed"} on destruction, unregister handle.}
@item{@bold{SET\_OU\_TOKEN}: stash OU token in the window for later certificate checks.}
@item{@bold{SET\_URL}: @tt{QWebEngineView::setUrl}; return success/failure after dispatch.}
@item{@bold{SET\_HTML}: @tt{QWebEngineView::setHtml}; return success/failure after dispatch.}
@item{@bold{SET\_TITLE}: set the native window title; return success/failure.}
@item{@bold{RUN\_JS}: invoke JavaScript without awaiting a value; return success/failure (evaluation dispatched).}
@item{@bold{CALL\_JS}: wrap in @tt{try/catch}, return JSON object; success when callback delivers result, propagate @tt{oke} to @tt{js_result_ok}.}
@item{@bold{DEV\_TOOLS}: open a separate window, attach the pages devtools page, and keep it in sync; dispose with parent.}
@item{@bold{SHOW/HIDE/PRESENT/MAXIMIZE/MINIMIZE/SHOW\_NORMAL}: call corresponding Qt methods and return @tt{oke} upon dispatch; @tt{present} additionally raises and activates.}
@item{@bold{WINDOW\_STATUS}: map @tt{isHidden}/@tt{isMinimized}/@tt{isMaximized}/@tt{isVisible} (+ @tt{isActiveWindow}) to a simplified enum (including @emph{active} variants).}
@item{@bold{MOVE}: call @tt{move(x,y)}, then wait until the move counter increments (debounced) before acknowledging success.}
@item{@bold{RESIZE}: call @tt{resize(w,h)}, then wait until the resize counter increments (debounced) before acknowledging success.}
@item{@bold{CHOOSE\_DIR / FILE\_OPEN / FILE\_SAVE}: open native dialogs, convert result to compact JSON, return as newly allocated result object.}
]
Data returned from the library and (javascript) events originating from the library
are allocated on the native side and must be released by the host using
@tt{rkt_webview_free_data}.
@section[#:tag "memory"]{Memory, Ownership, and Boundaries}
@;@seclabel{memory}
@itemlist[#:style 'compact
@item{@italic{Events} and @italic{JS-call results} are returned as small C structs containing freshly allocated UTF8 strings; the host must free them using the provided destroy helpers.}
@item{Qt types remain internal; boundary values are converted to/from UTF8 @tt{char*}.}
@item{Commands are transient and owned by the GUI thread; the host never owns a @tt{Command}.}
]
@section{Determinism, Failure Modes, and Guarantees}
@itemlist[#:style 'compact
@item{All UI-affecting operations execute on the GUI thread via posted commands (with normal priority), ensuring thread safety and sequential handling as the event queue is processed by Qt in order of posting.}
@item{Synchronous host calls return only after a definitive outcome:
@itemlist[#:style 'compact
@item{Navigation/HTML: boolean success/failure.}
@item{JS run: success/failure of dispatch.}
@item{JS call: structured JSON with @tt{oke}/@tt{result}/@tt{exn}.}
@item{Geometry: acknowledgment only after debounced counters change.}
@item{Dialogs: compact JSON describing @tt{"choosen"} or @tt{"canceled"}.}
]
}
@item{State queries (e.g., window state) are also executed as commands, reflecting the GUI threads authoritative view.}
]
@section{Edge Cases and Notes}
@itemlist[#:style 'compact
@item{Helper JS can be injected via two mechanisms; the active path injects after page load from the controller. A prebuilt @tt{QWebEngineScript} insertion in the window is present but disabled.}
@item{Debounce windows (500ms timers) intentionally delay geometry events and command completion under heavy interaction, trading a tiny latency for stability and reduced churn.}
@item{Host-driven pumps should be called frequently enough to maintain UI responsiveness and timely delivery of JS results and events.}
]
@section{Worked Walkthrough (Typical Session)}
A typical host session proceeds like this:
@itemlist[#:style 'compact
@item{Initialize controller (implicit on first call).}
@item{Create window A; move/resize; set URL or HTML.}
@item{Pump events in a loop; open devtools if needed.}
@item{Call JS to compute a value (returns structured JSON) and run JS for side-effects (fire-and-forget).}
@item{From page scripts, call @tt{rkt_send_event({...})}; the host periodically receives @tt{"js-evt"} payloads during pumps.}
@item{Optionally create window B as a modal child of A; navigate separately; continue pumping.}
@item{Close B; later close A (possibly after a @tt{"can-close?"} handshake).}
]
@section{Extensibility Guidelines}
@itemlist[#:style 'compact
@item{For new features, add a command code, implement a @tt{processCommand} case, and provide a controller wrapper that constructs the command and awaits completion.}
@item{Prefer compact JSON for multi-field results to keep the host boundary stable and language-agnostic.}
@item{If adding richer page bridges, inject helpers on load and keep harvesting batched and periodic to minimize overhead.}
@item{Maintain the convention that all externally observable events include an explicit @tt{"event"} tag plus relevant fields.}
]
This avoids exposing C++ ownership semantics across the FFI boundary.
@section{Summary}
Internally, this bridge is a command-queued, GUI-threadsafe façade over Qt
WebEngine. The host sees synchronous operations; under the hood, each action is
posted to the GUI thread, the event loop is pumped in short slices, and
completion is acknowledged only after the intended UI effect is verifiably in
place. A small JSON bridge carries page→host messages and supports synchronous
host→page calls with structured results, keeping the boundary stable and
language-neutral.
The @tt{rktwebview_qt} backend provides a compact architecture for embedding
Qt WebEngine inside Racket applications.
The design allows Racket programs to implement desktop applications using
HTML and JavaScript for the user interface while keeping application logic in
the host runtime.