How to implement a new DOM API for Servo
Servo is an awesome project about making a web browser engine in Rust! It is really well maintained with friendly contributors and mentors. Because Servo is a new web browser engine, there are so many relatively easy tasks waiting for new contributors. One of the things you as a contributor can do is implementing a DOM API for Servo.
Malisa and I implemented a few DOM APIs over the summer. There are lots of great documentation on how to get started, but we still felt a bit lost in the beginning. So we decided to make a tutorial for people who are relatively new to Servo and want a little more concrete example on implementing a new DOM API for Servo.
In this blog post, I will implement the up and coming DOM API called ✨Doge✨. This API has a spec that describes the standard Doge API should follow. You should give it a very brief skim before continuing.
Doge is a relatively simple DOM API. You can create a new Doge object that has an associated such list. If an initializer is given, that will be used to populate the new Doge. You can add more words to such list through the append
method. You can get a random word from such list through the random
method. Simple! So, how do we start?
Architecture
These are the most relevant directories for us. script
is a crate, which includes a module dom
. webidls
hosts all the Web IDL files for the DOM APIs:
Web IDL
First, we should get the Web IDL that is in the spec. Copy them, and put them in componenets/script/dom/webidls/Doge.webidl
. For now, we’ll comment out the methods for Doge in the Web IDL. It’s easiest to comment them all out, and then implement one method at a time. It will look like below after adding the license and the spec link:
If this is the first time adding a Web IDL to Servo from this specific domain, make sure to add it to python/tidy/servo_tidy/tidy.py
so that test-tidy
will know it’s a valid Web IDL standard.
Listing Doge as a submodule
doge.rs
is a submodule under dom
so we should make it discoverable by the rest of Servo. We do so by listing the submodule in the lib.rs
or mod.rs
at the root of the module. In our case, we would add the following line to componenets/script/dom/mod.rs
:
Implementing Doge
Doge is a dom struct and has an associated such list. So we can write that like the following in components/script/dom/doge.rs
:
Because everything we write will be in Rust but the web speaks JavaScript, we need something that can connect the JS world and the Rust world. This will be reflector_
, which provides a pointer to the JS object. We want such_list
to be able to be modified, so we’ll allow interior mutability by wrapping it with DOMRefCell
. DOMRefCell
is like RefCell
, but it enables JS garbage collector tracing.
Let’s write the functions required for creating a new Doge object!
impl Doge
We need three functions for creating a new Doge object: new_inherited
, new
and Constructor
. Constructor
is called by the JS world, which calls new
, which in turn calls new_inherited
. Let’s work on new_inherited
, new
, and then Constructor
.
new_inherited
: This is where an instance of the Doge struct is created. Sometimes, parameters may be used here, but Doge doesn’t require any (because Doge is a strong independent doggo💕):
new
: This is where a connection between the JS world (global
) and the Rust Doge object is created. It does so by calling a functionreflect_dom_object
, which takes three parameters including one calledDogeWrap
.DogeWrap
is a DOM bindings generated code (more on that later) that sets up the JS reflector (reflector_
) for the Doge type.new
method returns a JS Rooted object (Root<Doge>
), which will prevent the JavaScript garbage collector from collecting this object while it’s used in the Rust world.
Constructor
: This function constructs the new Doge object, and will add some fields if an initializer (init
) is given. In our case, Doge can be given aDogeInit
that is a sequence ofDOMString
. The spec for the constructor method states that:
- Let doge be a new
Doge
object.- If init is given, for each word in init, append word to the associated such list.
- Return doge.
So, we follow the spec, and write the constructor! Constructor()
must return the Fallible
type, which is one of Servo’s custom Result
types.
We can almost create a Doge object. This won’t compile yet, because the append method is not implemented. Awesome! Let’s start writing the Doge methods.
impl DogeMethods for Doge
append
method
Let’s uncomment the append
method from the Web IDL.
Now we’ll try to compile. This will definitely fail! However, the DOM bindings generator creates a code related to Doge. You can find that in target/debug/build/script-[hash]/out/Bindings/DogeBinding.rs
or something like that. This is also where you can find DogeWrap
, which I mentioned earlier. Search for DogeMethods
, and it will show:
Oh how neat! Now we know exactly how to start writing the append
method.
The spec for append states that:
The
append(word)
method, when invoked, must appendword
to the associated such list.
So we know that we should write the method like the following:
One down, one more to go!
random
method
The spec states:
The
random()
method, when invoked, must run these steps:
- Let list be the associated such list.
- If list is empty, throw a
TypeError
.- Return a randomly chosen word from list.
Because random
method can throw an error, we should specify it in our Web IDL. Add [Throws]
in front of random
like this:
Let’s now write the random
method per spec:
That’s it! We implemented the Doge API for Servo. Let’s try it out!
Wow! Such amazing! Very cool.
To see the full implementation, check out the doge commit in my Servo fork.
This tutorial does not cover everything related to implementing a DOM API, but I hope this helped you get started! Sometimes the spec will be unclear, and sometimes it will be impossible to implement an API exactly as the spec states. When you encounter a situation like that or when you need help, reach out to Servo and spec maintainers! They are friendly and helpful people. 😊
Malisa wrote a blog post about the DOM API testing process in Servo, so read about it!
Many thanks to Josh Matthews (jdm
) for reviewing the blog post.