Omzetting naar stdio.
This commit is contained in:
@@ -1,11 +1,22 @@
|
|||||||
# racket-webview
|
# racket-webview
|
||||||
|
|
||||||
Webview integration for racket
|
Webview integration for Racket.
|
||||||
|
|
||||||
## Release v0.1.8
|
## Release v0.1.10
|
||||||
|
|
||||||
Uses release v0.2.2 of racket-webview-qt.
|
This version replaces the FFI/shared-memory transport with a child-process
|
||||||
QtWebEngine select popup rendering issue observed with older Qt 6.x.
|
interface to racket-webview-qt v0.2.3:
|
||||||
Symptom: native HTML <select> popup keeps growing/repainting while open;
|
|
||||||
QtWebEngine process busy, Racket process idle.
|
- commands: JSON lines on child stdin
|
||||||
Resolved by upgrading to Qt 6.11.1.
|
- results and events: JSON lines on child stdout
|
||||||
|
- backend diagnostics: child stderr
|
||||||
|
|
||||||
|
The public Racket procedures, command numbers, command payloads, result symbols,
|
||||||
|
and event JSON remain unchanged. Set `RKT_WEBVIEW_PRG` to an explicit locally
|
||||||
|
built backend executable during development.
|
||||||
|
|
||||||
|
## Previous Qt update
|
||||||
|
|
||||||
|
QtWebEngine select popup rendering issues were observed with older Qt 6.x. The
|
||||||
|
native HTML `<select>` popup could keep growing or repainting while open while
|
||||||
|
the QtWebEngine process remained busy. Upgrading to Qt 6.11.1 resolved this.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#lang info
|
#lang info
|
||||||
|
|
||||||
(define pkg-authors '(hnmdijkema))
|
(define pkg-authors '(hnmdijkema))
|
||||||
(define version "0.1.9")
|
(define version "0.1.10")
|
||||||
(define license 'MIT)
|
(define license 'MIT)
|
||||||
(define collection "racket-webview")
|
(define collection "racket-webview")
|
||||||
(define pkg-desc "racket-webview - A Web Based GUI library, based on a Qt WebEngine backend")
|
(define pkg-desc "racket-webview - A Web Based GUI library, based on a Qt WebEngine backend")
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
(define rkt-qt-version-major 0)
|
(define rkt-qt-version-major 0)
|
||||||
(define rkt-qt-version-minor 2)
|
(define rkt-qt-version-minor 2)
|
||||||
(define rkt-qt-version-patch (if (eq? (system-type 'os) 'windows) 2 1))
|
(define rkt-qt-version-patch 3)
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; Internal functions
|
;; Internal functions
|
||||||
@@ -164,7 +164,7 @@
|
|||||||
(let ((out (open-output-file version-file #:exists 'replace)))
|
(let ((out (open-output-file version-file #:exists 'replace)))
|
||||||
(write version out)
|
(write version out)
|
||||||
(close-output-port out)))
|
(close-output-port out)))
|
||||||
(displayln "Version file written; ready for FFI integration")
|
(displayln "Version file written; backend process is ready")
|
||||||
#t
|
#t
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
+816
-868
File diff suppressed because it is too large
Load Diff
@@ -35,10 +35,11 @@ This documentation is provided for version @bold{@version} of racket webview.
|
|||||||
Racket Webview is a class-oriented webview library built on top of a Qt-based
|
Racket Webview is a class-oriented webview library built on top of a Qt-based
|
||||||
native runtime.
|
native runtime.
|
||||||
|
|
||||||
The library is layered. At the lowest level a native FFI layer is used. On top
|
The library is layered. At the lowest level, Racket starts a separate Qt helper
|
||||||
of that, @racketmodname[racket-webview/racket-webview] provides a functional API. The
|
process and exchanges line-delimited JSON messages through stdin and stdout.
|
||||||
class-oriented API described in this manual is built from smaller modules on top
|
On top of that, @racketmodname[racket-webview/racket-webview] provides a
|
||||||
of that layer.
|
functional API. The class-oriented API described in this manual is built from
|
||||||
|
smaller modules on top of that layer.
|
||||||
|
|
||||||
@section{Core Concepts}
|
@section{Core Concepts}
|
||||||
|
|
||||||
|
|||||||
@@ -2,23 +2,24 @@
|
|||||||
|
|
||||||
@defmodule{racket-webview/racket-webview-qt}
|
@defmodule{racket-webview/racket-webview-qt}
|
||||||
|
|
||||||
@title{Racket FFI Interface for @tt{rktwebview_qt}}
|
@title{Racket Process Interface for @tt{rktwebview_prg}}
|
||||||
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
|
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
|
||||||
|
|
||||||
@section{Overview}
|
@section{Overview}
|
||||||
|
|
||||||
The module @tt{racket-webview-qt.rkt} provides a Racket FFI wrapper around the
|
The module @tt{racket-webview-qt.rkt} provides the low-level Racket interface to
|
||||||
native @tt{rktwebview_qt} library. It loads the shared library, initializes the
|
the Qt helper process @tt{rktwebview_prg}. It keeps the existing command numbers,
|
||||||
native runtime, and exposes Racket functions for creating and controlling
|
JSON command payloads, result symbols, event strings, and public Racket
|
||||||
webview windows.
|
procedures, but no longer loads a shared library through FFI.
|
||||||
|
|
||||||
If the Qt backend is available locally, it is loaded directly. Otherwise the
|
The helper process is started lazily when the first backend operation is
|
||||||
module attempts to resolve and download the backend. If that is not possible,
|
requested. Commands are written as one JSON object per line to the process
|
||||||
the module continues in a degraded mode in which a limited subset of the
|
stdin. Results and asynchronous events are read from stdout. Backend diagnostics
|
||||||
FFI entry points will only display a warning and perform a no-op. All others will fail.
|
are copied from stderr to the current Racket error port.
|
||||||
|
|
||||||
The wrapper translates the low-level C interface into a Racket-oriented API
|
Each command has a request identifier. The identifier is transport metadata and
|
||||||
based on structures, callbacks, and ordinary Racket values.
|
does not alter the existing command payload. It makes it possible to associate
|
||||||
|
a result with the correct caller when multiple Racket threads use the backend.
|
||||||
|
|
||||||
The module provides:
|
The module provides:
|
||||||
|
|
||||||
@@ -30,77 +31,35 @@ The module provides:
|
|||||||
@item{window geometry and visibility control}
|
@item{window geometry and visibility control}
|
||||||
@item{native dialogs}
|
@item{native dialogs}
|
||||||
@item{asynchronous event delivery}
|
@item{asynchronous event delivery}
|
||||||
@item{version and cleanup utilities}
|
@item{version and cleanup utilities}]
|
||||||
]
|
|
||||||
|
|
||||||
@section{Requirements}
|
|
||||||
|
|
||||||
The native backend requires Qt version @tt{6.10.2} or newer.
|
|
||||||
|
|
||||||
The shared library @tt{rktwebview_qt} must therefore be built against Qt
|
|
||||||
@tt{6.10.2} or a compatible later release.
|
|
||||||
|
|
||||||
Earlier Qt versions are not supported.
|
|
||||||
|
|
||||||
@section{Backend Availability}
|
@section{Backend Availability}
|
||||||
|
|
||||||
The module first checks whether the expected @tt{racket-webview-qt} backend is
|
The environment variable @tt{RKT_WEBVIEW_PRG} may name an explicit backend
|
||||||
already installed.
|
executable. This is useful during development and testing.
|
||||||
|
|
||||||
If it is not installed, the module attempts to resolve the configured download
|
Without that variable, the module uses the existing downloader and installation
|
||||||
site. If the site can be resolved and the configured archive is downloadable,
|
directory. Availability is checked when the first backend operation is made,
|
||||||
the backend is downloaded automatically.
|
not while the module is loaded. If the configured backend cannot be found or
|
||||||
|
downloaded, that operation raises an exception with the reason.
|
||||||
If the download site cannot be resolved, if no archive is available for the
|
|
||||||
current operating system and machine architecture, or if the download fails, the
|
|
||||||
module does not immediately abort module loading. Instead it switches to a
|
|
||||||
degraded mode in which native FFI loading is disabled.
|
|
||||||
|
|
||||||
In that degraded mode, a textual reason is stored internally and selected FFI
|
|
||||||
entry points are replaced by fallback implementations.
|
|
||||||
|
|
||||||
When the backend cannot be loaded, the module defines fallback implementations
|
|
||||||
for missing FFI entry points through @racket[define-ffi-definer] and
|
|
||||||
@racket[#:default-make-fail].
|
|
||||||
|
|
||||||
These fallbacks behave in two different ways.
|
|
||||||
|
|
||||||
For a small set of initialization and shutdown functions, a non-failing fallback
|
|
||||||
is installed:
|
|
||||||
|
|
||||||
@itemlist[#:style 'compact
|
|
||||||
@item{@racket[rkt_webview_env] returns @racket[#t]}
|
|
||||||
@item{@racket[rkt_webview_events_waiting] returns @racket[0]}
|
|
||||||
@item{@racket[rkt_webview_init] returns @racket[#t]}
|
|
||||||
@item{@racket[rkt_webview_cleanup] returns @racket[#t]}]
|
|
||||||
|
|
||||||
All other missing FFI functions raise an exception when called.
|
|
||||||
|
|
||||||
Fallback warnings are emitted at most once per function. If native loading was
|
|
||||||
disabled because the backend was unavailable, the warning message includes the
|
|
||||||
recorded reason. If native loading was enabled but a specific symbol could not
|
|
||||||
be loaded from the library, the error names the library file.
|
|
||||||
|
|
||||||
@section{Module Initialization}
|
@section{Module Initialization}
|
||||||
|
|
||||||
Loading the module performs several initialization steps automatically.
|
Starting the backend performs the following steps:
|
||||||
|
|
||||||
@itemlist[#:style 'compact
|
@itemlist[#:style 'compact
|
||||||
@item{determines the operating system and architecture}
|
@item{locates or downloads @tt{rktwebview_prg}}
|
||||||
@item{sets Qt runtime environment variables}
|
@item{sets the Qt runtime environment for the child process}
|
||||||
@item{loads the @tt{rktwebview_qt} shared library}
|
@item{starts the child with separate stdin, stdout, and stderr pipes}
|
||||||
@item{initializes the native runtime}
|
@item{starts a stdout protocol reader}
|
||||||
@item{starts a background thread that processes native events}
|
@item{starts a stderr forwarding thread}
|
||||||
]
|
@item{waits for the protocol-version handshake}
|
||||||
|
@item{starts an independent Racket event-dispatch thread}]
|
||||||
|
|
||||||
Currently the wrapper supports the following platforms:
|
Stdout is reserved for protocol records. Human-readable backend logging is
|
||||||
|
written only to stderr.
|
||||||
|
|
||||||
@itemlist[#:style 'compact
|
Currently the wrapper supports @tt{'linux} and @tt{'windows} release layouts.
|
||||||
@item{@tt{'linux}}
|
|
||||||
@item{@tt{'windows}}
|
|
||||||
]
|
|
||||||
|
|
||||||
If the current system is unsupported, loading the module raises an error.
|
|
||||||
|
|
||||||
@section{Data Model}
|
@section{Data Model}
|
||||||
|
|
||||||
@@ -200,8 +159,9 @@ Events generated by the native layer are delivered asynchronously through
|
|||||||
|
|
||||||
Requests that the webview window be closed.
|
Requests that the webview window be closed.
|
||||||
|
|
||||||
The wrapper forwards the request to the native backend and schedules cleanup of
|
The wrapper forwards the request to the helper process. The Qt-generated
|
||||||
the event-processing loop.
|
@tt{"closed"} event is delivered asynchronously, after which the Racket handle
|
||||||
|
is removed.
|
||||||
|
|
||||||
Returns @racket[#t].
|
Returns @racket[#t].
|
||||||
}
|
}
|
||||||
@@ -450,14 +410,14 @@ The wrapper does not parse the JSON payload.
|
|||||||
@defproc[(rkt-webview-version)
|
@defproc[(rkt-webview-version)
|
||||||
(list/c list? list?)]{
|
(list/c list? list?)]{
|
||||||
|
|
||||||
Returns version information for the native backend.
|
Returns the stdio protocol version and the expected Qt backend release.
|
||||||
|
|
||||||
Example result:
|
Example result:
|
||||||
|
|
||||||
@racketblock[
|
@racketblock[
|
||||||
(list
|
(list
|
||||||
(list 'webview-c-api 1 0 0)
|
(list 'webview-stdio-api 1 0 0)
|
||||||
(list 'qt 6 10 2))
|
(list 'racket-webview-qt 0 2 3))
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -482,14 +442,14 @@ Example result:
|
|||||||
|
|
||||||
@section{Summary}
|
@section{Summary}
|
||||||
|
|
||||||
The FFI module provides a thin Racket interface to the native
|
The module provides a thin Racket interface to the separate
|
||||||
@tt{rktwebview_qt} backend.
|
@tt{rktwebview_prg} process.
|
||||||
|
|
||||||
Key characteristics:
|
Key characteristics:
|
||||||
|
|
||||||
@itemlist[#:style 'compact
|
@itemlist[#:style 'compact
|
||||||
@item{thin wrapper around the native C API}
|
@item{line-delimited JSON over stdin and stdout}
|
||||||
@item{asynchronous event delivery}
|
@item{request identifiers for concurrent synchronous calls}
|
||||||
@item{JSON-based event payloads}
|
@item{asynchronous event delivery on a separate Racket thread}
|
||||||
@item{simple Racket structures for webviews}
|
@item{unchanged JSON event payloads}
|
||||||
]
|
@item{simple Racket structures for webviews}]
|
||||||
|
|||||||
@@ -589,7 +589,8 @@ Returns runtime information.
|
|||||||
@defproc[(webview-set-loglevel [l (or/c 'error 'warning 'info 'debug)])
|
@defproc[(webview-set-loglevel [l (or/c 'error 'warning 'info 'debug)])
|
||||||
void?]{
|
void?]{
|
||||||
|
|
||||||
Sets the native log level.
|
Sets the Qt backend log level.
|
||||||
|
|
||||||
The native log file path can be obtained via @racket[webview-info].
|
Backend diagnostics are written to stderr. The destination reported by
|
||||||
|
@racket[webview-info] is @tt{"stderr"}.
|
||||||
}
|
}
|
||||||
@@ -6,9 +6,10 @@
|
|||||||
|
|
||||||
@title{C API for Racket Integration}
|
@title{C API for Racket Integration}
|
||||||
|
|
||||||
This section describes the C API exactly as defined in @tt{rktwebview.h} and
|
This section documents the legacy C API defined in @tt{rktwebview.h} and
|
||||||
@tt{rktwebview_types.h}. The API is used from Racket through FFI, but is
|
@tt{rktwebview_types.h}. The source remains in the backend repository for
|
||||||
documented here in its native C/C++ form.
|
reference and comparison, but the current build no longer exposes this shared
|
||||||
|
library to Racket. The active integration uses the stdio process protocol.
|
||||||
|
|
||||||
The interface is deliberately small. Handles are integers, most operations
|
The interface is deliberately small. Handles are integers, most operations
|
||||||
return a @tt{result_t}, and structured values are returned as @tt{rkt_data_t *}.
|
return a @tt{result_t}, and structured values are returned as @tt{rkt_data_t *}.
|
||||||
|
|||||||
@@ -1,265 +1,114 @@
|
|||||||
#lang scribble/manual
|
#lang scribble/manual
|
||||||
@(require racket/runtime-path)
|
|
||||||
|
|
||||||
@defmodule{racket-webview/internals}
|
@defmodule{racket-webview/internals}
|
||||||
|
|
||||||
@title{Qt WebView Backend Architecture}
|
@title{Qt WebView Backend Architecture}
|
||||||
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
|
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
|
||||||
|
|
||||||
@section{Introduction}
|
|
||||||
|
|
||||||
It would, of course, be preferable to place everything within a single process,
|
|
||||||
under a unified structure. This would be elegant. It is not how things are.
|
|
||||||
|
|
||||||
@(define-runtime-path img-path ".")
|
|
||||||
@(define img (path->string (build-path img-path
|
|
||||||
"rktwebview-shared-memory-diagram-simple.svg"
|
|
||||||
)))
|
|
||||||
@;@(displayln (format "image: '~a'" img))
|
|
||||||
|
|
||||||
@centered{
|
|
||||||
@image[#:scale 0.45]{@img}
|
|
||||||
}
|
|
||||||
|
|
||||||
Qt WebEngine establishes its own order: threads, event loops, internal state.
|
|
||||||
Once set in motion, it does not easily yield. It persists, and it expects its
|
|
||||||
environment to adapt accordingly. These conditions are accepted.
|
|
||||||
|
|
||||||
The Racket process, however, is of a different nature. It is light, precise,
|
|
||||||
capable of starting and stopping without residue. It must remain so.
|
|
||||||
|
|
||||||
So a boundary is drawn.
|
|
||||||
|
|
||||||
On one side, Qt: a large, immovable instrument—something like an organ. Once it
|
|
||||||
begins to sound, it fills the space, and it is not easily silenced. On the other,
|
|
||||||
Racket: a violin, agile and expressive, able to begin and end a phrase at will.
|
|
||||||
|
|
||||||
They do not become the same instrument. They are allowed to play together.
|
|
||||||
Communication is arranged accordingly. A shared memory region, containing three
|
|
||||||
queues: commands, results, and events. A command is issued. It crosses the boundary. It is taken up and executed. A result returns.
|
|
||||||
Events also arise, independently, and must be handled when they appear.
|
|
||||||
|
|
||||||
Within this structure, the violin may move freely—provided it does not attempt to
|
|
||||||
reconfigure the organ. No attempt is made to unify the instruments. Such efforts would not improve the music. Instead, the composition is written so that each plays its part.
|
|
||||||
|
|
||||||
From the outside, one hears only a simple exchange: a call, a response. Internally, the balance is carefully maintained. For now, this is sufficient. And it holds.
|
|
||||||
|
|
||||||
@section{Overview}
|
@section{Overview}
|
||||||
|
|
||||||
This backend provides a webview implementation by delegating all GUI and browser
|
The runtime consists of two processes. The Racket process owns the public API,
|
||||||
functionality to a separate Qt process.
|
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 embedding Racket process does not manipulate Qt widgets directly. Instead,
|
The processes communicate through standard pipes. There is no shared library,
|
||||||
it communicates with a helper process that owns the Qt event loop and all
|
shared-memory queue, or callback from native code into the Racket runtime.
|
||||||
@tt{QWebEngine} objects.
|
|
||||||
|
|
||||||
This design exists to work around limitations of Qt WebEngine in combination with
|
@section{Standard Streams}
|
||||||
the lifecycle model of the DrRacket environment.
|
|
||||||
|
|
||||||
@section{Execution Model}
|
The streams have fixed roles:
|
||||||
|
|
||||||
The runtime consists of two processes: the embedding Racket process and a helper
|
@itemlist[#:style 'compact
|
||||||
process running Qt and Qt WebEngine.
|
@item{child stdin carries commands from Racket}
|
||||||
|
@item{child stdout carries the handshake, command results, and events}
|
||||||
|
@item{child stderr carries diagnostic logging}]
|
||||||
|
|
||||||
All GUI state lives in the helper process. The embedding side holds no direct
|
Messages on stdin and stdout are compact UTF-8 JSON objects, one object per
|
||||||
references to Qt objects. Communication is explicit and happens through shared
|
line. Stdout must not contain ordinary logging because the Racket protocol
|
||||||
memory.
|
reader treats every value there as a protocol record.
|
||||||
|
|
||||||
@section{Shared Memory and Queues}
|
@section{Protocol Compatibility}
|
||||||
|
|
||||||
A shared memory region is created during initialization. Inside that region,
|
The command identifiers in @tt{rkt_protocol.h} and the JSON objects used as
|
||||||
three FIFO queues are established: a command queue, a result queue, and an event
|
command payloads are unchanged. The process transport adds an outer message
|
||||||
queue.
|
object with a type and, for commands and results, a request identifier.
|
||||||
|
|
||||||
Each message consists of a numeric code and a payload, typically JSON:
|
A command has this form:
|
||||||
|
|
||||||
@centerline{@tt{(code, payload)}}
|
@verbatim|{{"type":"command","id":17,"command":6,"data":{"wv":1,"url":"https://example.test"}}}|
|
||||||
|
|
||||||
The queues have distinct roles. The @italic{command queue} carries requests from the embedding process to the Qt process, for example creating a window, loading a URL, or executing JavaScript. The @italic{result queue} carries direct replies to those commands. A synchronous call on the embedding side blocks until a corresponding result is available. The @italic{event queue} carries asynchronous notifications generated by the Qt side, such as page load completion, navigation requests, window movement, or events
|
A corresponding result has this form:
|
||||||
originating from JavaScript.
|
|
||||||
|
@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{Command Execution}
|
@section{Command Execution}
|
||||||
|
|
||||||
A function call on the embedding side is translated into a command and written to
|
A Racket caller registers a private response channel, writes a command while
|
||||||
the command queue.
|
holding the transport write lock, and waits on that channel.
|
||||||
|
|
||||||
From there the flow is fixed: (1) the command is read by a worker thread in the
|
The Qt process has a command-reader thread. That thread parses stdin records and
|
||||||
helper process, (2) it is reposted onto the Qt GUI thread, (3) the GUI thread
|
calls the existing @tt{Rktwebview_qt} methods. Those methods post commands to the
|
||||||
executes the operation, and (4) the result is written back to the result queue.
|
Qt GUI thread and wait for completion. The command-reader thread then writes the
|
||||||
|
result record to stdout.
|
||||||
|
|
||||||
The worker thread never manipulates Qt objects. All GUI work happens on the GUI
|
The Racket stdout reader is the only thread that reads protocol output. It uses
|
||||||
thread.
|
the request identifier to forward each result to the correct waiting caller.
|
||||||
|
|
||||||
From the caller’s perspective, a synchronous call returns only after the GUI
|
|
||||||
thread has completed the action.
|
|
||||||
|
|
||||||
@section{Event Delivery}
|
@section{Event Delivery}
|
||||||
|
|
||||||
Many relevant events are not tied to a specific command. Page loading, navigation
|
Qt events remain the original JSON strings. The Qt callback converts them only
|
||||||
attempts, window movement, and JavaScript-originated events are delivered through
|
into this transport record:
|
||||||
the event queue.
|
|
||||||
|
|
||||||
Events are retrieved explicitly by polling.
|
@verbatim|{{"type":"event","wv":1,"data":"{...original event JSON...}"}}|
|
||||||
|
|
||||||
Each event contains a name, an identifier, and optional fields depending on its
|
The stdout reader captures the callback associated with the handle and places
|
||||||
type. Events are delivered in FIFO order.
|
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.
|
||||||
|
|
||||||
@section{Contexts and Webviews}
|
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.
|
||||||
|
|
||||||
The backend uses a two-level model consisting of contexts and webviews.
|
@section{Contexts and Handles}
|
||||||
|
|
||||||
A context represents a browser environment and corresponds to a
|
A context corresponds to a @tt{QWebEngineProfile} and is identified by an
|
||||||
@tt{QWebEngineProfile}. It defines how pages run, including injected scripts and optional trust configuration using explicitly trusted self-signed certificates.
|
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.
|
||||||
|
|
||||||
Each context is identified by an integer handle.
|
The existing Racket @tt{rkt-wv} structure is retained so the higher layers do
|
||||||
|
not need to change.
|
||||||
|
|
||||||
Within a context, one or more webviews can be created. A webview represents a
|
@section{Lifecycle and Failure Handling}
|
||||||
window containing a browser view. Webviews are also identified by integer
|
|
||||||
handles.
|
|
||||||
|
|
||||||
A webview always belongs to exactly one context. When creating a webview, the
|
The backend writes a protocol-version handshake after initialization. Racket
|
||||||
context handle must be provided.
|
waits for this handshake before sending application commands.
|
||||||
|
|
||||||
Webviews may optionally have a parent webview. If a parent is specified, the
|
For normal shutdown, Racket closes open handles, sends the existing quit
|
||||||
resulting window is created as a modal child of that parent; otherwise it is
|
command, waits for its result, closes the child's stdin, and waits for the child
|
||||||
created as a top-level window.
|
process and reader threads.
|
||||||
|
|
||||||
From the Racket side, this means that a context must be created first. That
|
If Racket terminates unexpectedly, the operating system closes the stdin pipe.
|
||||||
context handle is then used to create webviews, which are subsequently addressed
|
The command-reader thread sees EOF, closes all Qt windows, and quits the Qt event
|
||||||
through their own handles.
|
loop. This replaces the former shared-memory alive and alive-ack queues.
|
||||||
|
|
||||||
All Qt objects remain internal to the helper process; only these integer handles
|
If stdout closes unexpectedly, Racket marks all handles invalid and delivers an
|
||||||
cross the process boundary.
|
exception to every command still waiting for a result.
|
||||||
|
|
||||||
@section{JavaScript Bridge}
|
@section{Logging}
|
||||||
|
|
||||||
Each context installs a small JavaScript bridge into every page, allowing
|
The Qt backend writes its existing log messages to stderr. Racket forwards that
|
||||||
JavaScript code to send structured data to the host via:
|
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.
|
||||||
|
|
||||||
@centerline{@tt{window.rkt_send_event(obj)}}
|
@section{Development Override}
|
||||||
|
|
||||||
The objects are collected and forwarded to the event queue.
|
Setting @tt{RKT_WEBVIEW_PRG} to a backend executable bypasses the installed
|
||||||
|
release directory. This makes it possible to test a newly compiled backend
|
||||||
@section{Navigation and Window Behavior}
|
without repackaging or changing the downloader first.
|
||||||
|
|
||||||
User actions are not always executed immediately; navigation initiated by the
|
|
||||||
user may result in a @tt{"navigation-request"} event instead of being followed
|
|
||||||
automatically, and closing a window may result in a @tt{"can-close?"} event. The
|
|
||||||
Racket side is expected to decide how to handle these situations.
|
|
||||||
|
|
||||||
@section{Design Considerations}
|
|
||||||
|
|
||||||
@bold{Qt WebEngine lifecycle.}
|
|
||||||
Qt WebEngine cannot be safely reinitialized within a single process.
|
|
||||||
|
|
||||||
In practice, once a @tt{QApplication} using WebEngine has been started and shut
|
|
||||||
down, the WebEngine runtime cannot be started again. This is a known and
|
|
||||||
documented limitation (see for example QTBUG-70519, QTBUG-87460,
|
|
||||||
QTBUG-145033). The underlying cause is that WebEngine starts internal threads
|
|
||||||
and resources that are not fully released, even after application shutdown.
|
|
||||||
|
|
||||||
Attempts to reinitialize WebEngine in the same process result in undefined
|
|
||||||
behavior, including crashes, hangs, or inconsistent state.
|
|
||||||
|
|
||||||
In the DrRacket environment, where components may be restarted under a
|
|
||||||
custodian, this makes an in-process design fundamentally unsuitable. Even when
|
|
||||||
libraries are loaded and unloaded using @tt{#:custodian}, the WebEngine runtime
|
|
||||||
cannot be reset to a clean state.
|
|
||||||
|
|
||||||
By moving Qt and WebEngine into a separate process, this limitation is avoided
|
|
||||||
entirely: each start of the backend creates a fresh runtime, and terminating the
|
|
||||||
helper process guarantees that all associated threads and resources are released
|
|
||||||
by the operating system.
|
|
||||||
|
|
||||||
@bold{Event loop and threading.}
|
|
||||||
Qt requires that GUI operations are performed on the Qt GUI thread.
|
|
||||||
|
|
||||||
Instead of attempting to integrate Qt’s event loop with Racket, the design
|
|
||||||
isolates Qt completely and runs it in its own process.
|
|
||||||
|
|
||||||
Within that process, a worker thread receives commands and forwards them to the
|
|
||||||
GUI thread using Qt’s event mechanism (via @tt{postEvent}). The Racket side never
|
|
||||||
interacts with Qt objects directly.
|
|
||||||
|
|
||||||
@bold{Failure isolation.}
|
|
||||||
Qt WebEngine is a large subsystem with its own internal processes (including the
|
|
||||||
Chromium-based @tt{QtWebEngineProcess}) and is generally stable in practice.
|
|
||||||
|
|
||||||
Running the Qt side in a separate process provides isolation: if the helper
|
|
||||||
process terminates, the embedding Racket process remains unaffected and can
|
|
||||||
decide how to recover.
|
|
||||||
|
|
||||||
@bold{Shared memory communication.}
|
|
||||||
The communication pattern consists of commands, results, and events, mapped onto
|
|
||||||
shared memory FIFO queues, keeping the model simple and explicit.
|
|
||||||
|
|
||||||
@bold{JSON encoding.}
|
|
||||||
All payloads are encoded as JSON, providing a natural bridge between JavaScript,
|
|
||||||
Qt/C++, and Racket: JavaScript produces JSON natively, Qt maps it to variant
|
|
||||||
types, and Racket can decode it easily.
|
|
||||||
|
|
||||||
For control commands, payload sizes are small and infrequent, so serialization
|
|
||||||
cost is negligible compared to GUI-thread execution and WebEngine processing; for
|
|
||||||
dynamic data such as JavaScript results and custom events, JSON is the
|
|
||||||
appropriate representation. A binary protocol would reduce overhead but increase
|
|
||||||
complexity and reduce inspectability.
|
|
||||||
|
|
||||||
@section{Shared Memory Architecture}
|
|
||||||
|
|
||||||
Communication between the Racket process and @tt{rktwebview_prg} is implemented
|
|
||||||
using a shared memory region. This region serves three purposes at once: it
|
|
||||||
stores shared data structures, it provides a simple allocator, and it hosts the
|
|
||||||
FIFO queues used for message passing.
|
|
||||||
|
|
||||||
At the start of the shared memory block, a small administration area is stored,
|
|
||||||
including a pointer to the current end of allocated memory, a list of active
|
|
||||||
allocations, a free list, and a fixed slot table. The slot table acts as a
|
|
||||||
directory of shared objects; queues are created once, stored in slots, and can be
|
|
||||||
retrieved by both processes using only the slot number.
|
|
||||||
|
|
||||||
Memory allocation inside the shared block is intentionally simple. Each
|
|
||||||
allocation is preceded by a small header containing size information and links
|
|
||||||
for a double-linked list. Allocation first attempts to reuse a block from the
|
|
||||||
free list; if no suitable block is available, memory is taken from the unused
|
|
||||||
tail of the region. Freed blocks are returned to the free list and may later be
|
|
||||||
reused. Blocks are not compacted or coalesced. This is not a general-purpose
|
|
||||||
heap; it is a small, predictable allocator for queue items and payload strings.
|
|
||||||
|
|
||||||
Shared objects are referenced primarily through offsets (via @tt{ShmPlace})
|
|
||||||
rather than raw pointers. This makes the layout independent of the virtual
|
|
||||||
address at which the shared memory is mapped in each process.
|
|
||||||
|
|
||||||
Queues are built directly on top of this allocator. Each queue consists of a
|
|
||||||
small header containing the first item, the last item, and a count, followed by a
|
|
||||||
linked list of queue items. Each item stores a numeric command or event code, a
|
|
||||||
pointer to its payload in shared memory, and links to neighboring items.
|
|
||||||
|
|
||||||
Synchronization is split into two layers. A shared lock protects allocator and
|
|
||||||
queue metadata, while each queue has its own semaphore indicating whether items
|
|
||||||
are available. One mechanism protects the structure; the other tells you whether
|
|
||||||
there is anything worth reading.
|
|
||||||
|
|
||||||
On POSIX systems such as Linux, shared memory is implemented using
|
|
||||||
@tt{shm_open}, @tt{ftruncate}, and @tt{mmap}, with synchronization via named
|
|
||||||
POSIX semaphores created using @tt{sem_open}. The owner process initializes these
|
|
||||||
objects and removes them again using @tt{shm_unlink} and @tt{sem_unlink}.
|
|
||||||
|
|
||||||
On Windows, the same model is implemented using @tt{CreateFileMappingA} and
|
|
||||||
@tt{MapViewOfFile} for shared memory, and @tt{CreateSemaphoreA} or
|
|
||||||
@tt{OpenSemaphoreA} for synchronization. The design is identical, but the kernel
|
|
||||||
objects follow the Windows lifetime model and are released when the last handle
|
|
||||||
is closed.
|
|
||||||
|
|
||||||
The shared memory region has a fixed size (currently 10MB) and is not resized at
|
|
||||||
runtime. Although the use of @tt{ShmPlace} offsets would in principle allow
|
|
||||||
relocation, resizing would require coordinated remapping in both processes while
|
|
||||||
all activity is paused. The current design therefore treats the region as
|
|
||||||
fixed-size and relies on reuse of freed blocks.
|
|
||||||
|
|
||||||
This implies that the communication channel is bounded. Payloads such as
|
|
||||||
@tt{set_html} or large JavaScript results must fit within the available free
|
|
||||||
space in the shared memory block. In practice, the usable limit is somewhat below
|
|
||||||
the nominal 10MB due to allocator overhead, queue administration, and concurrent
|
|
||||||
messages.
|
|
||||||
|
|
||||||
This is a message transport, not an infinite sack of HTML.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user