Open fitzgen opened 6 years ago
https://github.com/rail44/squark I'm working and trying to resolve https://github.com/rustwasm/wasm-bindgen/issues/42 🤔
Neat! Although I don't see any mention of web components, so I think it may be orthogonal to this issue...
I'm working and trying to resolve rustwasm/wasm-bindgen#42
Awesome! Do you have any particular questions or anything? Probably best to move this part of the discussion into that issue.
I don't see any mention of web components
Oh, sorry 😅 Currently, I use stdweb for binding to web browser. I think, Importing WebIDL is necessary to provide more binding for browser world such as web component.
Hi,
I want to give a try on this but I'm quickly stuck on few things. My first try is simple:
Here are my questions:
define
expect a js_sys::Function
as argument. How can I retreive the JS prototype of the class created in the first step?constructor
. I don't see how a struct can extend web_sys::Element
nor web_sys::HTMLElement
as rust do not have inheritance. Hi @ctjhoa! Excited to see some movement here :)
The design that I think makes sense is to have the actual HTMLElement
subclass be a JavaScript class that creates a the inner #[wasm_bindgen]
-exposed Rust struct in the connectedCallback
and then frees it in the disconnectedCallback
. E.g. https://rustwasm.github.io/sfhtml5-rust-and-wasm/#74
This approach saves users from having to manage memory and lifetimes of objects themselves. It also side steps some of your questions above.
Also, the primary interface between a web-component and the outside world is the custom element's attributes. It would be A+ if we could provide a serde deserializer from attributes to rich rust types that the web component uses (or at minimum does things like parse integers from attribute value strings).
Are you available to come to the next WG meeting? If possible, it would be great to have some high-throughput design discussion on this stuff :) https://github.com/rustwasm/team/issues/252
@fitzgen It's not feasible to use connectedCallback
and disconnectedCallback
to manage Rust lifetimes, because they aren't actually tied to the lifecycle of the DOM node, and both callbacks can be called multiple times:
https://codepen.io/Pauan/pen/961b58f8fc23677268ad11f37e3c6cc9
(Open the dev console and see the messages)
As you can see, every time a DOM element is inserted/removed, it fires the connectedCallback
/disconnectedCallback
.
This happens even when merely moving around the DOM node (without removing it).
Unfortunately I don't see a clean way to fix this. You mentioned (during the WG meeting) using setTimeout
(or similar), which would technically work in some situations, but not others. It also feels very hacky.
So I think we're still stuck with manual memory management, manually calling a free
method (or similar).
@Pauan, thanks for making an example and verifying this behavior!
I still think a delayed (and cancellable!) destruction is what we want here, because of the superior UX. Custom elements are supposed to "just" be another element, and they are supposed to completely encapsulate themselves without requiring users do anything that "normal" elements require.
I think using requestIdleCallback
would actually fit this use case really well:
import { RustCustomElement } from "path/to/rust-custom-element";
class CustomElement extends HTMLElement {
constructor() {
this.inner = null;
this.idleId = null;
if (this.isConnected) {
this.inner = new RustCustomElement(/* ... */);
}
}
connectedCallback() {
if (!this.inner) {
window.cancelIdleCallback(this.idleId);
this.idleId = null;
this.inner = new RustCustomElement(/* ... */);
}
}
disconnectedCallback() {
this.idleId = window.requestIdleCallback(() => {
const inner = this.inner;
this.inner = null;
inner.free();
});
}
}
The other option would be to require the state to be serializable. That way you can store all the state directly on the component (or in a WeakMap) and you don't actually need to free anything.
@fitzgen I still think a delayed (and cancellable!) destruction is what we want here, because of the superior UX. Custom elements are supposed to "just" be another element, and they are supposed to completely encapsulate themselves without requiring users do anything that "normal" elements require.
I agree that's a good goal, I'm just not seeing a clean way to accomplish that.
If a user removes the custom element from the DOM, then waits for a bit (perhaps using setTimeout
, or perhaps waiting for an event), and then re-inserts it into the DOM, then things will be broken since the Rust objects will have been freed.
An unusual case, sure, but it definitely can happen, and I can even imagine use-cases for it (e.g. a modal dialog which is only inserted into the DOM while the modal is open, and is otherwise detached from the DOM).
So proper support probably requires WeakRef or similar.
Having said that, we can totally experiment with custom elements even without WeakRef (just with the above caveat).
Also, as for encapsulation, custom elements can actually have custom methods, which the consumer can then access:
class Foo extends HTMLElement {
myMethod() {
...
}
}
customElements.define("my-foo", Foo);
var foo = document.createElement("my-foo");
foo.myMethod();
So I don't think it's that unusual to call methods on custom elements. I imagine that's probably the preferred way to update the custom element's internal state from the outside.
In addition, because JS doesn't have finalizers, I think there will be custom elements (written entirely in JS) which require a free
method (or similar), for the sake of cleaning up event listeners (and other resources which can't be claimed by the JS garbage collector).
So overall I don't think it's that unusual to have a free
method for custom elements, but I guess we should wait and see how the ecosystem evolves.
If a user removes the custom element from the DOM, then waits for a bit (perhaps using
setTimeout
, or perhaps waiting for an event), and then re-inserts it into the DOM, then things will be broken since the Rust objects will have been freed.
The reinsertion will trigger a new Rust object to be created, so this won't result in a bug. See the if (!this.inner)
check in the connectedCallback
in the JS snippet in my previous comment.
Yes, there can be multiple Rust objects used across the lifetime of the JS custom element: the idea is that creating and destroying the inner Rust thing on every attach or detach is the baseline, and the delay is an optimization to cut down on thrashing when just moving the element instead of removing it.
Unless I am misunderstanding what you are saying?
So I don't think it's that unusual to call methods on custom elements. I imagine that's probably the preferred way to update the custom element's internal state from the outside.
I think the usual way is via setting attributes (eg same as value, min, and max for <input type="range"/>
) unless there are Other Reasons where that doesn't make sense.
Ultimately, yes we want finalizers, and we can actually polyfill it now. Which is something we need to get published...
@fitzgen Ah, okay, I had (incorrectly) assumed there would be a 1:1 relationship between the Rust struct and the custom element.
If you instead make it N:1 then yeah, it can just dynamically create/destroy the Rust objects on demand. I don't think that'll work for every use case, but it should work for most.
I'm a bit concerned about the user's expectations though, I think other users will also expect a 1:1 relationship.
As for attributes, (at least in HTML) those are primarily used for static pre-initialized things. For dynamic things, users instead use setters/methods like foo.min = 5
. The same goes for adding event listeners (and other things).
Since custom elements can listen to attribute changes, they can respond to dynamic attribute changes, but using foo.setAttribute("bar", "qux")
seems rather inconvenient and unidiomatic compared to foo.bar = "qux"
(or similar).
Since getters/setters/methods are so common with regular DOM elements, I expect them to be similarly common with custom elements (I don't have any statistics or experience to back that up, though).
I've made a proof of concept
https://github.com/ctjhoa/rust-web-component
It uses requestIdleCallback
to free rust resources.
I've tried to use other techniques with WeakMap
& WeakSet
without success.
So what's going on in this project.
Seems like any wasm module just needs a JS custom element glue class to call into the module (to trigger the lifecycle hooks). Even if wasm gets ability to reference DOM elements in the future, there will be no way to avoid the JS glue class (to pass into customElements.define()
), unless the Custom Elements API evolves to accept wasm modules and not just JS classes.
Can't you create the "class" from rust creating a function object changing the prototype, etc, etc?
@olanod That still requires JS glue to create the function object (and change the prototype).
Secondly, as far as I know, it's not possible to use ES5 classes for custom elements:
function Foo() {
console.log("HI");
}
Foo.prototype = Object.create(HTMLElement.prototype);
customElements.define("my-foo", Foo);
// Errors
var x = document.createElement("my-foo");
This is because ES5 classes cannot inherit from special built-ins like Array
, RegExp
, Error
, or HTMLElement
.
But ES6 classes were specifically designed so that they can inherit from built-ins. So ES6 classes aren't just a bit of syntax sugar, they actually have new behavior.
However, even given the above, we only need a single JS function to create all of the classes (this was mentioned by @trusktr ):
export function make_custom_element(parent, observedAttributes, connectedCallback, disconnectedCallback, adoptedCallback, attributeChangedCallback) {
return class extends parent {
static get observedAttributes() { return observedAttributes; }
connectedCallback() {
connectedCallback();
}
disconnectedCallback() {
disconnectedCallback();
}
adoptedCallback() {
adoptedCallback();
}
attributeChangedCallback(name, oldValue, newValue) {
attributeChangedCallback(name, oldValue, newValue);
}
};
}
Now wasm can just call the make_custom_element
function.
In fact, it should be possible to do that right now, no changes needed to wasm-bindgen.
These might be interesting, if not already known: https://github.com/webcomponents/polyfills/tree/master/packages/custom-elements
With https://github.com/rustwasm/wasm-bindgen/pull/1737, it's possible to do:
#[wasm_bindgen(prototype=web_sys::HtmlElement)]
struct MyCustomElement {}
#[wasm_bindgen]
impl MyCustomElement {
#[wasm_bindgen(constructor)]
fn new() -> WasmType<MyCustomElement> {
instantiate! { MyCustomElement{} }
}
}
// ...
web_sys::window()
.unwrap()
.custom_elements()
.define("my-custom-element", &js_sys::JsFunction::of::<MyCustomElement>())?;
// ...
Any thoughts or input into that PR and/or the (early draft) RFC for which it's a prototype would be greatly appreciated!
@eggyal Oh no! What happened with your draft?
What would it take to get this train moving again? I could really use the ability to extend and implement AudioWorkletProcessor
in Rust.
It would be super cool if someone wrote a slim library for writing web components and custom elements in rust!
I'm imagining the library would have a trait, and if I (the library user) implement that trait for my type, then the library wires up all the glue to turn that trait implementation into a web component.
A great way to integrate nicely with the JS ecosystem! A website could use these web components without even needing to know that it was implemented in rust and wasm.
A potential web component to implement with this library might be a graphing/charting library that has to churn through a lot of data before displaying it.