Commitcc7e5752Recorded22 Jul 2026Repositorylantern
webview: result-returning eval (webview-eval-js-result)
Message
Add an optional result-returning JS eval to the webview interface, closing the fire-and-forget gap: webkit_web_view_evaluate_javascript was called with a NULL GAsyncReadyCallback, so the page's return value died in GLib.
- Bind webkit_web_view_evaluate_javascript_finish and add on-eval-ready, a 3-arg GAsyncReadyCallback (same shape as the script-message handler) that resolves the JSCValue to a string and fills a parked slot.
- Correlate async completions to callers via an integer token passed as the C user_data + a per-process pending alist; the caller polls its slot with a cooperative sleep so the pump goroutine dispatches the completion in between. Single scheduler thread, so only the caller mutates the table.
- webview-eval-js-result is an OPTIONAL op (backends without it return #f), so callers degrade to no-result rather than erroring.
Consumed by the slate-desktop control server to return real eval values/errors over the slate-client channel (the 0.2.0 MCP server dependency).
Changed
lantern/src/lantern/backend/gtk.sgl | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
lantern/src/lantern/webview.sgl | 8 ++++++++
2 files changed, 80 insertions(+)Diff
lantern/src/lantern/backend/gtk.sglmodified
@@ -102,6 +102,7 @@
102
(define webkit-web-view-get-ucm #f) 103
(define webkit-web-view-set-background-color #f) 104
(define webkit-web-view-eval-js #f)+105
(define webkit-web-view-eval-js-finish #f) 106
(define webkit-ucm-register #f) 107
(define webkit-ucm-add-script #f) 108
(define webkit-user-script-new #f)@@ -133,6 +134,28 @@
134
(define policy-cb #f) 135
(define close-cb #f) 136
(define keypress-cb #f)+137
(define eval-ready-cb #f)+138
+139
;; Result-returning eval correlation. webkit_web_view_evaluate_javascript is+140
;; async: the value arrives LATER on the GLib loop via a GAsyncReadyCallback,+141
;; not from the call itself. Each `eval-js-result` call mints a token, parks a+142
;; slot (a 1-vector) in *eval-pending* under it, and passes the token as the C+143
;; `user_data`; on-eval-ready reads the token back, resolves the JS value to a+144
;; string, and fills that slot. The caller (a service/handler goroutine) polls+145
;; the slot with a cooperative `sleep`, so the pump goroutine runs and dispatches+146
;; the completion in between. Single scheduler thread → only the CALLER mutates+147
;; the alist (put/del); the callback only reads it + mutates the slot vector, so+148
;; there is no table race. Token is a plain int (assv/eqv? keying).+149
(define *eval-pending* (list))+150
(define *eval-token* 0)+151
+152
(define (eval-pending-put! token slot)+153
(set! *eval-pending* (cons (cons token slot) *eval-pending*)))+154
(define (eval-pending-ref token)+155
(let ((e (assv token *eval-pending*))) (and e (cdr e))))+156
(define (eval-pending-del! token)+157
(set! *eval-pending*+158
(filter (lambda (e) (not (= (car e) token))) *eval-pending*))) 159
160
;; ---- per-process backend state --------------------------------------- 161
;;@@ -205,6 +228,26 @@
228
(*msg-handler* (jsc-value-to-string value))) 229
#f))) 230
+231
;; GAsyncReadyCallback for webkit_web_view_evaluate_javascript (3-arg:+232
;; source_object=the WebKitWebView, res=GAsyncResult, user_data=our token).+233
;; IRON RULE: marshal + fill the parked slot + return — no suspension. The+234
;; JS value comes back as a JSCValue; we stringify it (window.slateClientEval+235
;; returns a JS string, so this is the printed eval result). A NULL finish+236
;; (page gone / JS-level error) delivers "" — the caller maps empty to an+237
;; error. The slot holds (list str) so #f still means "not yet delivered".+238
(define (on-eval-ready source res user-data)+239
(guarded "eval-ready" #f+240
(lambda ()+241
(let* ((token (pointer-address user-data))+242
(slot (eval-pending-ref token)))+243
(when slot+244
(let* ((jsc (webkit-web-view-eval-js-finish source res NULL))+245
(str (if (and jsc (not (null-pointer? jsc)))+246
(jsc-value-to-string jsc)+247
"")))+248
(vector-set! slot 0 (list str)))))+249
#f)))+250
251
;; decide-policy (4-arg -> exercises sigil_applyN). Route navigations: keep 252
;; app-origin ones in the view, send everything else to the system browser. 253
(define (on-decide-policy web-view decision decision-type user-data)@@ -379,6 +422,11 @@
422
(set! webkit-web-view-eval-js 423
(c-function libwebkit "webkit_web_view_evaluate_javascript" 424
(list ffi/pointer ffi/string ffi/long ffi/pointer ffi/pointer ffi/pointer ffi/pointer ffi/pointer) ffi/void))+425
;; webkit_web_view_evaluate_javascript_finish(view, GAsyncResult*, GError**)+426
;; -> JSCValue* (or NULL on error; we pass NULL for the GError**).+427
(set! webkit-web-view-eval-js-finish+428
(c-function libwebkit "webkit_web_view_evaluate_javascript_finish"+429
(list ffi/pointer ffi/pointer ffi/pointer) ffi/pointer)) 430
(set! webkit-ucm-register 431
(c-function libwebkit "webkit_user_content_manager_register_script_message_handler" 432
(list ffi/pointer ffi/string ffi/pointer) ffi/bool))@@ -415,6 +463,8 @@
463
;; process lifetime so the traced-callback list never collects them). 464
(set! scheme-cb (c-callback (list ffi/pointer ffi/pointer) ffi/void on-scheme-request)) 465
(set! message-cb (c-callback (list ffi/pointer ffi/pointer ffi/pointer) ffi/void on-script-message))+466
;; GAsyncReadyCallback: (GObject* source, GAsyncResult* res, gpointer user_data) -> void+467
(set! eval-ready-cb (c-callback (list ffi/pointer ffi/pointer ffi/pointer) ffi/void on-eval-ready)) 468
(set! policy-cb (c-callback (list ffi/pointer ffi/pointer ffi/int ffi/pointer) ffi/bool on-decide-policy)) 469
(set! close-cb (c-callback (list ffi/pointer ffi/pointer) ffi/bool on-close-request)) 470
;; GtkEventControllerKey::key-pressed = (controller keyval keycode state user-data) -> gboolean@@ -507,6 +557,27 @@
557
(define (eval-js window js) 558
(webkit-web-view-eval-js (dict-ref window view: #f) js -1 NULL NULL NULL NULL NULL)) 559
+560
;; Result-returning eval. Fires the async webkit eval with our ready+561
;; callback + a correlation token, then polls the parked slot with a+562
;; cooperative sleep (yielding so the pump goroutine dispatches the GLib+563
;; completion) up to `timeout-ticks` × ~20ms. Returns the JS result string,+564
;; or #f on timeout (the caller maps that to an "eval timed out" error).+565
;; Runs on a service/handler goroutine, NOT the callback extent — so the+566
;; sleep-yield is legal (unlike inside on-eval-ready itself).+567
(define (eval-js-result window js timeout-ticks)+568
(set! *eval-token* (+ *eval-token* 1))+569
(let ((token *eval-token*)+570
(slot (make-vector 1 #f))+571
(view (dict-ref window view: #f)))+572
(eval-pending-put! token slot)+573
(webkit-web-view-eval-js view js -1 NULL NULL NULL eval-ready-cb (make-pointer token))+574
(let loop ((n timeout-ticks))+575
(let ((v (vector-ref slot 0)))+576
(cond+577
((pair? v) (eval-pending-del! token) (car v))+578
((<= n 0) (eval-pending-del! token) #f)+579
(else (sleep 0.02) (loop (- n 1))))))))+580
581
(define (set-background window r g b a) 582
(set-view-background (dict-ref window view: #f) r g b a)) 583
@@ -626,6 +697,7 @@
697
register-messages: register-messages 698
inject-script: inject-script 699
eval-js: eval-js+700
eval-js-result: eval-js-result 701
set-background: set-background 702
set-nav-handler: set-nav-handler 703
set-close-handler: set-close-handlerlantern/src/lantern/webview.sglmodified
@@ -44,6 +44,7 @@
44
webview-register-messages 45
webview-inject-script 46
webview-eval-js+47
webview-eval-js-result 48
webview-set-background 49
webview-set-nav-handler 50
webview-set-close-handler@@ -103,6 +104,13 @@
104
((op wv inject-script:) window js)) 105
(define (webview-eval-js wv window js) 106
((op wv eval-js:) window js))+107
;; Optional op: result-returning eval (host <- page). Returns the JS result+108
;; string, #f on timeout, or #f when the backend does not implement it — so a+109
;; caller that wants a value degrades to "no result" rather than erroring.+110
;; timeout-ticks bounds the wait (× the backend's poll tick, ~20ms).+111
(define (webview-eval-js-result wv window js timeout-ticks)+112
(let ((f (dict-ref wv eval-js-result: #f)))+113
(and f (f window js timeout-ticks)))) 114
(define (webview-set-background wv window r g b a) 115
((op wv set-background:) window r g b a)) 116
(define (webview-set-nav-handler wv window handler)