-
Notifications
You must be signed in to change notification settings - Fork 0
JS smart pointers
DOM objects have special lifetime requirements due to their interactions with the JavaScript engine, which requires us to use smart pointers to encapsulate them. That may sound complicated, but it's actually very easy in practice. JS<T>
types should be passed by reference when possible, and cloned when necessary (this only clones the pointer container, not the underlying DOM object).
fn do_something(node: &JS<Node>) {
node.get().parent_node().map(|parent| ...);
}
fn do_some_mutation(node: &mut JS<Node>) {
node.get_mut().something = Some(5);
}
The get
and get_mut
methods will return a borrowed reference to the wrapped DOM object, allowing you to interact with it as you would expect.
Let's say you have a JS<Element>
type. Node
is a parent type of Element
, and HTMLTitleElement
is a derived class of Element. Therefore, we can do the following:
let base: JS<Node> = NodeCast::from(element);
if base.get().type_id() == ElementNodeTypeId(HTMLTitleElementTypeId) {
let derived: JS<HTMLTitleElement> = HTMLTitleElementCast::to(element);
}
Downcasting is an unsafe operation, so you must be confident that the cast will succeed or task failure will occur.
This is not possible, unfortunately, and is the reason for the abstract_self
arguments in many methods. To obtain a JS type for a self value, you need to modify Bindings.conf and add the method to a needsAbstract
list for the type (see the configuration for Node
for examples), which will add an abstract_self
argument to the method argument list - this corresponds to the self value wrapped in a JS<T>
smart pointer. Any non-self values must be passed in by callers; in general, prefer passing &JS<T>
instead of &T
.
The current implementation is the following:
When Servo creates a Rusty DOM object, the binding code creates a wrapper JSObject
with SpiderMonkey, is correspond to each Rusty DOM Object. It’s produced and set to the Rusty object in FooBinding::Wrap
.
In FooBinding::Wrap
, the wrapper JSObject gets the pointer for Rusty Object to itself. And the same time, the wrapper JSObject
are set to the Rusty Object’s Reflector
field (All Rusty DOM objects have dom::bindings::utils::Reflector
in their most basis field). These step are the “binding” work to create the relationship of both objects.
This is very tricky and magically mechanism helped by Rust Compiler. The outline is:
- SpiderMonkey's GC calls
JSClass.trace
defined inFooBinding
when marking phase. This JSClass is basis of each wrapper JSObject. -
JSClass.trace
callsFoo::trace()
defined in InhertTypes.rs. -
Foo::trace()
callsFoo::encode()
. Thisencode()
method is derived by the annotation of#[deriving(Encodable)]
for a Rust DOM Element struct. -
Foo::encode()
callsJS<T>::encode()
method ofJS<T>
which is contained toFoo
’s member. So this is the compiler magic! Rust compiler generates codes like this for all structs annotated#[deriving(Encodable)]
. This is based on the assumption. -
JS<T>::encode()
callsdom::bindings::trace::trace_reflector()
. -
trace_reflector()
fetches the reflector that is reachable from a Rust object, and notifies it to the GC with using JSTracer. - This operation continues to the end of the graph.
- Finally, GC gets whether Rust object lives or not from JSObjects which is hold by Rust object.
When destructing DOM objects (wrapper JSObjects) by SpiderMonkey, SpiderMonkey calls the JSClass.finalize()
which is basis of each wrapper JSObject
s. This method refers each FooBinding::_finalize()
.
In this function, the pointer of Rusty DOM Object that is contained in the wrapper JSObject is unwrapped, it cast to Rust owned pointer, and we assign its owned pointer to the empty local variable of FooBinding::_finalize()
. Thus we can destruct the Rusty Object after we left from it.