WebAssembly / WebAssembly/component-model

What's the best way to model OOP-style inheritance in WIT?

Open
#564 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
WebAssembly
Stars
1.4k
Forks
130
Avg merge
2d 1h
Merged PRs (30d)
15

Description

Situation: I want to map the JS DOM Node, Element, HTMLElement, etc. classes to WIT somehow so that I can import them from the JS host in my Rust WASM Component. These DOM classes have an inheritance hierarchy. I am unsure how to best map these concepts to WIT resource types.

yes i could just use wasm_bindgen ecosystem but this is more hypothetical experiment-y than practical i-need-this-for-a-business-project

Option 1: T::cast_from_super(u) and t.cast_to_super() owned to owned conversions
  • avoids duplicating methods for each class in the chain
  • lots of consuming conversions to go back and forth
  • the most barebones of the bunch; also the most cross-wasm func calls
  • i dont think WIT can do methods that consume (not borrow) the self param so these would be static methods i guess
// document.querySelector() returns Element | null
let element = document.query_selector("input[name=username]").unwrap();
let html_element = HtmlElement::cast_from_super(element).unwrap();
let html_input_element = HtmlInputElement::cast_from_super(html_element).unwrap();
// use the <input> tag with things like .value or .reportValidity()
html_input_element.get_value();
html_input_element.report_validity();
// cast back to Element to use Element.prototype.id
let html_element = html_input_element.cast_to_super();
let element = html_element.cast_to_super();
element.get_id();
element.set_id("username");
Option 2: T::cast_from_super(u) and t.cast_to_super() owned to owned conversions but with inherited methods duplicated on extended classes
  • lots of duplicating base class methods for all classes that extend it
  • less cast_to_super, still walk the cast_from_super chain
  • EACH METHOD on EACH CHILD TYPE which can be A LOT
// document.querySelector() returns Element | null
let element = document.query_selector("input[name=username]").unwrap();
let html_element = HtmlElement::cast_from_super(element).unwrap();
let html_input_element = HtmlInputElement::cast_from_super(html_element).unwrap();
// use the <input> tag with things like .value or .reportValidity()
html_input_element.get_value();
html_input_element.report_validity();
// DON'T cast back to Element to use Element.prototype.id
html_input_element.get_id();
html_input_element.set_id("username");
Option 3: T::from_U(&) and (&t).as_U() borrowed to owned-copy methods
  • two owned resources for the same underlying value -- can't return borrow<T> iirc?
  • can skip to the end of the inheritance chain in one function instead of walking it manually
// document.querySelector() returns Element | null
let element = document.query_selector("input[name=username]").unwrap();
let html_input_element = HtmlInputElement::from_element(element).unwrap(); // see also from_html_element() and from_node()
// use the <input> tag with things like .value or .reportValidity()
html_input_element.get_value();
html_input_element.report_validity();
// reuse element to use Element.prototype.id
element.get_id();
// or create another owned copy by downcasting
let element2 = html_input_element.as_element();
element2.set_id("username");
do_something_with_element(element); // takes owned Element instance, not HtmlInputElement
// somehow we still have our own owned HtmlInputElement instance AND element2
Option 4: T::from_U(&) and (&t).as_U() borrowed to owned-copy methods but with inherited methods duplicated on all extending classes
  • still two owned resources
  • basically only from_U() unless need to make T into U for a method argument
  • EACH METHOD on EACH CHILD TYPE which can be A LOT
// document.querySelector() returns Element | null
let element = document.query_selector("input[name=username]").unwrap();
let html_input_element = HtmlInputElement::from_element(element).unwrap(); // see also from_html_element() and from_node()
// element is still owned too!
// use the <input> tag with things like .value or .reportValidity()
html_input_element.get_value();
html_input_element.report_validity();
// use Element.prototype.id dup'd on all classes that extend Element
html_input_element.get_id();
html_input_element.set_id("username");
do_something_with_element(element); // takes owned Element instance, not HtmlInputElement
// somehow we still have our own owned HtmlInputElement instance
Option 5: T::from_U(u) and t.into_U() owned to owned conversion methods for ALL parent class types
  • quicker shortcuts than option 1
  • still have to cast back and forth for inherited methods
// document.querySelector() returns Element | null
let element = document.query_selector("input[name=username]").unwrap();
let html_input_element = HtmlInputElement::from_element(element).unwrap(); // see also from_html_element() and from_node()
// use the <input> tag with things like .value or .reportValidity()
html_input_element.get_value();
html_input_element.report_validity();
// cast back to Element to use Element.prototype.id
let element = html_input_element.into_element(); // see also into_html_element() and into_node()
element.get_id();
element.set_id("username");
Option 6: T::from_U(u) and t.into_U() owned to owned conversion methods for ALL parent class types
  • most ergonomic i think; also minimal cross-wasm func calls
  • have to dup for each conversion from_T() and into_T()
  • EACH METHOD on EACH CHILD TYPE which can be A LOT
// document.querySelector() returns Element | null
let element = document.query_selector("input[name=username]").unwrap();
let html_input_element = HtmlInputElement::from_element(element).unwrap(); // see also from_html_element() and from_node()
// use the <input> tag with things like .value or .reportValidity()
html_input_element.get_value();
html_input_element.report_validity();
// DON'T cast back to Element to use Element.prototype.id
html_input_element.get_id();
html_input_element.set_id("username");
do_something_with_element(html_input_element.into_element()); // see also into_html_element() and into_node()

Option 7? are there other ways im missing?

i havent manually tested making bindings and running each of these rust code options in a js runtime that provides these import to it (too much effort). im looking for some feedback on how to model the general "class T extends U" scenario in WIT. this is just an example scenario that im having right now. Which one of these best fits the WIT vibes? are the "shortcut"-style methods encouraged in WIT (since they incur an extra across-component-boundary func call) or is that so fast nowadays that it shouldnt matter? what about if its worth the extra bindings size to duplicate the entire HTMLElement prototype attribute list (60+ getter/setter methods) for EVERY SINGLE HTML<something>Element subclass -- should that be done in the guest language (impl in Rust instead of in WIT)?

looking for more opinions

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the WIT resource types and Rust WASM Component examples described in the issue, then compare the seven inheritance and conversion options against WIT's resource and borrowing model. Review the single existing comment for context. Done means documenting a decided recommendation for modeling the general T extends U case, including guidance on conversions, duplicated methods, and cross-component calls.

Written by the indexing model from the issue text.

Assessment

Tech stack
wasm
Domain
compilers
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.