Files
racket-webview/scrbl/rktwebviewqt-internals.scrbl
T
2026-08-03 16:50:36 +02:00

115 lines
5.1 KiB
Racket

#lang scribble/manual
@defmodule{racket-webview/internals}
@title[#:tag "rktwebviewqt-internals"]{Qt WebView Backend Architecture}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@section[#:tag "rktwebviewqt-internals-overview"]{Overview}
The runtime consists of two processes. The Racket process owns the public API,
contexts used by the local HTTPS server, handle wrappers, and user callbacks.
The helper process @tt{rktwebview_prg} owns @tt{QApplication}, Qt widgets,
@tt{QWebEngineProfile} objects, and all browser state.
The processes communicate through standard pipes. There is no shared library,
shared-memory queue, or callback from native code into the Racket runtime.
@section[#:tag "rktwebviewqt-internals-standard-streams"]{Standard Streams}
The streams have fixed roles:
@itemlist[#:style 'compact
@item{child stdin carries commands from Racket}
@item{child stdout carries the handshake, command results, and events}
@item{child stderr carries diagnostic logging}]
Messages on stdin and stdout are compact UTF-8 JSON objects, one object per
line. Stdout must not contain ordinary logging because the Racket protocol
reader treats every value there as a protocol record.
@section[#:tag "rktwebviewqt-internals-protocol-compatibility"]{Protocol Compatibility}
The command identifiers in @tt{rkt_protocol.h} and the JSON objects used as
command payloads are unchanged. The process transport adds an outer message
object with a type and, for commands and results, a request identifier.
A command has this form:
@verbatim|{{"type":"command","id":17,"command":6,"data":{"wv":1,"url":"https://example.test"}}}|
A corresponding result has this form:
@verbatim|{{"type":"result","id":17,"result":0,"data":null}}|
The request identifier prevents concurrent calls from consuming each other's
results. It is not visible in the public API.
@section[#:tag "rktwebviewqt-internals-command-execution"]{Command Execution}
A Racket caller registers a private response channel, writes a command while
holding the transport write lock, and waits on that channel.
The Qt process has a command-reader thread. That thread parses stdin records and
calls the existing @tt{Rktwebview_qt} methods. Those methods post commands to the
Qt GUI thread and wait for completion. The command-reader thread then writes the
result record to stdout.
The Racket stdout reader is the only thread that reads protocol output. It uses
the request identifier to forward each result to the correct waiting caller.
@section[#:tag "rktwebviewqt-internals-event-delivery"]{Event Delivery}
Qt events remain the original JSON strings. The Qt callback converts them only
into this transport record:
@verbatim|{{"type":"event","wv":1,"data":"{...original event JSON...}"}}|
The stdout reader captures the callback associated with the handle and places
the callback plus event string on an asynchronous Racket channel. A separate
event thread invokes user callbacks. Therefore a slow user callback cannot block
protocol parsing or prevent command results from being received.
Capturing the callback while the event record is read is important for window
closure. Qt writes the @tt{"closed"} event before the result of the close command.
The handle can be removed immediately after the result reaches its caller, but
the already captured callback still receives the event.
@section[#:tag "rktwebviewqt-internals-contexts-and-handles"]{Contexts and Handles}
A context corresponds to a @tt{QWebEngineProfile} and is identified by an
integer. A webview or tray icon is also identified by an integer handle. Only
these numbers cross the process boundary; Qt object pointers never do.
The existing Racket @tt{rkt-wv} structure is retained so the higher layers do
not need to change.
@section[#:tag "rktwebviewqt-internals-lifecycle-and-failure-handling"]{Lifecycle and Failure Handling}
The backend writes a protocol-version handshake after initialization. Racket
waits for this handshake before sending application commands.
For normal shutdown, Racket closes open handles, sends the existing quit
command, waits for its result, closes the child's stdin, and waits for the child
process and reader threads.
If Racket terminates unexpectedly, the operating system closes the stdin pipe.
The command-reader thread sees EOF, closes all Qt windows, and quits the Qt event
loop. This replaces the former shared-memory alive and alive-ack queues.
If stdout closes unexpectedly, Racket marks all handles invalid and delivers an
exception to every command still waiting for a result.
@section[#:tag "rktwebviewqt-internals-logging"]{Logging}
The Qt backend writes its existing log messages to stderr. Racket forwards that
stream to its current error port. The @racket[rkt-webview-info] result reports
@tt{"stderr"} as the backend log destination and retains zero-valued
shared-memory metrics for compatibility with callers that display those fields.
@section[#:tag "rktwebviewqt-internals-development-override"]{Development Override}
Setting @tt{RKT_WEBVIEW_PRG} to a backend executable bypasses the installed
release directory. This makes it possible to test a newly compiled backend
without repackaging or changing the downloader first.