Omzetting naar stdio.

This commit is contained in:
2026-08-03 08:20:56 +02:00
parent 93453c351f
commit 1ecb678e38
9 changed files with 989 additions and 1218 deletions
+18 -7
View File
@@ -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.
+9 -9
View File
@@ -1,8 +1,8 @@
#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")
@@ -13,13 +13,13 @@
) )
(define deps (define deps
'("racket/base" '("racket/base"
"http-easy" "http-easy"
"gregor" "gregor"
"racket/gui" "racket/gui"
"html-printer-lib" "html-printer-lib"
"racket/net" "racket/net"
"simple-ini" "simple-ini"
"gregor-utils" "gregor-utils"
"racket-sprintf" "racket-sprintf"
"lru-cache" "lru-cache"
+2 -2
View File
@@ -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
) )
) )
+832 -884
View File
File diff suppressed because it is too large Load Diff
+5 -4
View File
@@ -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}
+44 -84
View File
@@ -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].
} }
@@ -370,7 +330,7 @@ Opens the browser developer tools window.
@section{Native Dialogs} @section{Native Dialogs}
Dialog functions return immediately with a status code. Dialog functions return immediately with a status code.
The users choice is delivered asynchronously through the event callback. The users choice is delivered asynchronously through the event callback.
@defproc[(rkt-webview-choose-dir @defproc[(rkt-webview-choose-dir
@@ -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}]
+3 -2
View File
@@ -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"}.
} }
+4 -3
View File
@@ -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 *}.
+72 -223
View File
@@ -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 callers 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 Qts 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 Qts 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.