o
odinpkg.dev
packages / library / icuwebview

icuwebview

8e237balibrary

Using tao and wry from rust, this is a port for a native webview windows for Odin or many other languages. Just import the .dll, look at the readme file file for more info.

Apache-2.0 · updated 1 month ago

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)

icuwebview

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.

Quick Start

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()
}

How binding works

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.

Bindings and page changes

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.


Strings

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)seq arrives from a C callback and goes straight back to the DLL.
  • free_string(s) — frees a pointer allocated by the DLL itself.

Parsing JS Arguments

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",    true

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


Screen Corners

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 too

get_corners queries GetSystemMetrics and returns a Corners struct with four [2]c.int fields.


Multiple Windows

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)

Debug Logging

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

API Reference

Lifecycle

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.

Window

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())

Content

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.

Binding

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))
}

Utilities

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.

Error Values

.Ok · .Duplicate · .Not_Found · .Invalid_Argument · .Invalid_State · .Unspecified · .Missing_Dependency · .Canceled


Using from Other Languages

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=3

Python (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)