Native shells

One interface, five 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.

1. There are only two engines

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.

TargetWebViewEngineBindingDifficulty
Webthe page itselfnone — U already targets WASMtrivial
LinuxWebKitGTKWebKitplain Ceasy
macOSWKWebViewWebKitObjective-Ceasy
iOSWKWebViewWebKitsame shim as macOSeasy
WindowsWebView2ChromiumCOMmoderate
Androidandroid.webkitChromiumJNI → Javahardest

2. The U-side interface

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.

U
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.

3. The bridge — three variants, not five

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.

HostJS sideNative side
WebKitGTKwindow.webkit.messageHandlers.u.postMessage(m)webkit_user_content_manager_register_script_message_handler
WKWebViewwindow.webkit.messageHandlers.u.postMessage(m)WKScriptMessageHandler
WebView2window.chrome.webview.postMessage(m)add_WebMessageReceived
Androidwindow.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.

the generated shim — nobody writes this
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.

4. Every host needs UI-thread marshalling

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.

HostRequirement
WKWebViewall calls on the main thread
AndroidWebView methods AND evaluateJavascript on the UI thread
WebView2the COM apartment that created it; needs the message pump
WebKitGTKthe 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.

5. Serving the assets

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.

HostMechanism
WKWebViewWKURLSchemeHandler
WebKitGTKwebkit_web_context_register_uri_scheme
WebView2AddWebResourceRequestedFilter + WebResourceRequested
AndroidWebViewClient.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.

6. Qbix compatibility — the front end does not change

Both engines are modern, so the Qbix JS runtime loads and runs unmodified. Nothing about tools, templates or events needs a native variant.

QbixInside a U WebView
Q.activate(el)unchanged — U emits the tool markup, Q activates it
Q.Tool.defineunchanged
Q.Template.renderunchanged; Handlebars`…` exports the same template
delegated namespaced eventsunchanged — and U's on* prohibition enforces the convention
Q.Tool.clear before innerHTMLstill required
Q.req / Q.requestworks 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.

the tool is ordinary Qbix; only the transport differs
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");
    });
});

7. Per-target notes

8. What this is not good for

9. Build order

  1. Linux (WebKitGTK) — plain C, fastest to a working window, and it proves the bridge and the UI queue
  2. macOS (WKWebView) — the Objective-C shim, reused verbatim by iOS
  3. iOS — packaging and the scheme handler; the shim already exists
  4. Windows (WebView2) — COM plumbing
  5. Android — JNI and the Java UI thread; last because it is a subproject, not a shim

The UI-thread queue lands with step 1 and is shared by all of them.