- Scheme 93.1%
- Makefile 4.7%
- C 2.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Select the user prefix explicitly because staging gxpkg reads the old global flag key. Build after linking to publish bindings outside the checkout, and declare the C header as a binding build input. |
||
| demo | ||
| test | ||
| vendor | ||
| .gitignore | ||
| build.ss | ||
| gerbil.pkg | ||
| libwebview.ss | ||
| Makefile | ||
| README.md | ||
| test-runner.ss | ||
| webview-test-support.ss | ||
| webview-test.ss | ||
| webview.ss | ||
gerbil-webview
Desktop HTML/CSS/JavaScript applications with Gerbil Scheme backends, using the webview native library. This package targets Gerbil v0.19. The build and cooperative Scheme-thread integration currently support Linux with GTK 3 and WebKitGTK 4.1; macOS and Windows build integration are not implemented.
Validated against the local v0.19-staging checkout at 2591dcd9 (v0.19-dev-62-g2591dcd9, Gambit v4.9.8-18-gdcd677cd). The vendored webview version is 0.12.0.
Prerequisites
On Ubuntu/Debian:
sudo apt install libgtk-3-dev libwebkit2gtk-4.1-dev g++ pkg-config
# Also needed for display-backed tests:
sudo apt install xvfb xauth
Gerbil v0.19 must provide gerbil, gxi, and its gxtest tool modules. Older v0.18 installations are not supported by this port.
Build and installation
git clone https://github.com/ober/gerbil-webview.git
cd gerbil-webview
make build
# Optional: link and build this checkout in the user's global package environment.
make install
make install links the current checkout and builds its bindings in the selected user prefix, rather than downloading a different remote revision. It explicitly sets GERBIL_PATH to avoid the staging package manager's global-flag mismatch. Keep the checkout in place. The compiled native binding uses an absolute library search path to vendor; rebuild after moving the checkout.
To select a development checkout without installing it system-wide, run its environment wrapper from this directory:
/path/to/gerbil/run.sh make build
/path/to/gerbil/run.sh make test-native
Alternatively, override the commands:
make GERBIL='/path/to/gerbil/run.sh gerbil' GXI='/path/to/gerbil/run.sh gxi' test
The Makefile retains existing GERBIL_LOADPATH entries. Build products and the generated version manifest are ignored. Inspecting build metadata or cleaning does not compile the native library.
Quick start
(import :gerbil-webview/webview)
(def (main . args)
(let (wv (webview-create debug: #t))
(unwind-protect
(begin
(webview-set-title! wv "Hello")
(webview-set-size! wv 400 300)
(webview-set-html! wv "<h1>Hello from Gerbil!</h1>")
(webview-run! wv))
(webview-destroy! wv))))
Destroy every view explicitly, normally with unwind-protect. Native destruction drains UI events and can invoke Scheme, so it must not run inside garbage collection. Dropping an undestroyed handle does not close its native window or reclaim its native resources.
API
Lifecycle
| Function | Arguments | Behavior |
|---|---|---|
webview-create |
debug: #f |
Create a view; raise if native initialization fails. debug: #t enables development tools. |
webview-destroy! |
wv |
Destroy once, invalidate the handle, and remove its registered callbacks. Repeated destruction is harmless. |
webview-run! |
wv |
Run the native event loop until termination or window closure. |
webview-terminate! |
wv |
Request that the running loop exit. |
Operations other than destruction raise Closed from :std/error on a destroyed handle. Do not destroy a view from a binding/dispatch callback or while its event loop is running; request termination, let webview-run! return, then destroy it.
Window and content
| Function | Arguments |
|---|---|
webview-set-title! |
wv title |
webview-set-size! |
wv width height hint: WEBVIEW_HINT_NONE |
webview-navigate! |
wv url |
webview-set-html! |
wv html |
webview-eval! |
wv javascript |
webview-init! |
wv javascript |
webview-init! installs JavaScript for every page load. Install initialization scripts before setting HTML or navigating. Use webview-eval! for an already-loaded page. Size hints are WEBVIEW_HINT_NONE, WEBVIEW_HINT_MIN, WEBVIEW_HINT_MAX, and WEBVIEW_HINT_FIXED.
Bindings and dispatch
| Function | Arguments | Behavior |
|---|---|---|
webview-bind! |
wv name handler |
The handler takes an argument list and returns a JSON-encodable result. |
webview-bind-async! |
wv name handler |
The handler takes an argument list and a respond! procedure. |
webview-unbind! |
wv name |
Remove the native binding and release its Scheme closure. |
webview-dispatch! |
wv thunk |
Schedule a one-shot procedure on the event-loop owner thread. |
webview-version |
none | Return the native (major minor patch) version list. |
JavaScript calls return Promises. Parsing, handler, and response-serialization failures reject the Promise with an exception message. Duplicate registration leaves the existing binding intact and does not retain the failed handler. Negative native registration statuses trigger best-effort native unbinding before Scheme rollback; C argument-conversion failures never unbind an existing name. Dispatched procedures are removed before invocation; their exceptions are reported to current-error-port, not unwound through native callback frames. Destroying a view cancels its pending Scheme dispatch procedures.
(webview-bind! wv "add"
(lambda (args) (apply + args)))
;; JavaScript: const result = await add(2, 3); // 5
The bridge preserves these representations even when the caller changes Gerbil's ambient JSON reader options:
| JavaScript | Scheme |
|---|---|
| number | number |
| string | string |
| boolean | #t / #f |
| null | #!void |
| array | proper list |
| object | hash table with string keys |
The v0.19 implementation uses :std/encoding/json, explicit JSONReadOptions, string->json, and json->string. It does not rely on the new decoder's default association-list representation or the old port-based calling convention.
Asynchronous work
(import (only-in :gerbil/runtime/thread spawn-thread))
(webview-bind-async! wv "fetchData"
(lambda (args respond!)
(spawn-thread
(lambda ()
(let (data (do-expensive-work (car args)))
(respond! data))))))
The application owns its workers: handle worker exceptions, arrange replies/rejections as appropriate, and finish/join workers before destroying the view. respond! is for a live outstanding request; do not call it after destruction or race it against closing the view. Native response data is copied and its delivery is dispatched to the UI thread.
Threading
Create, configure, bind/unbind, run, and destroy a view on one UI owner Scheme thread. Binding and dispatched handlers execute on that thread. Background Scheme workers may call respond!, webview-dispatch!, or webview-terminate! while the view is alive; other native operations belong on the UI thread. Arbitrary foreign operating-system threads must not enter the Scheme callback trampolines.
On Linux, webview-run! installs a 10 ms GLib timer that yields to Gerbil's cooperative scheduler and removes it when the loop returns. Without that scheduling point, an otherwise-idle native loop prevents sleeping Scheme workers from resuming. Long-running Scheme computation must still yield; blocking a handler blocks the UI.
Demos
make demo-todo
make demo-dashboard
Both demos are self-contained. The dashboard renders its small SVG load chart directly with the v0.19 formatting API; gerbil-svg is no longer a package dependency. Its system information reads Linux /proc directly, avoiding subprocess waits that timed out after GTK/WebKit event-loop activity on the tested toolchain. The dashboard's Scheme evaluator is intentionally unrestricted: use it only with its trusted, embedded local page, not untrusted remote content.
Tests
make test # eleven headless cases; native suites are empty without DISPLAY
make test-native # all twenty cases under Xvfb, with a 60-second process watchdog
make test-native-failure # one additional post-insertion failure/rollback case
TEST_TIMEOUT overrides the native-test deadline. The native run treats GTK/GLib critical diagnostics as fatal. GPU-device and child-process-watch warnings can still occur in a virtual-display environment; they are not GTK invalid-widget errors.
Headless tests exercise the actual C callback entry points through a compiled test-support module, one-shot dispatch, exception containment, JSON representations, released-handle guards, and demo backend regressions. Native tests check JavaScript Promise success/rejection, duplicate and C-conversion rollback, unbind/destroy cleanup, raw/high-level destroy combinations, queued callback cancellation, and a sleeping worker's response while the UI is otherwise idle. The dedicated failure target preloads a test-only native interposer that returns an error after inserting a binding; the test then retries the same name and checks duplicate preservation and closure counts. Test results are explicitly converted to process exit statuses because the v0.19 tool entry point returns its status as a value.
Implementation
libwebview.ss::std/ffideclarations, typed registry procedures, native scheduling hook, and C-to-Scheme trampolines.webview.ss: public API, JSON conversion, reply/error handling, and released-handle checks.webview-test-support.ss: compiled, headless trampoline test helpers; not re-exported by the public API.vendor/webview.h: amalgamated webview 0.12.0 with the documented GTK destruction-order patch invendor/README.md.
Callback IDs are synchronized and associated with their native view's address. Failed registrations roll back; unbind removes persistent bindings; dispatch removes its own entry before running; destruction removes all remaining ownership records before draining native events. The raw webview_destroy follows the same explicit destroy-once path and returns the native integer status. Low-level foreign bindings remain an expert API; prefer :gerbil-webview/webview for checked application calls.
License
Same as the webview library: MIT.