Programming Servo: A generic “worker event-loop”
--
Let’s take a look at how in Rust you can have an algorithm generic over T, where T is further bound by a trait, which itself is generic over one of several parameters, as used in the Servo codebase.
A little introduction to workers and their event-loops in the web
The HTML living standard tells us the following with regards to the event-loops of an user agent:
To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. There are two kinds of event loops: those for browsing contexts, and those for workers.
A bit more on the worker event-loops:
Each
WorkerGlobalScope
object has a distinct event loop, separate from those used by units of related similar-origin browsing contexts. This event loop has no associated browsing context, and its task queues only have events, callbacks, and networking activity as tasks. These event loops are created by the run a worker algorithm.
Furthermore, there are several kinds of workers:
There are two kinds of workers; dedicated workers, and shared workers.
And the W3c also defines a “service worker.”
Lastly, the living standard also writes that “The global scope is the “inside” of a worker”.
Workers in Servo
As a result of the above, Servo ended-up with several worker “global scopes”, each with their own “run the event-loop” algorithm. While working on a related issue, I realized that I was often making the same changes in both of those worker event-loops, which was error prone. So I set to out to turn that code into one “generic worker event-loop”.
This proved harder then I though, because what I needed was a run_worker_event_loop<T>
sort of algorithm, where T
was a type of “Worker Globalscope” and a replacement for &self.
But since &self
was previously used to access some capabilities of the “Worker Globalscope”, I would need T
to implement a trait giving run_worker_event_loop
access to these capabilities of T
.
Yet those capabilities themselves required to be generic because each event-loop consisted of handling different messages on different channels, and then handling those messages as different type of event-loop specific events.
Furthermore, those run_event_loop
differed in some ways, for example because the “dedicated worker global scope” takes this worker
argument, and handles it as part of each iteration of the event-loop, whereas the “service worker global scope” lacks that concept.
At first, I tried to have some sort of “double generic” setup looking like run_worker_event_loop<T<TimerMsg, WorkerMsg, Event>>
, where I wanted to express something like run_worker_event_loop
is generic over T
, which itself is generic over <TimerMsg, WorkerMsg, Event>
, and the Rust compiler didn’t seem to appreciate that approach a lot.
In the end, the solution came in the form of a run_worker_event_loop<T, TimerMsg, WorkerMsg, Event>
, where TimerMsg, WorkerMsg, Event
are arguments passed on to the underlying trait bound on T: WorkerEventLoopMethods<TimerMsg = TimerMsg, WorkerMsg = WorkerMsg, Event = Event>
. Let’s take a look into what that exactly means…
A trait
So our “worker global scope” T
, will have to implement WorkerEventLoopMethods
.
The trait itself is generic with regards to three “associated types” TimerMsg, WorkerMsg, Event
.
This gives the generic event-loop a few ‘hooks’ to access the specific capabilities of each “worker global scope”, which are needed for the parts of the event-loop that are specific to the “worker global scope” at hand.
Those ‘hooks’ themselves require a few generic parameters, because each “worker global scope” receives different types of messages, and expects different types of events resulting from those.
Here is what an implementation looks like:
As you can see, all that the implementation does is “make available” that which is specific to the DedicatedWorkerGlobalScope
, it doesn’t contain any event-loop logic, just the stuff that the generic event-loop will need to specific it’s behavior for a given “worker global scope”.
A generic worker event-loop
At the end of the day, we get a generic event-loop logic, using WorkerEventLoopMethods
for the parts that are worker-specific.
Through our use of associated items on the trait, we’re able to express the fact that T
is a struct implementing a generic trait, in a way that the compiler appreciates…