StarlingMonkey
A SpiderMonkey-based JS runtime on WebAssembly
A Bytecode Alliance project
Building | Adopters | Documentation | Chat
StarlingMonkey is a SpiderMonkey based JS runtime optimized for use in WebAssembly Components. StarlingMonkey’s core builtins target WASI 0.2.0 to support a Component Model based event loop and standards-compliant implementations of key web builtins, including the fetch API, WHATWG Streams, text encoding, and others. To support tailoring for specific use cases, it’s designed to be highly modular, and can be readily extended with custom builtins and host APIs.
StarlingMonkey is used in production for Fastly’s JS Compute platform, and Fermyon’s Spin JS SDK. See the ADOPTERS file for more details.
Documentation
For comprehensive documentation, visit our Documentation Site.
Quick Start
Requirements
The runtime’s build is managed by cmake, which also takes care of downloading the build dependencies. To properly manage the Rust toolchain, the build script expects rustup to be installed in the system.
Usage
With sufficiently new versions of cmake and rustup installed, the build process is as follows:
- Clone the repo
git clone https://github.com/bytecodealliance/StarlingMonkey
cd StarlingMonkey
- Run the configuration script
For a release configuration, run
cmake -S . -B cmake-build-release -DCMAKE_BUILD_TYPE=Release
For a debug configuration, run
cmake -S . -B cmake-build-debug -DCMAKE_BUILD_TYPE=Debug
- Build the runtime
The build system provides two targets for the runtime: starling-raw.wasm and starling.wasm. The former is a raw WebAssembly core module that can be used to build a WebAssembly Component, while the latter is the final componentized runtime that can be used directly with a WebAssembly Component-aware runtime like wasmtime.
A key difference is that starling.wasm can only be used for runtime-evaluation of JavaScript code,
while starling-raw.wasm can be used to build a WebAssembly Component that is specialized for a specific
JavaScript application, and as a result has much faster startup times.
Using StarlingMonkey with dynamically loaded JS code
The following command will build the starling.wasm runtime module in the cmake-build-release
directory:
# Use cmake-build-debug for the debug build
cmake --build cmake-build-release -t starling --parallel $(nproc)
The resulting runtime can be used to load and evaluate JS code dynamically:
wasmtime -S http cmake-build-release/starling.wasm -e "console.log('hello world')"
# or, to load a file:
wasmtime -S http --dir . starling.wasm index.js
Creating a specialized runtime for your JS code
To create a specialized version of the runtime, first build a raw, unspecialized core wasm version of StarlingMonkey:
# Use cmake-build-debug for the debug build
cmake --build cmake-build-release -t starling-raw.wasm --parallel $(nproc)
Then, the starling-raw.wasm module can be turned into a component specialized for your code with the following command:
cd cmake-build-release
./componentize.sh index.js -o index.wasm
This mode currently only supports the creation of HTTP server components, which means that the index.js file must register a fetch event handler. For example, your index.js could contain the following code:
addEventListener('fetch', event => {
event.respondWith(new Response('Hello, world!'));
});
Componentizing this code like above allows running it like this:
wasmtime serve -S cli --dir . index.wasm
Building and Running
Requirements
The runtime’s build is managed by cmake, which also takes care of downloading the build dependencies. To properly manage the Rust toolchain, the build script expects rustup to be installed in the system.
See also Project workflow using just for documentation on how to use
just to streamline the project interface.
Usage
Step 1: Clone the repo
git clone https://github.com/bytecodealliance/StarlingMonkey
cd StarlingMonkey
Step 2: Run the configuration script
Choose one of the following configurations:
-
For a release configuration:
cmake -S . -B cmake-build-release -DCMAKE_BUILD_TYPE=Release -
For a debug configuration:
cmake -S . -B cmake-build-debug -DCMAKE_BUILD_TYPE=Debug
Step 3: Build the runtime
Building the runtime is done in two phases: first, cmake is used to build a raw version as a
WebAssembly core module. Then, that module is turned into a
WebAssembly Component using the componentize.sh
script.
-
Build the WebAssembly component
The following command will build the runtime, creating the files
starling-raw.wasmandstarling.wasmin thecmake-build-releasedirectory:# Use cmake-build-debug for the debug build # Change the value for `--parallel` to match the number of CPU cores in your system cmake --build cmake-build-release --parallel 8 --target starling -
Test the runtime
The
starling.wasmcomponent can be used to load and evaluate JS code dynamically:wasmtime -S http starling.wasm -e "console.log('hello world')" # or, to load a file: wasmtime -S http --dir . starling.wasm index.js -
Create components from JS files (alternative approach)
Alternatively, the
componentize.jsscript also provided in the build directory lets us create a component from a JS file:cd cmake-build-release ./componentize.sh index.js wasmtime -S http --dir . index.wasmThis way, the JS file will be loaded during componentization, and the top-level code will be executed, and can e.g. register a handler for the
fetchevent to serve HTTP requests.
Testing StarlingMonkey
Testing the build
After completing the build (a debug build in this case), the integration test runner can be built:
cmake --build cmake-build-debug --target integration-test-server
Then tests can be run with ctest directly via:
ctest --test-dir cmake-build-debug -j$(nproc) --output-on-failure
Alternatively, the integration test server can be directly run with wasmtime serve via:
wasmtime serve -S common cmake-build-debug/test-server.wasm
Then visit http://0.0.0.0:8080/timers, or any test name and filter of the form [testName]/[filter]
Web Platform Tests
To run the Web Platform Tests suite, the WPT runner requires
Node.js above v18.0 to be installed, and the list of hosts in deps/wpt-hosts needs to be added to etc/hosts.
cmake -S . -B cmake-build-debug -DCMAKE_BUILD_TYPE=Debug
cmake --build cmake-build-debug --parallel $(nproc) --target wpt-runtime
cat deps/wpt-hosts | sudo tee -a /etc/hosts # Required to resolve test server hostnames
cd cmake-build-debug
ctest -R wpt --verbose # Note: some of the tests run fairly slowly in debug builds, so be patient
The Web Platform Tests checkout can also be customized by setting the
WPT_ROOT=[path to your WPT checkout] environment variable to the cmake command.
WPT tests can be filtered with the WPT_FILTER=string variable, for example:
WPT_FILTER=fetch ctest -R wpt -v
Custom flags can also be passed to the test runner via WPT_FLAGS="...", for example to update
expectations use:
WPT_FLAGS="--update-expectations" ctest -R wpt -v
Project workflow using just
Getting Started
The justfile provides a streamlined interface for executing common project-specific tasks. To
install just, you can use the command cargo install just or cargo binstall just. Alternatively,
refer to the official installation instructions for your specific system.
Once installed, navigate to the project directory and run just commands as needed. For instance,
the following commands will configure a default cmake-build-debug directory and build the project.
just build
To load a JS script during componentization and serve its output using Wasmtime, run:
just serve <filename>.js
To build and run integration tests run:
just test
To build and run Web Platform Tests run:
just wpt-setup # prepare WPT hosts
just wpt-test # run all tests
just wpt-test console/console-log-symbol.any.js # run specific test
To view a complete list of available recipes, run:
just --list
note
By default, the CMake configuration step is skipped if the build directory already exists. However, this can sometimes cause issues if the existing build directory was configured for a different target. For instance:
- Running
just buildcreates a build directory for the default target, - Running
just wpt-buildafterward may fail because the WPT target hasn’t been configured in the existing build directory.
To resolve this, you can force cmake to reconfigure the build directory by adding the
reconfigure=true parameter. For example:
just reconfigure=true wpt-build
Customizing build
The default build mode is debug, which automatically configures the build directory to
cmake-build-debug. You can switch to a different build mode, such as release, by specifying the
mode parameter. For example:
just mode=release build
This command will set the build mode to release, and the build directory will automatically change
to cmake-build-release.
If you want to override the default build directory, you can use the builddir parameter.
just builddir=mybuilddir mode=release build
This command configures CMake to use mybuilddir as the build directory and sets the build mode to
release.
Starting the WPT Server
After running just wpt-setup as described above, you can also start a Web Platform Tests (WPT) server with:
just wpt-server
After starting the server, tests can be run interactively with a basic web interface running at http://127.0.0.1:7879/. Tests can be filtered by providing prefixes or exact names of subsets or specific tests.
Some examples:
- Running all of StarlingMonkey’s WPT tests:
http://127.0.0.1:7879/ - Running all console tests:
http://127.0.0.1:7879/console - Running a specific test:
http://127.0.0.1:7879/console/console-log-symbol.any.js
curl http://127.0.0.1:7676/console/console-log-symbol.any.js
Devoloping builtins in C++
Adding custom builtins
Adding builtins is as simple as calling add_builtin in the importing project’s CMakeLists.txt.
Say you want to add a builtin defined in the file my-builtin.cpp, like so:
// The extension API is automatically on the include path for builtins.
#include "extension-api.h"
// The namespace name must match the name passed to `add_builtin` in the CMakeLists.txt
namespace my_project::my_builtin {
bool install(api::Engine* engine) {
printf("installing my-builtin\n");
return true;
}
} // namespace my_builtin
This file can now be included in the runtime’s builtins like so:
add_builtin(my_project::my_builtin SRC my-builtin.cpp)
If your builtin requires multiple .cpp files, you can pass all of them to add_builtin as values
for the SRC argument.
Writing builtins
Rooting
important
SpiderMonkey has a moving GC, it is very important that it knows about each and every pointer to a GC thing in the system. SpiderMonkey’s rooting API tries to make this task as simple as possible.
It is highly recommended that readers review the SpiderMonkey
documentation on rooting before proceeding with this section. For
convenience, here is a brief recap of the main rooting rules from the referenced page:
- Use
JS::Rooted<T>typedefs for local variables on the stack. - Use
JS::Handle<T>typedefs for function parameters. - Use
JS::MutableHandle<T>typedefs for function out-parameters. - Use an implicit cast from
JS::Rooted<T>to get a JS::Handle. - Use an explicit address-of-operator on
JS::Rooted<T>to get a JS::MutableHandle. - Return raw pointers from functions.
- Use
JS::Rooted<T>fields when possible for aggregates, otherwise use an AutoRooter. - Use
JS::Heap<T>members for heap data. Note: Heapare not “rooted”: they must be traced! - Do not use
JS::Rooted<T>,JS::Handleor JS::MutableHandle on the heap. - Do not use
JS::Rooted<T>for function parameters. - Use
JS::PersistentRooted<T>for things that are alive until the process exits (or until you manually delete thePersistentRootedfor a reason not based on GC finalization.)
StarlingMonkey builtins API
The builtin API provided by StarlingMonkey is built on top of SpiderMonkey’s
jsapi, Mozilla’s C++ interface for embedding JavaScript. It simplifies the
creation and management of native JavaScript classes and objects by abstracting common patterns and
boilerplate code required by the underlying SpiderMonkey engine.
See also SpiderMonkey documentation on Custom Objects.
This section explains how to implement a native JavaScript class using the StarlingMonkey framework. We’ll use the following JavaScript class as an example:
class MyClass {
constructor(a, b) {
this._a = a;
this._b = b;
}
get prop() {
return 42;
}
method() {
return this.a + this.b;
}
static get static_prop() {
return "static";
}
static static_method(a, b) {
return a + b;
}
}
Step 1: Define the Header File (my_class.h)
Create a header file to declare the native class and its methods/properties.
#ifndef BUILTINS_MY_CLASS_H
#define BUILTINS_MY_CLASS_H
#include "builtin.h"
namespace builtins {
namespace my_class {
class MyClass : public BuiltinImpl<MyClass> {
// Instance methods
static bool method(JSContext *cx, unsigned argc, JS::Value *vp);
static bool prop_get(JSContext *cx, unsigned argc, JS::Value *vp);
// Static methods
static bool static_method(JSContext *cx, unsigned argc, JS::Value *vp);
static bool static_prop_get(JSContext *cx, unsigned argc, JS::Value *vp);
public:
enum Slots : uint8_t { SlotA, SlotB, Count };
static constexpr const char *class_name = "MyClass";
static constexpr unsigned ctor_length = 2;
static const JSFunctionSpec static_methods[];
static const JSPropertySpec static_properties[];
static const JSFunctionSpec methods[];
static const JSPropertySpec properties[];
static bool init_class(JSContext *cx, HandleObject global);
static bool constructor(JSContext *cx, unsigned argc, Value *vp);
};
bool install(api::Engine *engine);
} // namespace my_class
} // namespace builtins
#endif // BUILTINS_MY_CLASS_H
Step 2: Implement the Class (my_class.cpp)
#include "my_class.h"
namespace builtins {
namespace my_class {
const JSFunctionSpec MyClass::methods[] = {
JS_FN("method", MyClass::method, 0, JSPROP_ENUMERATE),
JS_FS_END,
};
const JSPropertySpec MyClass::properties[] = {
JS_PSG("prop", MyClass::prop_get, JSPROP_ENUMERATE),
JS_STRING_SYM_PS(toStringTag, "MyClass", JSPROP_READONLY),
JS_PS_END,
};
const JSFunctionSpec MyClass::static_methods[] = {
JS_FN("static_method", MyClass::static_method, 2, JSPROP_ENUMERATE),
JS_FS_END,
};
const JSPropertySpec MyClass::static_properties[] = {
JS_PSG("static_prop", MyClass::static_prop_get, JSPROP_ENUMERATE),
JS_PS_END,
};
// Constructor implementation
bool MyClass::constructor(JSContext *cx, unsigned argc, JS::Value *vp) {
CTOR_HEADER("MyClass", 2);
RootedObject self(cx, JS_NewObjectForConstructor(cx, &class_, args));
if (!self) {
return false;
}
SetReservedSlot(self, Slots::SlotA, args.get(0));
SetReservedSlot(self, Slots::SlotB, args.get(1));
args.rval().setObject(*self);
return true;
}
// Instance method implementation
bool MyClass::method(JSContext *cx, unsigned argc, JS::Value *vp) {
METHOD_HEADER(0);
double a, b;
if (!JS::ToNumber(cx, JS::GetReservedSlot(self, Slots::SlotA), &a) ||
!JS::ToNumber(cx, JS::GetReservedSlot(self, Slots::SlotB), &b)) {
return false;
}
args.rval().setNumber(a + b);
return true;
}
// Instance property getter implementation
bool MyClass::prop_get(JSContext *cx, unsigned argc, JS::Value *vp) {
METHOD_HEADER(0);
args.rval().setInt32(42);
return true;
}
// Static method implementation
bool MyClass::static_method(JSContext *cx, unsigned argc, JS::Value *vp) {
CallArgs args = CallArgsFromVp(argc, vp);
if (!args.requireAtLeast(cx, "static_method", 2)) {
return false;
}
double a, b;
if (!JS::ToNumber(cx, args.get(0), &a) ||
!JS::ToNumber(cx, args.get(1), &b)) {
return false;
}
args.rval().setNumber(a + b);
return true;
}
// Static property getter implementation
bool MyClass::static_prop_get(JSContext *cx, unsigned argc, JS::Value *vp) {
CallArgs args = CallArgsFromVp(argc, vp);
args.rval().setString(JS_NewStringCopyZ(cx, "static"));
return true;
}
// Class initialization
bool MyClass::init_class(JSContext *cx, JS::HandleObject global) {
return init_class_impl(cx, global);
}
bool install(api::Engine *engine) {
return MyClass::init_class(engine->cx(), engine->global());
}
} // namespace my_class
} // namespace builtins
Deriving from BuiltinImpl automatically creates a JSClass definition using parameters provided
by the implementation:
Explanation of StarlingMonkey API Usage
static constexpr JSClass class_{
Impl::class_name,
JSCLASS_HAS_RESERVED_SLOTS(static_cast<uint32_t>(Impl::Slots::Count)) | class_flags,
&class_ops,
};
-
SetReservedSlotandGetReservedSlot:- Used to store and retrieve internal state associated with the JavaScript object.
-
JSFunctionSpecandJSPropertySpecarrays:- Define instance/static methods and properties exposed to JavaScript.
StarlingMonkey provides macros and helper functions to simplify native class implementation:
-
CTOR_HEADER(name, required_argc):- Initializes constructor arguments.
- Ensures the constructor is called with the
newkeyword. - Checks the minimum number of arguments.
-
METHOD_HEADER(required_argc):- Initializes method arguments.
- Ensures the receiver (
this) is an instance of the correct class. - Checks the minimum number of arguments.
-
is_instance(JSObject *obj):- Used to verify if object passed is instance of builtin class or subclass.
- Used often as assertion in class methods to ensure expected memory layout, for example:
JSString *MyObject::my_method(JSObject *self) { MOZ_ASSERT(is_instance(self)); return JS::GetReservedSlot(self, Slots::MySlot).toString(); }
note
Registering the Class with StarlingMonkey Engine
When you use add_builtin in your CMakeLists.txt file, it automatically adds your builtin to the
builtins.inclfile, which is then processed to ensure your install function is
called during engine initialization.
Providing a custom host API implementation
The host-apis directory can contain implementations of the host API for different
versions of WASI—or in theory any other host interface. Those can be selected by setting the
HOST_API environment variable to the name of one of the directories. Currently, implementations in terms of wasi-0.2.0, wasi-0.2.2, and wasi-0.2.3 are provided, with the latter used by default.
To provide a custom host API implementation, you can set HOST_API to the (absolute) path of a
directory containing that implementation. As is done in the implementations for wasi-0.2.{2,3}, it’s possible to extend existing implementations instead of duplicating any shared implementation.
Devoloping Builtins in Rust
Developing Changes to SpiderMonkey
StarlingMonkey uses SpiderMonkey as its underlying JS engine, and by default, downloads build artifacts from a wrapper repository around our local SpiderMonkey tree. That wrapper repository contains a SpiderMonkey commit-hash in a file, and its CI jobs build the artifacts that StarlingMonkey downloads during its build.
This flow is optimized for ease of development of StarlingMonkey, and avoiding the need to build SpiderMonkey locally, which requires some additional tools and is resource-intensive. However, sometimes it is necessary or desirable to make modifications to SpiderMonkey directly, whether to make fixes or optimize performance.
In order to do so, first clone the above two repositories, with gecko-dev (SpiderMonkey itself) as
a subdirectory to spidermonkey-wasi-embedding:
git clone https://github.com/bytecodealliance/spidermonkey-wasi-embedding
cd spidermonkey-wasi-embedding/
git clone https://github.com/bytecodealliance/gecko-dev
and switch to the commit that we are currently using:
git checkout `cat ../gecko-revision`
# now edit the source
Then make changes as necessary, eventually rebuilding from the spidermonkey-wasi-embedding root:
cd ../ # back to spidermonkey-wasi-embedding
./rebuild.sh release
This will produce a release/ directory with artifacts of the same form normally downloaded by
StarlingMonkey. So, finally, from within StarlingMonkey, set an environment variable
SPIDERMONKEY_BINARIES:
export SPIDERMONKEY_BINARIES=/path/to/spidermonkey-wasi-embedding/release
cmake -S . -B cmake-build-release -DCMAKE_BUILD_TYPE=Release
cmake --build cmake-build-release --parallel 8
and use/test as described in testing section.
Debugging StarlingMonkey application
TODO
Community
Reaching out
For general questions, ideas, and community discussions, visit the BytecodeAlliance Zulip #StarlingMonkey channel. This is a great place for:
- Asking questions about usage
- Sharing projects built with StarlingMonkey
- Proposing new features or improvements
- General community interaction
If you encounter bugs or have feature requests, please file them on the StarlingMonkey GitHub Issues page.
Community Meetings
Join the bi-weekly JavaScript SIG (Special Interest Group) meetings to connect with the StarlingMonkey community, discuss development progress, and contribute to the project’s direction. Meeting details and agendas can be found at the BytecodeAlliance JavaScript SIG page.
Code of Conduct
StarlingMonkey follows the BytecodeAlliance Code of Conduct. We are committed to providing a welcoming and inclusive environment for all participants.
StarlingMonkey Adopters
If you are using StarlingMonkey in production at your organization, please add your company name to this list. The list is in alphabetical order.
| Organization | Contact | Status | Description of Use |
|---|---|---|---|
| Fastly | @zkat and @TartanLlama | Fastly’s JS SDK for Compute is powered by StarlingMonkey. | |
| Fermyon | @tschneidereit | Fermyon Spin supports serverless Wasm apps, using StarlingMonkey for its Spin JS SDK. | |
| Gcore | @godronus | FastEdge-sdk-js powered by StarlingMonkey for JavaScript execution. | |
| Riza | @kyleconroy | The Riza Code Interpreter API relies on StarlingMonkey for isolated JavaScript execution. |
Contributor Covenant Code of Conduct
Note: this Code of Conduct pertains to individuals’ behavior. Please also see the Organizational Code of Conduct.
Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
Our Standards
Examples of behavior that contributes to creating a positive environment include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others’ private information, such as a physical or electronic address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting
Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the Bytecode Alliance CoC team at report@bytecodealliance.org. The CoC team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The CoC team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the Bytecode Alliance’s leadership.
Attribution
This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at http://contributor-covenant.org/version/1/4