WEP: WebIDL Binding Generator (wado-from-idl)
Context
Wado targets WebAssembly and WASI, but the web browser remains the most important deployment platform for Wasm. Browser APIs (DOM, Fetch, Canvas, WebSocket, etc.) are specified in WebIDL — a language-neutral interface description format maintained by W3C. To use these APIs from Wado, we need a tool that generates type-safe Wado bindings from WebIDL definitions.
Prior Art: wasm-bindgen / web-sys
Rust's wasm-bindgen ecosystem has proven that WebIDL-to-language binding generation works. However, it has well-documented ergonomic problems:
Type casting boilerplate — JavaScript's prototype inheritance maps to Rust's Deref chain, requiring explicit dyn_into() / dyn_ref() casts:
// web-sys: get an input element and read its value
let el = document.get_element_by_id("search").unwrap();
let input: HtmlInputElement = el.dyn_into().unwrap(); // can panic
let value = input.value();
Untyped promises — All promises return JsValue, losing type information:
// web-sys: fetch returns untyped JsValue
let resp_value = JsFuture::from(window.fetch_with_str(&url)).await?;
let resp: Response = resp_value.dyn_into()?; // manual cast
let text = JsFuture::from(resp.text()?).await?;
let s: String = text.as_string().unwrap(); // another manual cast
Ugly overload naming — WebIDL method overloads produce suffixed names:
// web-sys: fetch has 4 generated variants
window.fetch_with_str(&url);
window.fetch_with_str_and_init(&url, &opts);
window.fetch_with_request(&req);
window.fetch_with_request_and_init(&req, &opts);
Closure boilerplate — Event handlers require Closure::wrap + .forget():
// web-sys: adding an event listener
let closure = Closure::wrap(Box::new(move |event: Event| {
// ...
}) as Box<dyn FnMut(Event)>);
element.add_event_listener_with_callback("click", closure.as_ref().unchecked_ref())?;
closure.forget(); // leak memory to prevent drop
Wado's Advantages
Wado eliminates several root causes of these pain points:
- GC-based memory — Closures are first-class values managed by Wasm GC. No
Closure::wrap, no.forget(), no memory leaks. - Effect system — Side effects are tracked in function signatures (
with Dom,with Fetch), making it clear which functions touch browser APIs. - Colorless async — Wado uses Wasm Stack Switching for async.
Promise<T>maps toFuture<T>without viralasync/awaitcoloring. - Resource types — Wado's
resourcekeyword is a natural fit for opaque browser object handles.
Decision
Source Data
wado-from-idl uses the @webref/idl npm package as its source of truth. This package provides curated, validated WebIDL definitions scraped from W3C and WHATWG specifications, updated weekly. All definitions are guaranteed to parse and be internally consistent.
npm install @webref/idl
The package's parseAll() API (powered by webidl2) produces parsed AST nodes with structured default fields for optional parameters and dictionary members. Default value types: boolean, number, string, null, dictionary (= {}), sequence (= []). When no default is specified, the default field is null.
Tool Implementation
wado-from-idl (renamed from wado-from-wit) already provides a type-safe IR (WadoModule, WadoResource, WadoStruct, WadoEffect, etc.) and a WadoCodeGenerator that emits clean Wado source. The concepts map directly: WIT interfaces and WebIDL interfaces both produce resources, structs, enums, and effects.
The crate gains a WebIDL parser alongside the WIT parser, sharing the IR and codegen layers:
wado-from-idl --webidl-dir ./webidl-json/ --output-dir ./web/
Pipeline:
@webref/idl (JSON) → Parse → IR (WadoModule: resources, types, effects) → WadoCodeGenerator → web/*.wado
The WIT path remains unchanged (--wit-dir). Both frontends produce the same IR, so codegen improvements benefit both WASI and Web API bindings.
Unified Attribute: #[cm("...")]
The #[cm("...")] attribute provides Component Model ABI mappings for all namespaces, with the namespace encoded in the value:
// Web API binding
#[cm("web:dom#Element")]
pub resource Element extends Node { ... }
#[cm("web:dom#Element.getAttribute")]
fn get_attribute(&self, name: String) -> Option<String>;
// WASI binding
#[cm("wasi:http/types@0.3.0#request")]
pub resource Request { ... }
Sub-attributes for method kinds:
#[cm("web:dom#Element.id", getter)] // property getter
#[cm("web:dom#Element.id", setter)] // property setter
#[cm("web:dom#[constructor]Document")] // constructor
#[cm("web:url#[static]URL.createObjectURL")] // static method
Parameter name mapping:
#[cm_params("self", "qualified-name")]
fn get_attribute(&self, qualified_name: String) -> Option<String>;
Language Extension: resource extends
WebIDL interfaces form single-inheritance hierarchies (e.g., EventTarget → Node → Element → HTMLElement → HTMLInputElement). Wasm-bindgen handles this with #[wasm_bindgen(extends = Node, extends = EventTarget)] attributes that generate Deref chains — a well-known Rust anti-pattern.
wado-from-idl introduces resource extends as a first-class Wado language feature:
#[cm("web:dom#EventTarget")]
pub resource EventTarget { ... }
#[cm("web:dom#Node")]
pub resource Node extends EventTarget { ... }
#[cm("web:dom#Element")]
pub resource Element extends Node { ... }
#[cm("web:dom#HTMLElement")]
pub resource HtmlElement extends Element { ... }
#[cm("web:dom#HTMLInputElement")]
pub resource HtmlInputElement extends HtmlElement { ... }
Semantics:
- Single inheritance only — matches WebIDL and JS prototype chains.
- Upcast is implicit —
&HtmlInputElementis usable where&Elementis expected. The compiler inserts the conversion automatically. - Downcast is safe —
node.downcast::<Element>()returnsOption<&Element>. The runtime performs aninstanceofcheck on the host side. - Method delegation — parent methods are callable on children without explicit upcast.
element.text_content()callsNode.textContenttransparently. - CM compilation — compiles to flat CM resources (the Component Model has no inheritance concept). The
extendsrelationship is purely a Wado-side convenience for type-safe handle subtyping.
This applies only to resource (opaque handle types), not to struct (value types). Value types remain non-inheritable — inheritance is about handle polymorphism, not data layout.
WebIDL → Wado Type Mapping
Primitive Types
| WebIDL | Wado |
|---|---|
boolean |
bool |
byte / octet |
i8 / u8 |
short / unsigned short |
i16 / u16 |
long / unsigned long |
i32 / u32 |
long long / unsigned long long |
i64 / u64 |
float / unrestricted float |
f32 |
double / unrestricted double |
f64 |
DOMString / USVString / ByteString |
String |
undefined |
() |
Compound Types
| WebIDL | Wado | Example |
|---|---|---|
T? (nullable) |
Option<T> |
Element? → Option<Element> |
sequence<T> |
List<T> |
sequence<Node> → List<Node> |
FrozenArray<T> |
List<T> |
Immutable in JS, same Wado type |
Promise<T> |
Future<T> |
Promise<Response> → Future<Response> |
record<K, V> |
TreeMap<K, V> |
|
(A or B) (union) |
variant |
See union section |
any |
JsValue |
Opaque escape hatch |
Structural Types
| WebIDL | Wado | Pattern |
|---|---|---|
interface |
resource |
Opaque handle with methods |
interface X : Y |
resource X extends Y |
Single inheritance |
interface mixin |
trait |
Shared behavior across resources |
dictionary |
struct |
Data with field defaults |
enum |
enum |
String enum → Wado enum |
callback |
fn(...) |
Closure type |
typedef |
type |
Newtype alias |
Effect Design
Web API calls are side effects. wado-from-idl groups them into per-specification effects, following the same pattern as WASI effects (Stdout, FileSystem, etc.):
// web:dom — DOM access
pub interface Dom {
fn window() -> Window;
fn document() -> Document;
}
// web:fetch — network requests
pub interface Fetch {
fn fetch(input: String, init: Option<RequestInit>) -> Future<Response>;
}
// web:canvas — canvas rendering
pub interface Canvas2D { ... }
// web:websocket — WebSocket connections
pub interface WebSocket { ... }
// web:storage — localStorage / sessionStorage
pub interface Storage { ... }
User code declares which effects it needs:
fn app() with Dom, Fetch {
let doc = Dom::document();
let data = Fetch::fetch("/api/data", null);
}
Method Overload Strategy
WebIDL allows method overloading (same name, different signatures) and optional parameters with default values. wado-from-idl resolves these using default arguments:
// WebIDL: optional parameter with default
Node cloneNode(optional boolean deep = false);
// WebIDL: optional parameter without default
undefined fillText(DOMString text, double x, double y, optional double maxWidth);
// WebIDL: nullable callback with optional third argument
undefined addEventListener(DOMString type, EventListener? callback,
optional (AddEventListenerOptions or boolean) options);
// Generated Wado: optional params with known defaults use the actual type
fn clone_node(&self, deep: bool = false) -> Node;
// Optional params without a WebIDL default use Option<T> with = null
fn fill_text(&self, text: String, x: f64, y: f64, max_width: Option<f64> = null);
// Nullable params remain Option<T>; optional params get defaults
fn add_event_listener(&self, type: String, callback: Option<fn(Event)>,
options: Option<AddEventListenerOptions> = null);
Default arguments are desugared at the call site with zero overhead — see WEP: Default Arguments.
For true type overloads (different types at the same position), wado-from-idl generates a variant:
variant CanvasImageSourceOrPath {
Source(CanvasImageSource),
Path(Path2d),
}
Concrete Examples
EventTarget → Node → Element (Inheritance Chain)
WebIDL:
interface EventTarget {
constructor();
undefined addEventListener(DOMString type, EventListener? callback,
optional (AddEventListenerOptions or boolean) options);
undefined removeEventListener(DOMString type, EventListener? callback,
optional (EventListenerOptions or boolean) options);
boolean dispatchEvent(Event event);
};
interface Node : EventTarget {
readonly attribute unsigned short nodeType;
readonly attribute DOMString nodeName;
readonly attribute Node? parentNode;
readonly attribute NodeList childNodes;
attribute DOMString? textContent;
Node appendChild(Node node);
Node removeChild(Node child);
Node cloneNode(optional boolean deep = false);
};
interface Element : Node {
readonly attribute DOMString tagName;
attribute DOMString id;
attribute DOMString className;
DOMString? getAttribute(DOMString qualifiedName);
undefined setAttribute(DOMString qualifiedName, DOMString value);
Element? querySelector(DOMString selectors);
NodeList querySelectorAll(DOMString selectors);
};
Generated Wado:
#![generated]
pub struct EventListenerOptions {
pub capture: bool = false,
}
pub struct AddEventListenerOptions {
pub capture: bool = false,
pub passive: Option<bool> = null,
pub once: bool = false,
pub signal: Option<AbortSignal> = null,
}
#[cm("web:dom#EventTarget")]
pub resource EventTarget {
#[cm("web:dom#[constructor]EventTarget")]
fn new() -> EventTarget;
#[cm("web:dom#EventTarget.addEventListener")]
fn add_event_listener(&self, type: String, callback: Option<fn(Event)>,
options: AddEventListenerOptions = AddEventListenerOptions {});
#[cm("web:dom#EventTarget.removeEventListener")]
fn remove_event_listener(&self, type: String, callback: Option<fn(Event)>,
options: EventListenerOptions = EventListenerOptions {});
#[cm("web:dom#EventTarget.dispatchEvent")]
fn dispatch_event(&self, event: Event) -> bool;
}
#[cm("web:dom#Node")]
pub resource Node extends EventTarget {
#[cm("web:dom#Node.nodeType", getter)]
fn node_type(&self) -> u16;
#[cm("web:dom#Node.nodeName", getter)]
fn node_name(&self) -> String;
#[cm("web:dom#Node.parentNode", getter)]
fn parent_node(&self) -> Option<Node>;
#[cm("web:dom#Node.childNodes", getter)]
fn child_nodes(&self) -> NodeList;
#[cm("web:dom#Node.textContent", getter)]
fn text_content(&self) -> Option<String>;
#[cm("web:dom#Node.textContent", setter)]
fn set_text_content(&self, value: Option<String>);
#[cm("web:dom#Node.appendChild")]
fn append_child(&self, node: &Node) -> Node;
#[cm("web:dom#Node.removeChild")]
fn remove_child(&self, child: &Node) -> Node;
#[cm("web:dom#Node.cloneNode")]
fn clone_node(&self, deep: bool = false) -> Node;
}
#[cm("web:dom#Element")]
pub resource Element extends Node {
#[cm("web:dom#Element.tagName", getter)]
fn tag_name(&self) -> String;
#[cm("web:dom#Element.id", getter)]
fn id(&self) -> String;
#[cm("web:dom#Element.id", setter)]
fn set_id(&self, value: String);
#[cm("web:dom#Element.className", getter)]
fn class_name(&self) -> String;
#[cm("web:dom#Element.className", setter)]
fn set_class_name(&self, value: String);
#[cm("web:dom#Element.getAttribute")]
fn get_attribute(&self, qualified_name: String) -> Option<String>;
#[cm("web:dom#Element.setAttribute")]
fn set_attribute(&self, qualified_name: String, value: String);
#[cm("web:dom#Element.querySelector")]
fn query_selector(&self, selectors: String) -> Option<Element>;
#[cm("web:dom#Element.querySelectorAll")]
fn query_selector_all(&self, selectors: String) -> NodeList;
}
Key design points:
- Dictionary inheritance is flattened (
AddEventListenerOptionsincludescapturefromEventListenerOptions). Dictionary fields use struct field defaults. (AddEventListenerOptions or boolean)union is simplified toOption<AddEventListenerOptions>. Thebooleanshorthand ({capture: true}) is a JS convenience that the host runtime can handle.callback EventListener = undefined (Event event)becomesfn(Event)directly — Wado closures are GC-managed, no wrapping needed.optional boolean deep = falsebecomesdeep: bool = false— default arguments are desugared at the call site with zero overhead.
Document and the Dom Effect
#[cm("web:dom")]
pub interface Dom {
fn window() -> Window;
fn document() -> Document;
}
#[cm("web:dom#Document")]
pub resource Document extends Node {
#[cm("web:dom#[constructor]Document")]
fn new() -> Document;
#[cm("web:dom#Document.getElementById")]
fn get_element_by_id(&self, element_id: String) -> Option<Element>;
#[cm("web:dom#Document.createElement")]
fn create_element(&self, local_name: String) -> Element;
#[cm("web:dom#Document.createTextNode")]
fn create_text_node(&self, data: String) -> Text;
#[cm("web:dom#Document.querySelector")]
fn query_selector(&self, selectors: String) -> Option<Element>;
#[cm("web:dom#Document.querySelectorAll")]
fn query_selector_all(&self, selectors: String) -> NodeList;
#[cm("web:dom#Document.body", getter)]
fn body(&self) -> Option<HtmlElement>;
#[cm("web:dom#Document.head", getter)]
fn head(&self) -> Option<HtmlHeadElement>;
}
User code:
use { Dom, Document, Element } from "web:dom";
fn setup_page() with Dom {
let doc = Dom::document();
let el = doc.create_element("div");
el.set_id("app");
el.set_text_content(Option::Some("Hello, Wado!"));
if let Some(body) = doc.body() {
body.append_child(&el); // append_child from Node, available via extends
}
}
Compared to web-sys:
// web-sys: 6 unwrap/expect calls, 2 type casts
let window = web_sys::window().expect("no window");
let document = window.document().expect("no document");
let body = document.body().expect("no body");
let el = document.create_element("div").expect("create_element failed");
el.set_id("app");
el.set_text_content(Some("Hello, Wado!"));
body.append_child(&el).expect("append_child failed");
HTMLInputElement (Deep Inheritance)
Five-level chain: EventTarget → Node → Element → HTMLElement → HTMLInputElement.
#[cm("web:html#HTMLElement")]
pub resource HtmlElement extends Element {
#[cm("web:html#HTMLElement.title", getter)]
fn title(&self) -> String;
#[cm("web:html#HTMLElement.title", setter)]
fn set_title(&self, value: String);
#[cm("web:html#HTMLElement.hidden", getter)]
fn hidden(&self) -> Option<String>;
#[cm("web:html#HTMLElement.hidden", setter)]
fn set_hidden(&self, value: Option<String>);
#[cm("web:html#HTMLElement.click")]
fn click(&self);
#[cm("web:html#HTMLElement.innerText", getter)]
fn inner_text(&self) -> String;
}
#[cm("web:html#HTMLInputElement")]
pub resource HtmlInputElement extends HtmlElement {
#[cm("web:html#HTMLInputElement.value", getter)]
fn value(&self) -> String;
#[cm("web:html#HTMLInputElement.value", setter)]
fn set_value(&self, value: String);
#[cm("web:html#HTMLInputElement.type", getter)]
fn type_(&self) -> String;
#[cm("web:html#HTMLInputElement.type", setter)]
fn set_type(&self, value: String);
#[cm("web:html#HTMLInputElement.checked", getter)]
fn checked(&self) -> bool;
#[cm("web:html#HTMLInputElement.checked", setter)]
fn set_checked(&self, value: bool);
#[cm("web:html#HTMLInputElement.disabled", getter)]
fn disabled(&self) -> bool;
#[cm("web:html#HTMLInputElement.disabled", setter)]
fn set_disabled(&self, value: bool);
#[cm("web:html#HTMLInputElement.focus")]
fn focus(&self);
}
User code — thanks to resource extends, all ancestor methods are directly accessible:
use { Dom, Document, HtmlInputElement } from "web:dom";
fn setup_form() with Dom {
let doc = Dom::document();
if let Some(el) = doc.query_selector("#username") {
if let Some(input) = el.downcast::<HtmlInputElement>() {
// HtmlInputElement's own method
let val = input.value();
// Element's method (2 levels up) — no cast needed
input.set_attribute("placeholder", "Enter username");
// Node's method (3 levels up) — no cast needed
let parent = input.parent_node();
// EventTarget's method (4 levels up) — no cast needed
input.add_event_listener("input", Option::Some(|e: Event| {
// Closures just work — no Closure::wrap, no .forget()
log_stdout(`input changed`);
})); // options defaults to null
}
}
}
Compared to web-sys:
// web-sys: requires explicit dyn_into at every level crossing
let el = document.query_selector("#username")?.unwrap();
let input: HtmlInputElement = el.dyn_into()?; // explicit cast
input.set_attribute("placeholder", "Enter username")?;
let parent = input.parent_node(); // works via Deref to Node
// Event listener requires Closure::wrap boilerplate...
Events (Typed Event Handling)
#[cm("web:dom#Event")]
pub resource Event {
#[cm("web:dom#Event.type", getter)]
fn type_(&self) -> String;
#[cm("web:dom#Event.target", getter)]
fn target(&self) -> Option<EventTarget>;
#[cm("web:dom#Event.preventDefault")]
fn prevent_default(&self);
#[cm("web:dom#Event.stopPropagation")]
fn stop_propagation(&self);
}
#[cm("web:uievents#MouseEvent")]
pub resource MouseEvent extends Event {
#[cm("web:uievents#MouseEvent.clientX", getter)]
fn client_x(&self) -> i32;
#[cm("web:uievents#MouseEvent.clientY", getter)]
fn client_y(&self) -> i32;
#[cm("web:uievents#MouseEvent.button", getter)]
fn button(&self) -> i16;
}
#[cm("web:uievents#KeyboardEvent")]
pub resource KeyboardEvent extends Event {
#[cm("web:uievents#KeyboardEvent.key", getter)]
fn key(&self) -> String;
#[cm("web:uievents#KeyboardEvent.code", getter)]
fn code(&self) -> String;
#[cm("web:uievents#KeyboardEvent.ctrlKey", getter)]
fn ctrl_key(&self) -> bool;
#[cm("web:uievents#KeyboardEvent.shiftKey", getter)]
fn shift_key(&self) -> bool;
}
User code — typed event handler with downcast:
use { Dom, KeyboardEvent } from "web:dom";
fn setup_search() with Dom {
let doc = Dom::document();
if let Some(input) = doc.query_selector("#search") {
input.add_event_listener("keydown", Option::Some(|e: Event| {
if let Some(ke) = e.downcast::<KeyboardEvent>() {
if ke.key() == "Enter" {
e.prevent_default();
// ... perform search
}
}
})); // options defaults to null
}
}
Fetch (Typed Async)
WebIDL:
dictionary RequestInit {
ByteString method;
HeadersInit headers;
BodyInit? body;
RequestMode mode;
RequestCredentials credentials;
};
interface Response {
readonly attribute boolean ok;
readonly attribute unsigned short status;
readonly attribute ByteString statusText;
readonly attribute Headers headers;
Promise<USVString> text();
Promise<any> json();
Response clone();
};
Generated Wado:
pub struct RequestInit {
pub method: Option<String> = null,
pub headers: Option<Headers> = null,
pub body: Option<String> = null,
pub mode: Option<RequestMode> = null,
pub credentials: Option<RequestCredentials> = null,
}
pub enum RequestMode {
Cors,
NoCors,
SameOrigin,
Navigate,
}
pub enum RequestCredentials {
Omit,
SameOrigin,
Include,
}
#[cm("web:fetch#Response")]
pub resource Response {
#[cm("web:fetch#Response.ok", getter)]
fn ok(&self) -> bool;
#[cm("web:fetch#Response.status", getter)]
fn status(&self) -> u16;
#[cm("web:fetch#Response.statusText", getter)]
fn status_text(&self) -> String;
#[cm("web:fetch#Response.headers", getter)]
fn headers(&self) -> Headers;
#[cm("web:fetch#Response.text")]
fn text(&self) -> Future<String>;
#[cm("web:fetch#Response.json")]
fn json(&self) -> Future<JsValue>;
#[cm("web:fetch#Response.clone")]
fn clone(&self) -> Response;
}
#[cm("web:fetch")]
pub interface Fetch {
#[cm("web:fetch#fetch")]
fn fetch(input: String, init: RequestInit = RequestInit {}) -> Future<Response>;
}
User code:
use { Fetch, Response, RequestInit, RequestMode } from "web:fetch";
fn load_data(url: String) with Fetch {
let resp = Fetch::fetch(url).read(); // init defaults to RequestInit {}
if let Some(r) = resp {
if r.ok() {
let body = r.text().read();
if let Some(text) = body {
log_stdout(`Got: {text}`);
}
}
}
}
fn post_json(url: String, body: String) with Fetch {
let init: RequestInit = {
method: Option::Some("POST"),
body: Option::Some(body),
mode: Option::Some(RequestMode::Cors),
};
let resp = Fetch::fetch(url, init).read();
}
Compared to web-sys:
// web-sys: 12+ lines, untyped JsValue everywhere
let mut opts = RequestInit::new();
opts.method("POST");
opts.body(Some(&JsValue::from_str(&body)));
let request = Request::new_with_str_and_init(&url, &opts)?;
let window = web_sys::window().unwrap();
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
let resp: Response = resp_value.dyn_into()?;
let text_value = JsFuture::from(resp.text()?).await?;
let text: String = text_value.as_string().unwrap();
Canvas2D (Drawing API)
#[cm("web:html#HTMLCanvasElement")]
pub resource HtmlCanvasElement extends HtmlElement {
#[cm("web:html#HTMLCanvasElement.width", getter)]
fn width(&self) -> u32;
#[cm("web:html#HTMLCanvasElement.width", setter)]
fn set_width(&self, value: u32);
#[cm("web:html#HTMLCanvasElement.height", getter)]
fn height(&self) -> u32;
#[cm("web:html#HTMLCanvasElement.height", setter)]
fn set_height(&self, value: u32);
#[cm("web:html#HTMLCanvasElement.getContext")]
fn get_context(&self, context_id: String) -> Option<CanvasRenderingContext2d>;
}
#[cm("web:canvas#CanvasRenderingContext2D")]
pub resource CanvasRenderingContext2d {
fn save(&self);
fn restore(&self);
fn fill_rect(&self, x: f64, y: f64, w: f64, h: f64);
fn stroke_rect(&self, x: f64, y: f64, w: f64, h: f64);
fn clear_rect(&self, x: f64, y: f64, w: f64, h: f64);
fn begin_path(&self);
fn close_path(&self);
fn move_to(&self, x: f64, y: f64);
fn line_to(&self, x: f64, y: f64);
fn arc(&self, x: f64, y: f64, radius: f64,
start_angle: f64, end_angle: f64, anticlockwise: bool = false);
fn fill(&self);
fn stroke(&self);
#[cm("web:canvas#CanvasRenderingContext2D.fillStyle", getter)]
fn fill_style(&self) -> String;
#[cm("web:canvas#CanvasRenderingContext2D.fillStyle", setter)]
fn set_fill_style(&self, value: String);
#[cm("web:canvas#CanvasRenderingContext2D.strokeStyle", getter)]
fn stroke_style(&self) -> String;
#[cm("web:canvas#CanvasRenderingContext2D.strokeStyle", setter)]
fn set_stroke_style(&self, value: String);
#[cm("web:canvas#CanvasRenderingContext2D.lineWidth", getter)]
fn line_width(&self) -> f64;
#[cm("web:canvas#CanvasRenderingContext2D.lineWidth", setter)]
fn set_line_width(&self, value: f64);
fn fill_text(&self, text: String, x: f64, y: f64, max_width: Option<f64> = null);
#[cm("web:canvas#CanvasRenderingContext2D.font", getter)]
fn font(&self) -> String;
#[cm("web:canvas#CanvasRenderingContext2D.font", setter)]
fn set_font(&self, value: String);
}
User code:
use { Dom } from "web:dom";
use { HtmlCanvasElement, CanvasRenderingContext2d } from "web:canvas";
fn draw() with Dom {
let doc = Dom::document();
if let Some(canvas) = doc.query_selector("#myCanvas") {
if let Some(c) = canvas.downcast::<HtmlCanvasElement>() {
if let Some(ctx) = c.get_context("2d") {
ctx.set_fill_style("red");
ctx.fill_rect(10.0, 10.0, 100.0, 50.0);
ctx.begin_path();
ctx.arc(75.0, 75.0, 50.0, 0.0, f64::PI * 2.0); // anticlockwise defaults to false
ctx.set_fill_style("blue");
ctx.fill();
ctx.set_font("24px serif");
ctx.fill_text("Hello Wado!", 50.0, 150.0); // max_width defaults to null
}
}
}
}
Mixins (Trait Composition)
WebIDL:
interface mixin ParentNode {
readonly attribute HTMLCollection children;
Element? querySelector(DOMString selectors);
NodeList querySelectorAll(DOMString selectors);
};
Document includes ParentNode;
Element includes ParentNode;
DocumentFragment includes ParentNode;
Generated Wado:
pub trait ParentNode {
fn children(&self) -> HtmlCollection;
fn query_selector(&self, selectors: String) -> Option<Element>;
fn query_selector_all(&self, selectors: String) -> NodeList;
}
impl ParentNode for Document { /* generated delegation to host */ }
impl ParentNode for Element { /* generated delegation to host */ }
impl ParentNode for DocumentFragment { /* generated delegation to host */ }
Mixins are the one place where Wado traits shine over resource extends: a mixin can be applied to unrelated types that don't share an inheritance chain.
Union Types (Variant Generation)
WebIDL unions become Wado variants:
// WebIDL
typedef (Node or DOMString) NodeOrString;
// Generated Wado
pub variant NodeOrString {
AsNode(Node),
AsString(String),
}
Common unions get readable names from their WebIDL typedef. Unnamed unions get auto-generated names from constituent types.
Output File Structure
web/
├── dom/
│ ├── types.wado # EventTarget, Node, Element, Document, Window
│ ├── events.wado # Event, MouseEvent, KeyboardEvent, UIEvent
│ ├── html_elements.wado # HTMLElement, HTMLInputElement, HTMLDivElement, ...
│ └── effect.wado # pub effect Dom { ... }
├── dom.wado # flat re-export: pub use { ... } from "web:dom/types.wado";
├── fetch/
│ ├── types.wado # Request, Response, Headers, RequestInit
│ └── effect.wado # pub effect Fetch { ... }
├── fetch.wado
├── canvas/
│ ├── types.wado # CanvasRenderingContext2D, Path2D, ImageData
│ └── effect.wado
├── canvas.wado
├── websocket/
│ ├── types.wado # WebSocket, CloseEvent, MessageEvent
│ └── effect.wado
├── websocket.wado
├── url/
│ └── types.wado # URL, URLSearchParams
├── url.wado
└── ...
Organized by W3C/WHATWG specification, mirroring how wasi/ is organized by WASI interface.
Comparison Summary
| Pain Point | web-sys (Rust) | Wado (wado-from-idl) |
|---|---|---|
| Type casting | el.dyn_into::<HtmlInputElement>()? |
el.downcast::<HtmlInputElement>() → Option |
| Closure boilerplate | Closure::wrap(Box::new(...)).forget() |
\|e: Event\| { ... } — native GC closures |
| Untyped promises | JsFuture → JsValue (untyped) |
Future<Response> (typed) |
| Overload naming | fetch_with_str_and_init() |
fetch(url) or fetch(url, init) |
| Feature flags | features = ["Element", "Node", ...] |
None needed |
| Inheritance | Deref chain (anti-pattern) |
resource extends (first-class) |
| Effect tracking | None (side effects implicit) | with Dom, with Fetch (explicit) |
Consequences
Language Changes Required
resource extendssyntax — new compiler feature for resource inheritance.#[cm("...")]attribute — Component Model bindings with namespace in the value string.downcast::<T>()built-in on resources withextends.- Implicit upcast coercion for resource reference types.
Dependencies
@webref/idlnpm package as source of truth for WebIDL definitions.wado-from-idlcrate (shared IR and codegen with WIT frontend).- Host runtime (jco or similar) must provide
instanceofchecks for downcast.
Not In Scope
- Hand-tuned high-level APIs (e.g., ergonomic DOM builder DSL) — may come later as a separate library on top of the generated bindings.
- Web Components / Custom Elements support.
- Service Workers / Web Workers API design.
- Named parameters — a separate Wado language WEP.
