Native shells
The view is identical everywhere because it is HTML. Only the shell differs — the same one-interface / several-backends split U already uses for GPU (Dawn vs browser WebGPU) and sockets (epoll vs WASI poll).
Status: designed, not built. No shell exists yet. This page is the plan, written down before the code so the platform differences are known rather than discovered.
The compatibility story is much better than "five platforms" suggests. WebKit powers WKWebView and WebKitGTK; Chromium powers WebView2 and Android's WebView. Both are modern, both run ES6+, and both run the Qbix JS runtime unchanged.
| Target | WebView | Engine | Binding | Difficulty |
|---|---|---|---|---|
| Web | the page itself | — | none — U already targets WASM | trivial |
| Linux | WebKitGTK | WebKit | plain C | easy |
| macOS | WKWebView | WebKit | Objective-C | easy |
| iOS | WKWebView | WebKit | same shim as macOS | easy |
| Windows | WebView2 | Chromium | COM | moderate |
| Android | android.webkit | Chromium | JNI → Java | hardest |
Calling into a webview is asynchronous — the answer arrives later, from outside the process. That is exactly what a GPU readback is and what an HTTP response is, so it needs no new concurrency concept: it is one more thing the fiber scheduler waits on.
d WebView title: S width: I height: I f open(cfg: WebView +R) -> WebView +R ! NetworkError f load(view: WebView +R, page: HTML) -> N +A f eval(view: WebView +R, script: JS) -> S +A f on(view: WebView +R, channel: S) -> [Message] +R
Note eval takes a JS value, not a string — so a value
crossing into the page has already been encoded, and the
</script> and U+2028 hazards are handled before it leaves U.
Each host exposes a different JS→native channel. U emits a small shim that normalises them, so U-side code sees one interface. The WebKit family shares an identical JS call, which is why there are three variants rather than five.
| Host | JS side | Native side |
|---|---|---|
| WebKitGTK | window.webkit.messageHandlers.u.postMessage(m) | webkit_user_content_manager_register_script_message_handler |
| WKWebView | window.webkit.messageHandlers.u.postMessage(m) | WKScriptMessageHandler |
| WebView2 | window.chrome.webview.postMessage(m) | add_WebMessageReceived |
| Android | window.u.post(m) | addJavascriptInterface + @JavascriptInterface |
Native→JS is uniform in shape everywhere — "evaluate this script,
deliver the result later" — which is why it maps onto +A without
special cases: webkit_web_view_evaluate_javascript,
evaluateJavaScript:completionHandler:, ExecuteScript,
evaluateJavascript.
window.U = window.U || {};
U.post = (window.webkit && webkit.messageHandlers && webkit.messageHandlers.u)
? function (m) { webkit.messageHandlers.u.postMessage(m); }
: (window.chrome && chrome.webview)
? function (m) { chrome.webview.postMessage(m); }
: function (m) { window.u.post(JSON.stringify(m)); };Messages are JSON both ways. That is not laziness: Android's
addJavascriptInterface only passes primitives and strings reliably,
so JSON is the honest lowest common denominator across all four.
This is the one place the concurrency model has to grow to meet the GUI, and it is worth knowing before starting rather than discovering in the Android shell.
| Host | Requirement |
|---|---|
| WKWebView | all calls on the main thread |
| Android | WebView methods AND evaluateJavascript on the UI thread |
| WebView2 | the COM apartment that created it; needs the message pump |
| WebKitGTK | the GTK main loop |
So the scheduler needs a post-to-UI-thread queue alongside the
epoll reactor. Fibers keep running on the reactor; anything touching the view is
handed to the UI queue and its result comes back as a +A
completion.
Markup, CSS and JS have to reach the view. file:// is simplest and
wrong: it carries origin restrictions that break fetch and
XMLHttpRequest, which is exactly what a Qbix front end uses. The
answer is a custom scheme handler, which all four hosts
support.
| Host | Mechanism |
|---|---|
| WKWebView | WKURLSchemeHandler |
| WebKitGTK | webkit_web_context_register_uri_scheme |
| WebView2 | AddWebResourceRequestedFilter + WebResourceRequested |
| Android | WebViewClient.shouldInterceptRequest |
Assets are then served from memory under a real origin (say
app://local/), so relative URLs, fetch and cookies all
behave as they would on a server.
Both engines are modern, so the Qbix JS runtime loads and runs unmodified. Nothing about tools, templates or events needs a native variant.
| Qbix | Inside a U WebView |
|---|---|
Q.activate(el) | unchanged — U emits the tool markup, Q activates it |
Q.Tool.define | unchanged |
Q.Template.render | unchanged; Handlebars`…` exports the same template |
| delegated namespaced events | unchanged — and U's on* prohibition enforces the convention |
Q.Tool.clear before innerHTML | still required |
Q.req / Q.request | works over the custom scheme; or routes to the bridge |
That last row is the interesting one. A Qbix tool normally reaches the server over HTTP. Inside a U shell the same call can be routed to the bridge instead — the request never leaves the process, U handles it directly, and the tool's code does not change. Same front end, no network, no server to run locally.
Q.Tool.define("MyPlugin/thing", function (options) {
var tool = this;
Q.req("MyPlugin/data", ["items"], function (err, data) {
tool.state.items = data.slots.items; // served by U over the bridge
tool.stateChanged("items");
});
});loadFileURL:allowingReadAccessToURL: or the scheme handler.addJavascriptInterface methods need the
@JavascriptInterface annotation, only primitives and strings cross
reliably, and every view call must be on the UI thread.
evaluateJavascript needs API 19+.The UI-thread queue lands with step 1 and is shared by all of them.