Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
3916aba
Update to SpiderMonkey 140
tschneidereit Jul 29, 2025
62a3f19
Integrate SpiderMonkey build
tschneidereit Jul 31, 2025
a27bd90
ci: Update GH Actions testing workflow
tschneidereit Jul 31, 2025
132dd25
Don't use CPM to clone SpiderMonkey source
tschneidereit Aug 2, 2025
53525d9
Optimize cloning of WPT repository
tschneidereit Aug 2, 2025
c5dbe0e
Expand the README to explain using starling.wasm and starling-raw.wasm
tschneidereit Aug 2, 2025
13054ac
Update CPM to 0.42.0
tschneidereit Aug 2, 2025
59df7d3
Update OpenSSL to 3.0.17
tschneidereit Aug 2, 2025
a2fe844
Update Binaryen to version 123
tschneidereit Aug 2, 2025
fddc853
Ensure that the integration-test-server is rebuilt whenever any of it…
tschneidereit Jul 29, 2025
30cc3de
Release SpiderMonkey build artifacts when landing PRs updating Spider…
tschneidereit Aug 4, 2025
4f87a57
Fix setting default CPM source cache directory
tschneidereit Aug 4, 2025
dc53e11
Move `INVALID_POLLABLE_HANDLE` into the `api` namespace
tschneidereit Jul 15, 2024
3919dfb
Add Rust bindings for the runtime and the SpiderMonkey engines
tschneidereit Jul 15, 2024
cf8a8f5
Update various tools
tschneidereit Aug 4, 2024
7b341c9
Change Rust integration to emit a single static library instead of co…
tschneidereit Aug 9, 2024
68d980f
Add support for retrieving the `Engine` from a JSContext
tschneidereit Aug 18, 2024
267ce42
Update `AUTHORS.md` to document Rust bindings heritage
tschneidereit Nov 28, 2024
911cace
Update Rust bindings integration
tschneidereit Aug 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 94 additions & 4 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,46 @@ jobs:
strategy:
fail-fast: false
matrix:
build: [release, debug, weval]
build: [release, debug]
os: [ubuntu-latest]
outputs:
SM_TAG_EXISTS: ${{ steps.check-sm-release.outputs.SM_TAG_EXISTS }}
SM_TAG: ${{ steps.check-sm-release.outputs.SM_TAG }}
SM_CACHE_KEY_debug: ${{ steps.check-sm-release.outputs.SM_CACHE_KEY_debug }}
SM_CACHE_KEY_release: ${{ steps.check-sm-release.outputs.SM_CACHE_KEY_release }}
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2

- name: Install Rust 1.80.0
- name: Check if SpiderMonkey Release Exists
id: check-sm-release
run: |
rustup toolchain install 1.80.0
rustup target add wasm32-wasip1 --toolchain 1.80.0
SM_TAG="libspidermonkey_$(awk '/^set\(SM_TAG/ {gsub(/set\(SM_TAG |\)/, ""); print}' cmake/spidermonkey.cmake)"
echo "SM_TAG=${SM_TAG}" >> "$GITHUB_OUTPUT"
if gh release view "${SM_TAG}" >/dev/null 2>&1; then
echo "Found existing SpiderMonkey release tag: ${SM_TAG}"
echo "SM_TAG_EXISTS=true" >> "$GITHUB_OUTPUT"
else
echo "SM_TAG_EXISTS=false" >> "$GITHUB_OUTPUT"
echo "SM_CACHE_KEY_${{ matrix.build }}=spidermonkey-cache-${{ matrix.build }}-${{ hashFiles('cmake/spidermonkey.cmake') }}" >> "$GITHUB_OUTPUT"
fi
env:
GH_TOKEN: ${{ github.token }}

- name: Cache SpiderMonkey tarball
if: steps.check-sm-release.outputs.SM_TAG_EXISTS == 'false'
uses: actions/cache@v4
id: sm-cache
with:
path: |
spidermonkey-dist-${{ matrix.build }}
key: spidermonkey-cache-${{ matrix.build }}-${{ hashFiles('cmake/spidermonkey.cmake') }}

- name: Set env var to use cached SpiderMonkey tarball
if: steps.check-sm-release.outputs.SM_TAG_EXISTS == 'false' && steps.sm-cache.outputs.cache-hit == 'true'
run: |
tree spidermonkey-dist-${{ matrix.build }}
echo "SPIDERMONKEY_BINARIES=$(pwd)/spidermonkey-dist-${{ matrix.build }}" >> $GITHUB_ENV

