Skip to content

Servo and SpiderMonkey Report

Gregory Terzian edited this page Apr 19, 2024 · 9 revisions

Servo and SpiderMonkey

Introduction

Servo is a web engine — a piece of software written in Rust and implementing a host of standards which together form the web platform — which relies on SpiderMonkey, itself written in C++, for its script execution capabilities. This report will provide an overview of Servo's integration of SpiderMonkey, and provide an outlook on improving the modularity of that integration.

Script execution and the web

SpiderMonkey in and of itself is unrelated to the web platform; rather, it is an implementation of the ECMAScript specification, as well as of Wasm-related specifications such as the Wasm JS interface, core Wasm, and the Wasm Web API. Those capabilities are then integrated into Servo, the engine implementing the web platform as specified by core standards such as HTML, specifications providing various types of infrastructure shared across the web platform, such as Web IDL, and a host of additional specifications providing peripheral capabilities to the web platform, such as WebGPU.

The integration of SpiderMonkey into Servo takes place at various levels of granularity, resulting in an API surface that does not map to the various specifications which Servo and SpiderMonkey are meant to implement, and resulting in a tight coupling between the two. A good example of such tight coupling is Servo's implementation of the Streams standard, which, while not being part of ECMAScript but rather part of the web platform, relies on an implementation provided (and now deprecated) by SpiderMonkey itself.

SpiderMonkey: a modern script engine

SpiderMonkey is the JavaScript and Wasm implementation used in Firefox. As Firefox is a modern browser with a feature set similar to Chrome (differences between Chrome 124 and Firefox 125), we can deduce that SpiderMonkey is a modern script execution engine with a feature set similar to V8, the script engine used in Chrome.

Online forums often contain comments about V8 outperforming SpiderMonkey, but a more in-depth look appears to relegate this idea to the urban myth. The often cited performance testing results are based on older so-called synthetic test suites which have since been discredited. An example is Octane, a retired test-suite for which V8 was particularly tuned. The problem however was that this tuning was a form of over-optimization which often would slow down real-world browsing use cases. Modern performance test suites such as Speedometer are aimed at a holistic measurement of real-world browsing use cases. The SpiderMonkey, and broader Firefox, team targets the results of these benchmarks (e.g. for React and Google Docs).

While performance and feature sets are bound to differ when comparing specific releases, generally speaking SpiderMonkey appears just as capable and well-maintained as V8.

Servo's integration of SpiderMonkey

Servo being written in Rust, integration with the C++ codebase of SpiderMonkey happens via a crate of Rust bindings. These bindings consists of two parts:

  • mozjs-sys — low-level Rust bindings to the C++ API. In Servo these are used as use js::jsapi, use js::jsval, and use js::glue.
  • mozjs — higher-level bindings that hide the SpiderMonkey API behind an idiomatic Rust API. In Servo these are used as use js::rust.

Servo uses both parts of these bindings, additional code found in Servo itself at components/script/dom/bindings, and other code which is not part of the repository but rather generated as part of the build process. The generated code deals with mundane low-level matters that make Rust objects integrate with the SpiderMonkey runtime (in JS and Wasm) and vice-versa, while the manually written code found in components/script/dom/bindings consists of utilities used across Servo's script crate that make it easier to integrate the web platform with SpiderMonkey. In particular, integration with the SpiderMonkey garbage collector is an important concern - and one which in Servo follows an innovative design where the lifetime of Rust objects is managed by the JS garbarge collector - which is best dealt with through shared utilities to avoid programming mistakes. Servo's large set of utilities and code generation capabilities result from the initial investment made by Mozilla.

One issue with these capabilities, probably because it was unthinkable within Mozilla to use anything but SpiderMonkey, is their tight coupling to SpiderMonkey. Another issue is that, despite the utilities covering much ground, the Web Platform code in Servo's script crate often still uses the low-level SpiderMonkey API directly via js::jsapi — defeating some of the benefits in terms of avoiding programming errors provided by the more general utilities, and resulting in further tight-coupling.

The problem: lack of modularity

Servo's implementation of the Web platform is peppered with unsafe calls to js::jsapi: unsafe, non-idiomatic, and tightly coupled to the SpiderMonkey API. The utilities and generated code are also tightly coupled, but that is at least hidden from their point of use in the rest of the script crate — making it easier to reduce and eventually remove that coupling.

