This commit is contained in:
2026-03-09 09:28:46 +01:00
parent 0ec5a42c71
commit a078022401
7 changed files with 673 additions and 19 deletions

348
scrbl/rktwebview-api.scrbl Normal file
View File

@@ -0,0 +1,348 @@
#lang scribble/manual
@title{Public API Specification for the QtBacked WebView Bridge}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@italic{For use with Racket FFI}
This document defines the public C-level API of the Qt-based WebView bridge.
It targets Racket developers integrating the C library via the Racket FFI.
It specifies the interface, result values, memory ownership rules, event formats,
expected behavior, and recommended calling patterns. Internal architecture is documented separately.
@section{Calling Convention and Structure}
All functions are C functions with normal C linkage.
Window handles are simple integers that uniquely identify native windows.
@itemlist[#:style 'compact
@item{Header: @tt{rktwebview.h}}
@item{ABI: plain C, no name mangling}
@item{Handles remain valid from creation until the window is fully closed}
]
@section{Opaque and Helper Types}
@subsection{Window Handle}
@itemlist[#:style 'compact
@item{@tt{typedef int rktwebview_t}: Valid for any window created by @tt{rkt_webview_create} until closed.}
]
@subsection{Event Callback Types}
@itemlist[#:style 'compact
@item{@tt{typedef struct \{ rktwebview_t wv; char* event; \} rkt_event_t}: JSON event payload for the host (UTF8).}
@item{@tt{typedef void (*event_cb_t)(rkt_event_t*)}: Host-supplied callback for event delivery.}
]
@subsection{JS Result Structure}
@itemlist[#:style 'compact
@item{@tt{typedef struct \{ result_t result; char* value; \} rkt_js_result_t}: @tt{result} is always @tt{oke} or @tt{eval_js_failed}; @tt{value} holds a UTF8 JSON string describing the JS outcome.}
]
@section{Enumerations}
@subsection{result_t}
@itemlist[#:style 'compact
@item{@tt{oke = 0}}
@item{@tt{set_html_failed = 1}}
@item{@tt{set_navigate_failed = 2}}
@item{@tt{eval_js_failed = 3}}
@item{@tt{move_failed = 12}}
@item{@tt{resize_failed = 13}}
@item{@tt{choose_dir_failed = 14}}
@item{@tt{open_file_failed = 15}}
@item{@tt{save_file_failed = 16}}
@item{@tt{failed = 17}}
]
@subsection{window_state_t}
@itemlist[#:style 'compact
@item{@tt{invalid = -1}}
@item{@tt{normal = 0}}
@item{@tt{minimized = 1}}
@item{@tt{maximized = 2}}
@item{@tt{hidden = 3}}
@item{@tt{normal_active = 16}}
@item{@tt{maximized_active = 18}}
]
@section{Threading Model}
@itemlist[#:style 'compact
@item{All GUI work occurs on the Qt GUI thread.}
@item{Public calls block until the posted internal command has completed.}
@item{The host is expected to drive Qt by calling @tt{rkt_webview_process_events}.}
]
@section{Memory Ownership}
@itemlist[#:style 'compact
@item{@tt{rkt_event_t*} must be freed using @tt{rkt_webview_destroy_event}.}
@item{@tt{rkt_js_result_t*} must be freed using @tt{rkt_webview_destroy_js_result}.}
@item{All returned strings are UTF8 and owned by the library until destroyed with the corresponding function.}
]
@section{Function Reference (C signatures)}
The following are literal C signatures with behavioral descriptions.
@subsection{Initialization and Event Pumping}
@codeblock{
void rkt_webview_init(void);
}
Initializes Qt and internal state. Safe to call multiple times (subsequent calls are no-ops).
@codeblock{
void rkt_webview_process_events(int for_ms);
}
Pumps the Qt event loop for approximately @tt{for_ms} milliseconds.
Idea for implementation in racket:
Use a @emph{cooperative thread} with @emph{time slicing}. A practical starting point:
@itemlist[#:style 'compact
@item{~3 ms Qt pumping per iteration: @tt{(rkt_webview_process_events 3)}}
@item{~10 ms Racket time (scheduler): @tt{(sleep 0.01)}}
]
Tune these values for your application.
Example:
@verbatim|{
(define running? (box #t))
(define (pump-loop)
(let loop ()
(when (unbox running?)
(rkt_webview_process_events 3) ; ~3 ms Qt
(sleep 0.01) ; ~10 ms for Racket scheduler
(loop))))
(define pump-thread (thread pump-loop))
;; ... later on shutdown:
(set-box! running? #f)
(thread-wait pump-thread)
}|
@subsection{Window Lifecycle}
@codeblock{
rktwebview_t rkt_webview_create(rktwebview_t parent, event_cb_t cb);
}
Creates a window, optionally modal if @tt{parent} exists. Registers @tt{cb}.
The window is shown before returning.
@codeblock{
void rkt_webview_close(rktwebview_t wv);
}
Requests closure of @tt{wv}.
Behavior:
@itemlist[#:style 'compact
@item{If the user tries to close via the window manager (close button, AltF4, etc.), the library intercepts it and emits @tt{"can-close?"}. The window remains open.}
@item{To actually close after receiving @tt{"can-close?"}, the host must call @tt{rkt_webview_close}.}
@item{When the final close occurs, @tt{"closed"} is emitted and the handle becomes invalid.}
]
@codeblock{
bool rkt_webview_valid(rktwebview_t wv);
}
Returns whether @tt{wv} refers to an active window.
@subsection{Navigation and Content}
@codeblock{
result_t rkt_webview_set_url(rktwebview_t wv, const char* url);
}
Navigate to @tt{url}. Returns @tt{oke} or @tt{set_navigate_failed}.
@codeblock{
result_t rkt_webview_set_html(rktwebview_t wv, const char* html);
}
Set page content from @tt{html}. Returns @tt{oke} or @tt{set_html_failed}.
@codeblock{
result_t rkt_webview_set_title(rktwebview_t wv, const char* title);
}
Set the native window title. Returns @tt{oke} or @tt{failed}.
@subsection{JavaScript Execution}
@codeblock{
result_t rkt_webview_run_js(rktwebview_t wv, const char* js);
}
Fire-and-forget JavaScript execution. Returns @tt{oke} or @tt{eval_js_failed}.
@codeblock{
rkt_js_result_t* rkt_webview_call_js(rktwebview_t wv, const char* js);
}
Synchronous JS call. Guarantees:
@itemlist[#:style 'compact
@item{@tt{result} is @tt{oke} or @tt{eval_js_failed}.}
@item{@tt{value} is JSON: @tt{{"oke": <bool>, "result": <value-or-false>, "exn": <string-or-false>}}.}
]
Must be freed with @tt{rkt_webview_destroy_js_result()}.
@codeblock{
result_t rkt_webview_destroy_js_result(rkt_js_result_t* r);
}
Frees JS result memory. Returns @tt{oke}.
@subsection{Developer Tools}
@codeblock{
result_t rkt_webview_open_devtools(rktwebview_t wv);
}
Open devtools. Returns @tt{oke} or @tt{no_devtools_on_platform} (platform-dependent).
@subsection{Window Geometry and Visibility}
@codeblock{
result_t rkt_webview_move(rktwebview_t wv, int x, int y);
}
Waits until move is acknowledged. Returns @tt{oke} or @tt{move_failed}.
@codeblock{
result_t rkt_webview_resize(rktwebview_t wv, int w, int h);
}
Waits until resize is acknowledged. Returns @tt{oke} or @tt{resize_failed}.
@codeblock{
result_t rkt_webview_hide(rktwebview_t wv);
}
@codeblock{
result_t rkt_webview_show(rktwebview_t wv);
}
@codeblock{
result_t rkt_webview_show_normal(rktwebview_t wv);
}
@codeblock{
result_t rkt_webview_present(rktwebview_t wv);
}
@codeblock{
result_t rkt_webview_maximize(rktwebview_t wv);
}
@codeblock{
result_t rkt_webview_minimize(rktwebview_t wv);
}
All return @tt{oke} or @tt{failed} (when the operation cannot be applied).
@codeblock{
window_state_t rkt_webview_window_state(rktwebview_t wv);
}
Returns a simplified window state.
@subsection{Event & Dialog Systems}
@codeblock{
result_t rkt_webview_destroy_event(rkt_event_t* e);
}
Frees event memory. Returns @tt{oke}.
@subsection{Native Dialogs}
@codeblock{
rkt_js_result_t* rkt_webview_choose_dir(rktwebview_t wv, const char* title, const char* base_dir);
}
Returns JSON:
@itemlist[#:style 'compact
@item{@tt{"state":"choosen","dir":"..."}}
@item{@tt{"state":"canceled","dir":"..."}}
]
Must be freed with @tt{rkt_webview_destroy_js_result}.
@codeblock{
rkt_js_result_t* rkt_webview_file_open(rktwebview_t wv, const char* title, const char* base_dir, const char* permitted_exts);
}
@codeblock{
rkt_js_result_t* rkt_webview_file_save(rktwebview_t wv, const char* title, const char* base_dir, const char* permitted_exts);
}
Return JSON:
@itemlist[#:style 'compact
@item{@tt{"state":"choosen","file":"...","used-filter":"..."}}
@item{@tt{"state":"canceled","file":"","used-filter":"..."}}
]
Must be freed with @tt{rkt_webview_destroy_js_result}.
@subsection{Security}
@codeblock{
void rkt_webview_set_ou_token(rktwebview_t wv, const char* token);
}
This function configures a per-window "OU acceptance token" that affects how
certificate errors are handled inside the internal @tt{QWebEnginePage}.
When a TLS certificate error occurs, the internal page examines the certificate
chain. If all the following conditions are met:
@itemlist[#:style 'compact
@item{the certificate is self-signed,}
@item{the certificate contains an Organizational Unit (OU) field,}
@item{the OU value exactly matches the token previously set with @tt{rkt_webview_set_ou_token},}
]
then the certificate is programmatically accepted and the load is allowed to
continue.
This mechanism is intended for controlled environments (e.g., local development
servers or private deployments) where a known self-signed certificate is used
and its OU value is part of the trust boundary.
Note: starting with Qt 6.10, @tt{QWebEnginePage} introduces an API for providing
a specific certificate to be accepted directly by the page. However, this API is
very new ("bleeding edge") and not yet used or relied upon in this WebView
bridge.
@section{Event JSON Format}
@itemlist[#:style 'compact
@item{@tt{"event":"page-loaded","oke": <bool>}}
@item{@tt{"event":"show"}}
@item{@tt{"event":"hide"}}
@item{@tt{"event":"move","x": <int>,"y": <int>}}
@item{@tt{"event":"resize","w": <int>,"h": <int>}}
@item{@tt{"event":"can-close?"}}
@item{@tt{"event":"closed"}}
@item{@tt{"event":"js-evt","js-evt": <object>}}
]
@section{Typical Usage Pattern}
@itemlist[#:style 'compact
@item{Initialize once.}
@item{Create windows.}
@item{Run a cooperative time-sliced pump loop (e.g., ~3 ms Qt, ~10 ms Racket).}
@item{Invoke JS run/call functions.}
@item{Handle and free events.}
@item{Close windows explicitly.}
]
@section{Summary}
This API provides a stable, simple interface for embedding a Qt WebEngine
webview inside a Racket program via FFI. The API is synchronous, uses compact
JSON for results, and defines clear memory ownership rules.
``