Note: This README was generated by AI since I believe AI would do a MUCH better job at explaining the usage than me. (I gave the AI a large amount of context before, so do NOT worry lmao)
A lightweight Rust DLL that lets you open a native webview window from any language that can call C-compatible DLLs. You can find the bindings for Odin Language in this repository.
Built on wry + tao. JS↔native communication goes over a IPC bridge, and each call runs on its own thread queuing doesn't block up majority of actions.
package main
import "core:fmt"
import "core:strings"
import "base:runtime"
import ui "icuwebview"
App :: struct { counter: int; name: string }
HTML :: `<!DOCTYPE html><html><body>
<input id="n" placeholder="Name..."/>
<button onclick="do_greet()">Greet</button>
<button onclick="do_count()">Count</button>
<button onclick="do_quit()">Quit</button>
<div id="out">—</div>
<script>
const out = document.getElementById("out")
async function do_greet() {
out.textContent = await window._icu.greet(document.getElementById("n").value)
}
async function do_count() { out.textContent = "Count: " + await window._icu.increment() }
async function do_quit() { await window._icu.close() }
</script>
</body></html>`
// All callbacks must use "c" calling convention and set context at the top.
// Always call return_val before returning — forgetting it hangs the JS fetch for 5 s.
greet_cb :: proc "c" (seq, req: cstring, arg: rawptr) {
context = runtime.default_context()
app := (^App)(arg)
name, _ := json_arg(string(req), 0)
app.counter += 1
ui.return_val(seq, fmt.ctprintf("%q", fmt.tprintf("Hello, %s! Visit #%d", name, app.counter)))
}
increment_cb :: proc "c" (seq, req: cstring, arg: rawptr) {
context = runtime.default_context()
app := (^App)(arg)
app.counter += 1
ui.return_val(seq, fmt.ctprintf("%d", app.counter))
}
close_cb :: proc "c" (seq, req: cstring, arg: rawptr) {
context = runtime.default_context()
ui.return_val(seq, "true")
ui.terminate()
}
main :: proc() {
w := ui.create(true) // true = enable DevTools + debug logging
defer ui.destroy()
app := App{}
ui.set_title("My App")
ui.set_size(520, 400)
ui.set_position(300, 200)
ui.set_resizable(false)
ui.set_swipe_navigation(false) // disable back/forward swipe gesture
// bind() must be called BEFORE set_html().
// stay_on_reload = true means the binding survives navigate() calls too.
ui.bind("greet", greet_cb, &app)
ui.bind("increment", increment_cb, &app)
ui.bind("close", close_cb, nil)
ui.set_html(HTML)
ui.wait_for_page_load() // wait for DOM before calling eval
ui.run()
}ui.bind("greet", greet_cb, &app) injects a JS shim that makes window._icu.greet(args...) available. Calls that arrive before the shim is ready are queued and flushed automatically. If a binding isn't registered within 2 seconds, queued calls are rejected with a clear error.
To make a bind survive through a page refresh or redirection, specify stay_on_reload parameter and how you change the page:
set_html() |
navigate() |
|
|---|---|---|
stay_on_reload = false (default) |
✅ survives | ❌ lost |
stay_on_reload = true |
✅ survives | ✅ survives |
// Default — re-injected after set_html(), lost after navigate()
ui.bind("greet", greet_cb, &app)
// Persists across both set_html() and navigate()
ui.bind("greet", greet_cb, &app, stay_on_reload = true)unbind works correctly regardless of which mode was used.
All functions accept plain Odin string — no casting needed:
ui.set_title("My App")
ui.set_html(my_html)
ui.navigate("https://example.com")
ui.bind("greet", cb)
ui.eval(`document.body.style.background = "#111"`)Conversion to cstring happens internally via the temp allocator (bump-pointer alloc + memcpy — effectively free). Two intentional exceptions still use cstring:
return_val(seq, result)—seqarrives from a C callback and goes straight back to the DLL.free_string(s)— frees a pointer allocated by the DLL itself.
req in your callbacks is a JSON array like ["Alice", 42, true]. Use the bundled json_arg helper:
name, ok1 := json_arg(string(req), 0) // → "Alice", true
count, ok2 := json_arg(string(req), 1) // → "42", trueHandles commas inside strings, nested structures, and common JSON escape sequences. Always returns a plain Odin string — quotes stripped from string values, numbers/booleans returned as-is.
corners := ui.get_corners(520, 400) // pass your window dimensions
ui.set_position(corners.TopLeft)
ui.set_position(corners.TopRight)
ui.set_position(corners.BottomLeft)
ui.set_position(corners.BottomRight)
ui.set_position(100, 200) // plain x, y still works tooget_corners queries GetSystemMetrics and returns a Corners struct with four [2]c.int fields.
Functions default to the first window created when no handle is passed. Pass a handle to target a specific window:
w1 := ui.create(false)
w2 := ui.create(false)
ui.set_title("First Window") // → w1
ui.set_title("Second Window", w2) // → w2
ui.set_html(html1)
ui.set_html(html2, w2)
ui.run(w1)
ui.run(w2)Pass true to create() to enable debug logging to stderr. When false, the DLL runs silently — no output at all except genuine errors.
ui.create(true) // logs server port, bind calls, page load, eval_result, etc.
ui.create(false) // silent| Proc | Description |
|---|---|
create(debug: bool) -> webview |
Create a window. true enables DevTools and debug logging. Auto-loads the DLL on first call. |
destroy(w = nil) |
Free resources. Use defer ui.destroy(). |
run(w = nil) |
Start the event loop. Blocks until closed. Call last. |
terminate(w = nil) |
Signal the event loop to exit. Safe from any thread. |
is_running() -> bool |
True between run() and window close. |
is_visible(w = nil) -> bool |
True if the window is currently visible. Safe from any thread. |
wait_until_closed() |
Block until closed. Use when run() is on a background thread. |
| Proc | Description |
|---|---|
set_title(title: string, w = nil) |
Set the title bar text. |
set_size(w, h: c.int, hints: Hint = .None, w = nil) |
Resize. Hints: .None .Min .Max .Fixed |
set_position(x, y: c.int, w = nil) |
Move to screen coordinates. |
set_position(pos: [2]c.int, w = nil) |
Move using a corner from get_corners(). |
get_corners(w, h: c.int) -> Corners |
Screen-edge positions for a window of the given size. |
set_resizable(v: bool, w = nil) |
Enable/disable user resizing. |
set_frameless(v: bool, w = nil) |
Remove/restore title bar decorations. |
set_timeout(s: u32, w = nil) |
Auto-close after N seconds. 0 = disabled. |
set_swipe_navigation(v: bool, w = nil) |
Enable/disable the swipe back/forward gesture. Disable recommended for app-style UIs. |
show(w = nil) / hide(w = nil) |
Show or hide without destroying. |
get_hwnd(w = nil) -> int |
Win32 HWND as int. Cast: transmute(windows.HWND)(ui.get_hwnd()) |
| Proc | Description |
|---|---|
navigate(url: string, w = nil) |
Navigate to a URL. Bindings with stay_on_reload = true survive this. |
set_html(html: string, w = nil) |
Load raw HTML. Call bind() first. All bindings survive this. |
eval(js: string, w = nil) |
Fire-and-forget JS execution. |
eval_result(js: string, w = nil) -> string |
Execute JS and block until result comes back (5 s timeout). Returns JSON-encoded string. Do not call from inside a bind callback — deadlocks. |
init(js: string, w = nil) |
One-off JS injection. |
wait_for_page_load(timeout_ms: u32 = 5000, w = nil) -> bool |
Block until DOMContentLoaded fires. Call after set_html()/navigate() before using eval(). Returns false on timeout. Do not call from inside a bind callback — deadlocks. |
| Proc | Description |
|---|---|
bind(name: string, fn, arg = nil, stay_on_reload = false, w = nil) |
Bind window._icu.<n>(...) to an Odin callback. See survival table above. |
unbind(name: string, w = nil) |
Remove a binding. Works regardless of stay_on_reload value. |
return_val(seq, result: cstring, status: c.int = 0, w = nil) |
Resolve/reject the JS Promise. status != 0 rejects with {"error": result}. Must be called in every code path of your callback. |
Callback signature:
my_cb :: proc "c" (seq, req: cstring, arg: rawptr) {
context = runtime.default_context()
// seq — pass back to return_val unchanged
// req — JSON array of JS args, e.g. ["Alice", 42]
// arg — rawptr you passed to bind()
val, ok := json_arg(string(req), 0)
ui.return_val(seq, fmt.ctprintf("%q", val))
}| Proc | Description |
|---|---|
set_data_dir(path: string) |
Set persistent browser data dir (cookies, cache, localStorage). Call before create(). "" = ephemeral temp dir (default). |
dispatch(fn, arg, w = nil) |
Call a C-style callback with the webview handle. |
load() -> bool |
Manually load the DLL from %TEMP%. Called automatically by create(). |
load_from(path: string) -> bool |
Load DLL from a specific path. Call before create(). |
unload() |
Unload the DLL (optional — OS cleans up on exit). |
free_string(s: cstring) |
Free a DLL-allocated C string. |
.Ok · .Duplicate · .Not_Found · .Invalid_Argument · .Invalid_State · .Unspecified · .Missing_Dependency · .Canceled
icuwebview exports a plain C ABI — load it from Python, C/C++, Zig, Nim, Go, Rust, or anything else that can call a Windows DLL.
C ABI signatures:
void* create(bool debug, void* window)
int destroy(void* w)
int run(void* w)
int terminate(void* w)
bool is_running()
bool is_visible(void* w)
int set_title(char* title, void* w)
int set_size(int w, int h, int hints, void* w)
int set_position(int x, int y, void* w)
int set_resizable(bool v, void* w)
int set_frameless(bool v, void* w)
int set_timeout(uint32 s, void* w)
int set_swipe_navigation(bool enabled, void* w)
int show(void* w)
int hide(void* w)
int navigate(char* url, void* w)
int set_html(char* html, void* w)
int eval(char* js, void* w)
char* eval_result(char* js, void* w) // caller must free with free_string()
int init(char* js, void* w)
bool wait_for_page_load(uint32 timeout_ms, void* w)
int webview_bind(char* name, void (*cb)(char* seq, char* req, void* arg), void* arg, bool stay_on_reload, void* w)
int unbind(char* name, void* w)
int return_val(char* seq, char* result, int status, void* w)
void wait_until_closed()
void set_data_dir(char* path)
void free_string(char* s)
void* get_window(void* w)
intptr get_hwnd(void* w)
// Note: bind is exported as "webview_bind" to avoid a Winsock name clash.
// Hints enum: None=0 Min=1 Max=2 Fixed=3Python (ctypes) example:
import ctypes, json
lib = ctypes.WinDLL("icuwebview.dll")
lib.create.restype = ctypes.c_void_p
lib.create.argtypes = [ctypes.c_bool, ctypes.c_void_p]
lib.set_title.argtypes = [ctypes.c_char_p, ctypes.c_void_p]
lib.set_html.argtypes = [ctypes.c_char_p, ctypes.c_void_p]
lib.run.argtypes = [ctypes.c_void_p]
lib.webview_bind.argtypes = [ctypes.c_char_p,
ctypes.CFUNCTYPE(None, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_void_p),
ctypes.c_void_p, ctypes.c_bool, ctypes.c_void_p]
lib.return_val.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_int, ctypes.c_void_p]
BIND_FN = ctypes.CFUNCTYPE(None, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_void_p)
def on_hello(seq, req, arg):
args = json.loads(req)
lib.return_val(seq, json.dumps(f"Hello, {args[0]}!").encode(), 0, None)
cb = BIND_FN(on_hello) # keep reference — GC will break it otherwise
w = lib.create(False, None)
lib.set_title(b"Python App", None)
lib.set_swipe_navigation(False, None)
lib.webview_bind(b"hello", cb, None, False, None)
lib.set_html(b'<button onclick="window._icu.hello(\'world\').then(r=>alert(r))">Go</button>', None)
lib.run(None)