On the bright side, we have:

  • An existing Web standard that defines a clean interface between script execution and the rest of the Web platform: Web IDL.
  • Some integration with SpiderMonkey that is safe, idiomatic, and hides the SpiderMonkey API: see js::rust and the various utilities in components/script/dom/bindings.
  • Large part of the integration with SpiderMonkey that is generated automatically, through Rust code generated by a Python script found in components/script/dom/bindings/codegen.
  • Servo's integration with SpiderMonkey being the only "browser grade" integration (in terms of security and features) with a script execution engine in Rust land. Compare this with the problems experienced by the Deno project with their own script integration, for example, the absence of host objects, or the challenges encountered to support important features such as ExternalArrayBuffer.

Prior art: JSI and React Native

An existing effort to provide a common interface to any JS engine is React Native's JavaScript Interface (JSI), which is described as "a lightweight, general-purposed API for embedding a JavaScript engine into a C++ program". It allows React Native to run JS using different JS engines, including JavascriptCore, the engine used in Webkit, and Hermes, an engine that comes bundled with React Native. Chromium provides an historical example: when it was still embedding WebKit, it would run V8 in production but use JavaScriptCore for testing — sharing a bindings layer between the two. But as of today, JSI appears to be the only other effort to provide a common interface to an abstract JavaScript engine.

The current scope of JSI — in line with its description as lightweight — appears narrower than Servo's integration with SpiderMonkey. Relevant missing features in JSI include:

  • Integration between native objects and the JavaScript garbage collector — JSCRuntime, the JSI wrapper to JavascriptCore, does use JSValueProtect and JSValueUnProtect, and appears to be passing JSValue around, but it's not clear how well this approach could support integrating the DOM with JS garbage collection (more details on how WebKit does it). Servo follows an innovative design which would require additional APIs for integration: the lifetime of Rust objects are managed by the SpiderMonkey garbage collector.
  • ArrayBuffer — while the API is present, it is not implemented in the JSCRuntime, and doing so would likely require a more complicated API (more details on how WebKit does it)
  • WebIDL-like codegen infrastructure — only basic host objects and functions are present

In conclusion, the existence of JSI is encouraging, even though in its current form it seems to cover an API surface that is narrower than that required by a full-featured browser engine.

Long-term solutions

In an ideal world, Servo should have a clear separation of concerns between the web platform and the execution of script, integrated by way of a generic interface. The idea is a WebIDL layer implementation with a "bring your own JS engine" approach, by way of an interface that any bindings layer could implement. This would open the door for Servo to use other engines to execute script. Besides existing engines used in other browsers, this would also enable Wasm-based runtimes such as Wasmtime, as well as future runtimes not invented yet. But it is impossible to start formulating what such a layer would look like, until we have cleaned-up the components/script crate to hide the use of SpiderMonkey APIs behind safe and idiomatic Rust. This brings us to the short-term — or rather: continuous — approach we are pursuing.

Short-term solutions

A clean-up of script consists of the following:

  • Hide js::jsapi behind safe and idiomatic Rust abstractions in components/script/dom/bindings or in js::rust
  • Conform to WebIDL spec, and to how WebIDL is used in other specifications

An example of this is the work replacing the use of jsapi::JSObject, a kind of wrapper around a pointer to an untyped JS object, with higher-level and typed concepts. A recently completed part of this work was related to Servo's WebGPU implementation: the previous code would return a JSObject from a Web platform operation to SpiderMonkey, after having obtained this object using a low-level and unsafe SpiderMonkey API call. The current code works instead with higher-level and safe concepts, hiding the use of low-level and unsafe SpiderMonkey APIs. The result is code that is easier to use and contribute to, because it does not require fiddling with unsafe and SpiderMonkey specific concepts, that is less coupled to SpiderMonkey, because the coupling is centralized in one place and hidden from the rest, and that is closer to the WebGPU and WebIDL specs, because they deal not with an object but with an ArrayBuffer (WebIDL only rarely deals with objects as opposed to more specific types).

As this work continues, an increasing amount of SpiderMonkey specifics will be moved to components/script/dom/bindings, which will make it possible to enumerate what we are doing with our script engine, and to start investigating how this could be expressed through a generic interface: one that any bindings layer to a script execution engine could implement to integrate with Servo.

Clone this wiki locally