- uses: actions/setup-node@v2
with:
Expand Down Expand Up @@ -64,3 +94,63 @@ jobs:
- name: StarlingMonkey E2E, Integration, and WPT Tests
run: |
CTEST_OUTPUT_ON_FAILURE=1 ctest --test-dir cmake-build-${{ matrix.build }} -j$(nproc) --verbose

- name: Set up cacheable SpiderMonkey artifacts
if: steps.check-sm-release.outputs.SM_TAG_EXISTS == 'false' && steps.sm-cache.outputs.cache-hit != 'true'
run: |
mkdir -p spidermonkey-dist-${{ matrix.build }}
cp -a cmake-build-${{ matrix.build }}/spidermonkey-obj/dist/libspidermonkey.a spidermonkey-dist-${{ matrix.build }}/
cp -aL cmake-build-${{ matrix.build }}/spidermonkey-obj/dist/include spidermonkey-dist-${{ matrix.build }}/
tree spidermonkey-dist-${{ matrix.build }}

# Upload tarball as an artifact of the github action run, so the output
# can be inspected for pull requests.
- name: Upload SpiderMonkey tarball
uses: actions/upload-artifact@v4
if: (github.event_name != 'push' || (github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/tags/v')))
&& steps.check-sm-release.outputs.SM_TAG_EXISTS == 'false' && steps.sm-cache.outputs.cache-hit != 'true'
with:
name: spidermonkey-${{ matrix.build }}
path: spidermonkey-dist-${{ matrix.build }}/*

release-spidermonkey:
needs: test
if: needs.test.outputs.SM_TAG_EXISTS == 'false' && (github.event_name == 'push' &&
(github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')))
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Restore SpiderMonkey Debug Cache
uses: actions/cache/restore@v4
id: sm-cache-debug
with:
path: |
spidermonkey-dist-debug
key: ${{ needs.test.outputs.SM_CACHE_KEY_debug }}
fail-on-cache-miss: true
- name: Restore SpiderMonkey Release Cache
uses: actions/cache/restore@v4
id: sm-cache-release
with:
path: |
spidermonkey-dist-release
key: ${{ needs.test.outputs.SM_CACHE_KEY_release }}
fail-on-cache-miss: true

- name: Create SpiderMonkey Tar Balls
run: |
mkdir -p release-artifacts
tar -a -cf release-artifacts/spidermonkey-static-debug.tar.gz spidermonkey-dist-debug/*
tar -a -cf release-artifacts/spidermonkey-static-release.tar.gz spidermonkey-dist-release/*
tree release-artifacts

- name: Do the Release
uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 #2.3.2
with:
body: |
This release contains pre-built SpiderMonkey artifacts to be used by the StarlingMonkey
build system. It's not meant for general public consumption and doesn't come with any
stability or availability guarantees.
tag_name: ${{ needs.test.outputs.SM_TAG }}
files: release-artifacts/*
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@
/.spin

# Rust compilation output
/target
/runtime/crates/target/

/tests/e2e/*/*.wasm
/tests/e2e/*/*.log
/tests/integration/*/*.wasm
/tests/integration/*/*.log
/deps/*.lock
/deps/*-source
6 changes: 5 additions & 1 deletion AUTHORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@ The portions of this code found in the initial commit are Copyright © 2021-2023
contributors to Fastly's
[js-compute-runtime project](https://github.com/fastly/js-compute-runtime/).

Changes following that initial commit are Copyright © the respective contributors.
Changes following that initial commit are Copyright © the respective contributors where not documented otherwise.

## Rust Bindings

The Rust bindings found under [runtime/crates](runtime/crates) are based on the Servo project's [mozjs and mozjs-sys](https://github.com/servo/mozjs) crates. The code under that folder is Copyright © The Servo Project Developers up to the fork, and Copyright © the respective contributors where not documented otherwise from then on.
28 changes: 16 additions & 12 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ else()
endif()
message(STATUS "Using host API: ${HOST_API}")

# Ensure that the CPM cache is created outside the build dir, even if no location is specified by the developer.
if(NOT DEFINED ENV{CPM_SOURCE_CACHE})
set(ENV{CPM_SOURCE_CACHE} ${CMAKE_CURRENT_SOURCE_DIR}/deps/cpm_cache)
endif()
include("CPM")
include("toolchain")

Expand All @@ -32,36 +36,36 @@ include("binaryen")
include("wizer")
include("weval")
include("wasmtime")
include("cbindgen")

include("fmt")
include("spidermonkey")
include("openssl")
include("${HOST_API}/host_api.cmake")
include("build-crates")

option(ENABLE_JS_DEBUGGER "Enable support for debugging content via a socket connection" ON)

add_library(extension_api INTERFACE include/extension-api.h runtime/encode.h runtime/decode.h)
add_library(extension_api INTERFACE include/extension-api.h runtime/cpp/encode.h runtime/cpp/decode.h)
if (ENABLE_JS_DEBUGGER)
target_compile_definitions(extension_api INTERFACE ENABLE_JS_DEBUGGER)
endif()
target_link_libraries(extension_api INTERFACE rust-url spidermonkey)
target_include_directories(extension_api INTERFACE include deps/include runtime)
target_include_directories(extension_api INTERFACE include deps/include runtime/cpp)

include("builtins")

include("tests/wpt-harness/wpt.cmake")

set(SOURCES
runtime/js.cpp
runtime/allocator.cpp
runtime/encode.cpp
runtime/decode.cpp
runtime/engine.cpp
runtime/event_loop.cpp
runtime/builtin.cpp
runtime/script_loader.cpp
runtime/debugger.cpp
runtime/cpp/js.cpp
runtime/cpp/allocator.cpp
runtime/cpp/encode.cpp
runtime/cpp/decode.cpp
runtime/cpp/engine.cpp
runtime/cpp/event_loop.cpp
runtime/cpp/builtin.cpp
runtime/cpp/script_loader.cpp
runtime/cpp/debugger.cpp
)

add_executable(starling-raw.wasm ${SOURCES})
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.

Copyright 2020 Fastly, Inc.
Copyright StarlingMonkey project contributors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
51 changes: 34 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,43 +71,60 @@ cmake -S . -B cmake-build-debug -DCMAKE_BUILD_TYPE=Debug

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][wasm-component]
using the `componentize.sh` script generated by the build.
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](https://wasmtime.dev/).

The following command will build the `starling-raw.wasm` runtime module in the `cmake-build-release`
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:

```console
# 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
cmake --build cmake-build-release -t starling --parallel $(nproc)
```

Then, the `starling-raw.wasm` module can be turned into a component with the following command:
The resulting runtime can be used to load and evaluate JS code dynamically:

```console
cd cmake-build-release
./componentize.sh -o starling.wasm
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
```

The resulting runtime can be used to load and evaluate JS code dynamically:

## 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:

```console
wasmtime -S http starling.wasm -e "console.log('hello world')"
# or, to load a file:
wasmtime -S http --dir . starling.wasm index.js
# Use cmake-build-debug for the debug build
cmake --build cmake-build-release -t starling-raw.wasm --parallel $(nproc)
```

Alternatively, a JS file can be provided during componentization:
Then, the `starling-raw.wasm` module can be turned into a component specialized for your code with the following command:

```console
cd cmake-build-release
./componentize.sh index.js -o starling.wasm
./componentize.sh index.js -o index.wasm
```

This 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 `fetch` event to serve HTTP requests.
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:

```javascript
addEventListener('fetch', event => {
event.respondWith(new Response('Hello, world!'));
});
```

Componentizing this code like above allows running it like this:

```console
wasmtime serve -S cli --dir . index.wasm
```

[cmake]: https://cmake.org/
[gh-pages]: https://bytecodealliance.github.io/StarlingMonkey/
Expand Down
18 changes: 13 additions & 5 deletions builtins/install_builtins.cpp
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
#include "extension-api.h"

#define NS_DEF(ns) \
namespace ns { \
extern bool install(api::Engine *engine); \
#define NS_DEF(ns) \
namespace ns { \
extern bool install(api::Engine *engine); \
}
#define RS_DEF(install_fn) \
extern "C" bool install_fn(api::Engine *engine);
#include "builtins.incl"
#undef RS_DEF
#undef NS_DEF


bool install_builtins(api::Engine *engine) {
#define NS_DEF(ns) \
if (!ns::install(engine)) \
#define NS_DEF(ns) \
if (!ns::install(engine)) \
return false;
#define RS_DEF(install_fn) \
if (!install_fn(engine)) \
return false;
#include "builtins.incl"
#undef RS_DEF
#undef NS_DEF

return true;
Expand Down
2 changes: 1 addition & 1 deletion builtins/web/base64.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ JS::Result<std::string> forgivingBase64Decode(std::string_view data,
auto hasWhitespace = std::find_if(data.begin(), data.end(), &isAsciiWhitespace);
std::string dataWithoutAsciiWhitespace;

if (hasWhitespace) {
if (*hasWhitespace) {
dataWithoutAsciiWhitespace = data;
dataWithoutAsciiWhitespace.erase(std::remove_if(dataWithoutAsciiWhitespace.begin() +
std::distance(data.begin(), hasWhitespace),
Expand Down
4 changes: 2 additions & 2 deletions builtins/web/console.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,7 @@ static bool console_out(JSContext *cx, unsigned argc, JS::Value *vp) {

// https://console.spec.whatwg.org/#assert
// assert(condition, ...data)
static bool assert(JSContext *cx, unsigned argc, JS::Value *vp) {
static bool assert_(JSContext *cx, unsigned argc, JS::Value *vp) {
JS::CallArgs args = CallArgsFromVp(argc, vp);
args.rval().setUndefined();
auto condition = args.get(0).toBoolean();
Expand Down Expand Up @@ -811,7 +811,7 @@ static bool trace(JSContext *cx, unsigned argc, JS::Value *vp) {
}

const JSFunctionSpec Console::methods[] = {
JS_FN("assert", assert, 0, JSPROP_ENUMERATE),
JS_FN("assert", assert_, 0, JSPROP_ENUMERATE),
JS_FN("clear", no_op, 0, JSPROP_ENUMERATE),
JS_FN("count", count, 0, JSPROP_ENUMERATE),
JS_FN("countReset", countReset, 0, JSPROP_ENUMERATE),
Expand Down
2 changes: 0 additions & 2 deletions builtins/web/crypto/uuid.cpp
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
#include "uuid.h"
#include "host_api.h"

#include <fmt/core.h>

namespace builtins {
namespace web {
namespace crypto {
Expand Down
2 changes: 1 addition & 1 deletion builtins/web/event/event-target.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ bool default_passive_value() {

namespace JS {

template <typename T> struct JS::GCPolicy<RefPtr<T>> {
template <typename T> struct GCPolicy<RefPtr<T>> {
static void trace(JSTracer *trc, RefPtr<T> *tp, const char *name) {
if (T *target = tp->get()) {
GCPolicy<T>::trace(trc, target, name);
Expand Down
7 changes: 2 additions & 5 deletions builtins/web/fetch/fetch-api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ using blob::Blob;
using fetch::Headers;
using host_api::HostString;

static api::Engine *ENGINE;

enum class FetchScheme {
About,
Blob,
Expand Down Expand Up @@ -107,7 +105,8 @@ bool fetch_https(JSContext *cx, HandleObject request_obj, HostString method, Hos
// If the request body is streamed, we need to wait for streaming to complete
// before marking the request as pending.
if (!streaming) {
ENGINE->queue_async_task(new ResponseFutureTask(request_obj, pending_handle));
api::Engine::from_context(cx)
.queue_async_task(new ResponseFutureTask(request_obj, pending_handle));
}

SetReservedSlot(request_obj, static_cast<uint32_t>(Request::Slots::ResponsePromise),
Expand Down Expand Up @@ -345,8 +344,6 @@ bool fetch(JSContext *cx, unsigned argc, Value *vp) {
const JSFunctionSpec methods[] = {JS_FN("fetch", fetch, 2, JSPROP_ENUMERATE), JS_FS_END};

bool install(api::Engine *engine) {
ENGINE = engine;

if (!JS_DefineFunctions(engine->cx(), engine->global(), methods))
return false;
if (!request_response::install(engine)) {
Expand Down
2 changes: 1 addition & 1 deletion builtins/web/fetch/fetch-utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ std::optional<std::tuple<size_t, size_t>> extract_range(std::string_view range_q

auto to_size = [](std::string_view s) -> std::optional<size_t> {
size_t v;
auto [ptr, ec] = std::from_chars(s.begin(), s.end(), v);
auto [ptr, ec] = std::from_chars(&*s.begin(), &*s.end(), v);
return ec == std::errc() ? std::optional<size_t>(v) : std::nullopt;
};

Expand Down
Loading
Loading