MoonBit bindings for webview, a tiny cross-platform library for creating modern web-based desktop applications using HTML, CSS, and JavaScript.
- π Lightweight: Minimal overhead with native performance
- π¨ Modern UI: Build desktop apps using web technologies
- π Cross-platform: Works on Windows, macOS, and Linux
- π± Responsive: Native window management and controls
- π JavaScript Bridge: Seamless communication between MoonBit and web content
- π‘οΈ Type-safe: Full MoonBit type safety for WebView operations
β οΈ Note: This project is currently in active development. APIs may change in future releases.
Add justjavac/webview to your project dependencies:
moon update
moon add justjavac/webviewConfigure your moon.pkg file to link with the webview library:
options(
"is-main": true,
link: {
"native": {
"cc-flags": "-fwrapv -fsanitize=address -fsanitize=undefined",
"cc-link-flags": "-L .mooncakes/justjavac/webview/lib -lwebview",
},
},
)Set the dynamic library path:
export DYLD_LIBRARY_PATH="$(pwd)/.mooncakes/justjavac/webview/lib"set "MOONWEB_LIB=%CD%\.mooncakes\justjavac\webview\lib"
set _CL_=/link /LIBPATH:"%MOONWEB_LIB%" webview.lib /DEBUG
set "PATH=%PATH%;%MOONWEB_LIB%"Note: This Windows path handling uses an absolute library path, similar to the approach used in CI workflow (.github/workflows/ci.yml) to ensure library paths resolve correctly even when the working directory contains spaces.
$libPath = Join-Path -Path (Get-Location) -ChildPath ".mooncakes\justjavac\webview\lib"
$env:_CL_ = "/link /LIBPATH:`"$libPath`" webview.lib /DEBUG"
$env:PATH = "$env:PATH;$libPath"export LD_LIBRARY_PATH="$(pwd)/.mooncakes/justjavac/webview/lib:$LD_LIBRARY_PATH"Here's a simple example to get you started:
let html =
#| <html>
#| <head>
#| <title>MoonBit WebView</title>
#| <style>
#| body {
#| font-family: system-ui, -apple-system, sans-serif;
#| display: flex;
#| justify-content: center;
#| align-items: center;
#| height: 100vh;
#| margin: 0;
#| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
#| color: white;
#| }
#| h1 { text-align: center; font-size: 2.5em; }
#| </style>
#| </head>
#| <body>
#| <h1>Hello, MoonBit WebView! π</h1>
#| </body>
#| </html>
fn main {
@webview.Webview::new(debug=1)
..set_title("MoonBit WebView Example")
..set_size(800, 600, @webview.SizeHint::None)
..set_html(html)
.run()
}For structured JS <-> MoonBit communication, use CommandBridge on top of the
existing low-level bindings.
- JS -> MoonBit:
window.MoonBitBridge.send(name, payload) - MoonBit -> JS:
bridge.send(name, payload)andwindow.MoonBitBridge.onCommand(listener)
Notes:
window.MoonBitBridge.send(...)is request/reply. It returns aPromise<CommandResponse>.CommandResponse.statusis"ok"or"error".- On success,
CommandResponse.payloadcontains the handler result. - On failure,
CommandResponse.errorcontains the error message. - Use
bridge.handle_result(...)if a MoonBit handler should explicitly returnResult[Reply, String]instead of always succeeding. bridge.send(...)is fire-and-forget. It pushes aCommandevent into the page; it does not wait for a JavaScript reply.- MoonBit -> JS delivery is scheduled onto the webview event loop internally,
so
bridge.send(...)is safe to call off the UI thread. binding_namemust be unique perWebview.CommandBridge::new(...)aborts immediately if that internal binding name is already in use.- Call
bridge.destroy()if you want to unregister the bridge binding and reuse the samebinding_nameon the sameWebview. - If you want raw JSON handling on the MoonBit side, register the handler with
Jsonas the payload type.
struct SumPayload {
left : Int
right : Int
} derive(ToJson, FromJson)
struct SumReply {
total : Int
} derive(ToJson, FromJson)
struct NoticePayload {
message : String
} derive(ToJson, FromJson)
fn main {
let webview = @webview.Webview::new(debug=1)
let bridge = @webview.CommandBridge::new(webview)
bridge.handle_result("sum", fn(payload : SumPayload) {
let reply =
if payload.right < 0 {
Err("right must be non-negative")
} else {
Ok(SumReply::{ total: payload.left + payload.right })
}
bridge.send("notice", NoticePayload::{
message: "MoonBit handled sum",
})
reply
})
webview.set_html(
#| <script>
#| window.MoonBitBridge.onCommand((command) => {
#| if (command.name === "notice") {
#| console.log("MoonBit -> JS", command.payload);
#| }
#| });
#| window.MoonBitBridge
#| .send("sum", { left: 1, right: 2 })
#| .then((response) => {
#| if (response.status === "ok") {
#| console.log("MoonBit reply", response.payload);
#| } else {
#| console.error("MoonBit command error", response.error);
#| }
#| })
#| .catch(console.error);
#| </script>,
)
webview.run()
}For module-level JS APIs, install Plugin modules on a Webview. The default
plugin runtime is built on top of CommandBridge and exposed as
window.MoonBitPlugins.
- A MoonBit module exports a
Plugin. - The main program usually installs that plugin with
webview.install_plugin(...). - Plugins can define native install/destroy hooks.
- JavaScript calls the installed API through
window.MoonBitPlugins.<plugin>.<api>(payload)orwindow.MoonBitPlugins["@@call"](plugin, api, payload). - Plugin JS calls resolve to the same
CommandResponseshape returned byCommandBridge.send(...), withstatus,payload, anderror. - JavaScript subscribes to plugin events through
window.MoonBitPlugins.<plugin>["@@on"](listener)orwindow.MoonBitPlugins.<plugin>["@@onEvent"](name, listener).
struct SumPayload {
left : Int
right : Int
} derive(ToJson, FromJson)
struct SumReply {
total : Int
} derive(ToJson, FromJson)
struct NoticePayload {
message : String
} derive(ToJson, FromJson)
pub fn math_plugin() -> @webview.Plugin {
@webview.Plugin::new(
"math",
fn(plugin) {
plugin.command("sum", fn(payload : SumPayload) {
let reply = SumReply::{ total: payload.left + payload.right }
plugin.emit("computed", NoticePayload::{
message: "MoonBit computed " + reply.total.to_string(),
})
reply
})
},
on_install=fn(plugin) {
// Native setup can happen here.
let _ = plugin.name()
},
on_destroy=fn(plugin) {
// Native cleanup can happen here.
let _ = plugin.name()
},
)
}
fn main {
let webview = @webview.Webview::new(debug=1)
webview.install_plugin(math_plugin())
webview.set_html(
#| <script>
#| window.MoonBitPlugins.math["@@onEvent"]("computed", (event) => {
#| console.log("plugin event", event);
#| });
#| window.MoonBitPlugins.math
#| .sum({ left: 20, right: 22 })
#| .then((response) => {
#| if (response.status === "ok") {
#| console.log("plugin payload", response.payload);
#| return;
#| }
#| console.error("plugin error", response.error);
#| })
#| .catch(console.error);
#| </script>,
)
webview.run()
}Notes:
webview.install_plugin(...)uses a defaultPluginHostunderwindow.MoonBitPlugins.- Use
webview.emit_plugin(plugin, name, payload)when the main program, rather than a plugin context, needs to push a plugin-scoped event into JavaScript. webview.plugin_host()returns that default host if you need direct access toPluginHostor the underlyingCommandBridge.- You can still construct
PluginHost::new(...)directly when you need custom JS namespace or bridge names. - Reusable plugins can live in separate MoonBit modules such as plugins/fs/README.md.
- Plugin modules can add utility APIs beyond handle-based commands. For
example, the fs plugin exposes
fs.resolvePath({ path })so JavaScript can ask the native backend for the platform-specific absolute path. - Plugin cleanup is attached to the
Webviewlifecycle, soon_destroyhooks run during normalwebview.run()/webview.destroy(). - Install a plugin before loading content if you want the JS API to exist on the first page load.
- Plugin names and API names starting with
@@are reserved for framework/internal use. - Special JavaScript property names such as
__proto__,prototype, andconstructorare also reserved for plugin names and API names. - The JS host uses reserved helper keys
@@call,@@has,@@ensurePlugin,@@defineApi,@@on,@@onEvent, and@@emit. - Duplicate plugin names or duplicate APIs inside the same plugin abort during installation.
This repository includes several examples in the examples/ directory:
- 01_run - Basic window creation
- 02_local - Loading local HTML files
- 03_remote - Loading remote web pages
- 04_user_agent - Custom user agent configuration
- 05_alert - JavaScript alerts and dialogs
- 06_onload - Handling page load events
- 07_inject_js - Injecting JavaScript code
- 08_eval - Evaluating JavaScript expressions
- 09_dispatch - Event dispatching
- 10_bind - Binding MoonBit functions to JavaScript
- 11_multi_window - Multiple window management
- 12_embed - Embedding resources
- 13_todo - Complete todo application
- 14_beforeunload - Handling window close events
- 15_close - Window close management
- 16_command - Structured JS <-> MoonBit command bridge
- 17_plugin - Generic plugin modules exposed as JavaScript APIs
- 18_plugin_fs - Filesystem plugin workbench with absolute-path resolution
Run any example:
moon -C examples run <example_name> --target native- MoonBit toolchain
- CMake 3.15 or higher
- Ninja build system
- C/C++ compiler (GCC, Clang, or MSVC)
-
Clone and build dependencies:
cmake -G Ninja -B build -S . -D CMAKE_BUILD_TYPE=Release cmake --build build -
Set up environment variables:
# macOS export DYLD_LIBRARY_PATH="$(pwd)/lib" # Windows (Command Prompt) set "MOONWEB_LIB=%CD%\lib" set _CL_=/link /LIBPATH:"%MOONWEB_LIB%" webview.lib /DEBUG set "PATH=%PATH%;%MOONWEB_LIB%" # Windows (PowerShell) $libPath = Join-Path -Path (Get-Location) -ChildPath "lib" $env:_CL_ = "/link /LIBPATH:`"$libPath`" webview.lib /DEBUG" $env:PATH = "$env:PATH;$libPath" # Linux export LD_LIBRARY_PATH="$(pwd)/lib:$LD_LIBRARY_PATH"
-
Install dependencies and run examples:
moon update # Note: Installing with `moon install` without arguments is deprecated. # If you need a specific package, use `moon install <package>`. moon -C examples run 02_local --target native
moon test --target nativeMIT License Β© justjavac
