feat: add a histrical wit-bindgen

This commit is contained in:
2023-01-01 00:25:48 +08:00
parent 01e8f5a959
commit aa50d63aec
419 changed files with 45283 additions and 1 deletions

View File

@@ -0,0 +1,20 @@
[package]
name = "wit-bindgen-gen-spidermonkey"
version = "0.1.0"
authors = ["Nick Fitzgerald <fitzgen@gmail.com>"]
edition = "2018"
[lib]
test = false
doctest = false
[dependencies]
lazy_static = "1.4.0"
structopt = { version = "0.3", optional = true }
wasm-encoder = "0.8.0"
wit-bindgen-gen-core = { path = "../gen-core" }
heck = "0.3"
[dev-dependencies]
test-helpers = { path = '../test-helpers', features = ['wit-bindgen-gen-spidermonkey'] }
wasmparser = "0.80"

View File

@@ -0,0 +1,4 @@
fn main() {
println!("cargo:rerun-if-changed=build.rs");
// this build script is currently only here so OUT_DIR is set for testing.
}

View File

@@ -0,0 +1,7 @@
spidermonkey-*
lib/*
*.o
spidermonkey.initial.wasm
wasi-sdk-12.0
mozbuild
.exports

View File

@@ -0,0 +1,142 @@
SM_REPO := https://github.com/fitzgen/gecko-dev
SM_COMMIT := dafd3165f45c55023ece4787a86444029e4f475e
# TODO: support building `spidermonkey.wasm` on other OSes. But for some reason
# the resulting `.wasm` binary is slower when the host compiler is on macOS.
WASI_SDK_URL := https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-12/wasi-sdk-12.0-linux.tar.gz
CC := $(CURDIR)/wasi-sdk-12.0/bin/clang
CXX := $(CURDIR)/wasi-sdk-12.0/bin/clang++
# Set this to `1` to enable logging via all the `SMW_LOG(...)` calls.
LOGGING := 0
# Set this to `-DDEBUG` and uncomment the `--enable-debug` line in `mozconfig`
# to enable debug builds of SpiderMonkey.
DEBUG := ""
# Set this to `""` in debug mode for better debugging.
OPT := "-O2"
CFLAGS := \
--sysroot=$(CURDIR)/wasi-sdk-12.0/share/wasi-sysroot \
-Wall \
--target=wasm32-unknown-wasi \
-Ispidermonkey-$(SM_COMMIT)/obj-wasm32-unknown-wasi/dist/include \
-I$(CURDIR)/include \
$(DEBUG) \
$(OPT) \
-DLOGGING=$(LOGGING)
CXXFLAGS := \
$(CFLAGS) \
-fno-exceptions \
-std=c++17
# Local object files.
LOCAL_OBJECTS := $(patsubst %.cpp,%.o,$(wildcard *.cpp))
# Object files needed within SpiderMonkey's obj dir.
SM_OBJECTS := \
js/src/build/libjs_static.a \
memory/build/Unified_cpp_memory_build0.o \
memory/mozalloc/mozalloc_abort.o \
memory/mozalloc/Unified_cpp_memory_mozalloc0.o \
mfbt/Unified_cpp_mfbt0.o \
mfbt/Unified_cpp_mfbt1.o \
mfbt/lz4.o \
mfbt/lz4frame.o \
mfbt/lz4hc.o \
mfbt/xxhash.o \
mozglue/misc/AutoProfilerLabel.o \
mozglue/misc/ConditionVariable_noop.o \
mozglue/misc/Decimal.o \
mozglue/misc/MmapFaultHandler.o \
mozglue/misc/Mutex_noop.o \
mozglue/misc/Printf.o \
mozglue/misc/StackWalk.o \
mozglue/misc/TimeStamp.o \
mozglue/misc/TimeStamp_posix.o \
mozglue/misc/Uptime.o \
modules/zlib/src/compress.o \
modules/zlib/src/gzclose.o \
modules/zlib/src/infback.o \
modules/zlib/src/uncompr.o \
wasm32-wasi/release/libjsrust.a
# The `./lib/*` copies of SpiderMonkey's object files that we check into the
# repo.
SM_LIB_OBJECTS := $(shell echo $(SM_OBJECTS) | xargs -d' ' -I{} basename {} | xargs -I{} echo lib/{})
.PHONY: all clean clean-all clean-spidermonkey clean-wasi-sdk
all: spidermonkey.wasm
@echo "Done!"
spidermonkey.initial.wasm: $(SM_LIB_OBJECTS) $(LOCAL_OBJECTS)
$(CXX) $(CXXFLAGS) \
-mexec-model=reactor \
$(LOCAL_OBJECTS) \
$(SM_LIB_OBJECTS) \
-o spidermonkey.initial.wasm \
-Wl,--export-dynamic \
-Wl,--growable-table \
-Wl,--export-table \
-Wl,--gc-sections
spidermonkey.wasm: spidermonkey.initial.wasm
# Uncomment this `wasm-opt` invocation and comment the following one out to
# enable better debugging.
#
# wasm-opt -g --duplicate-import-elimination spidermonkey.initial.wasm -o spidermonkey.wasm
wasm-opt -O2 --strip-dwarf --duplicate-import-elimination spidermonkey.initial.wasm -o spidermonkey.wasm
# Build all `*.cpp` files into `*.o` files.
%.o: %.cpp $(SM_LIB_OBJECTS)
$(CXX) $(CXXFLAGS) -c $< -o $@
# Actually build SpiderMonkey.
$(SM_LIB_OBJECTS): spidermonkey-$(SM_COMMIT) mozbuild wasi-sdk-12.0 mozconfig
cd spidermonkey-$(SM_COMMIT) \
&& MOZBUILD_STATE_PATH=$(CURDIR)/mozbuild MOZCONFIG=$(CURDIR)/mozconfig ./mach build
mkdir -p lib
for x in $(SM_OBJECTS); do \
cp spidermonkey-$(SM_COMMIT)/obj-wasm32-unknown-wasi/$$x lib/; \
done
# Clone `mozilla-central` at the `SM_COMMIT` commit.
spidermonkey-$(SM_COMMIT):
-rm -rf spidermonkey-temp
mkdir spidermonkey-temp
cd spidermonkey-temp \
&& git init \
&& git remote add origin $(SM_REPO) \
&& git fetch origin $(SM_COMMIT) \
&& git checkout $(SM_COMMIT)
mv spidermonkey-temp spidermonkey-$(SM_COMMIT)
mozbuild: spidermonkey-$(SM_COMMIT)
-mkdir mozbuild
cd spidermonkey-$(SM_COMMIT) \
&& MOZBUILD_STATE_PATH=$(CURDIR)/mozbuild ./mach bootstrap --application-choice js --no-system-changes \
|| rm -rf $(CURDIR)/mozbuild
wasi-sdk-12.0:
curl -L $(WASI_SDK_URL) | tar -x -z
clean-all: clean clean-spidermonkey clean-wasi-sdk
clean-wasi-sdk:
-rm -rf wasi-sdk-12.0
clean-spidermonkey:
-rm -rf spidermonkey-$(SM_COMMIT)
-rm -rf spidermonkey-$(SM_COMMIT)/obj-wasm32-unknown-wasi/
-rm -rf mozbuild
clean:
@echo 'Only cleaning our own artifacts, not upstream deps. Run `make clean-{all,spidermonkey,wasi-sdk}` to clean others.'
-rm -rf spidermonkey-temp
-rm -rf ./*.o
-rm -rf spidermonkey.wasm

View File

@@ -0,0 +1,13 @@
# `spidermonkey.wasm`
This directory contains the source code for `spidermonkey.wasm`, which is an
embedding of the SpiderMonkey JavaScript engine for targeting `wasm32-wasi` and
use with `wit-bindgen-gen-spidermonkey`. It exports a variety of helper
functions that are used by `wit-bindgen-gen-spidermonkey`'s generated glue
code. These helpers are typically named something like `SMW_whatever_function`.
## Building `spidermonkey.wasm`
```
make
```

View File

@@ -0,0 +1,46 @@
#include <stdlib.h>
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Winvalid-offsetof"
#include "js/Exception.h"
#pragma clang diagnostic pop
#include "smw/abort.h"
#include "smw/cx.h"
#include "smw/dump.h"
namespace smw {
void abort(const char* msg) {
abort(get_js_context(), msg);
}
void abort(JSContext *cx, const char* msg) {
fprintf(stderr, "Error: %s", msg);
if (JS_IsExceptionPending(cx)) {
fprintf(stderr, ":");
JS::ExceptionStack exception(cx);
if (!JS::GetPendingExceptionStack(cx, &exception)) {
fprintf(stderr, " failed to get pending exception value and stack\n");
} else {
fprintf(stderr, "\n exception value: ");
if (!dump_value(cx, exception.exception(), stderr)) {
fprintf(stderr, "<failed to dump value>");
}
fprintf(stderr, "\n exception stack:\n");
if (!dump_stack(cx, exception.stack(), stderr)) {
fprintf(stderr, "<failed to dump stack>\n");
}
}
} else {
fprintf(stderr, "\n");
}
// TODO: check for unhandled promise rejections.
fflush(stderr);
::abort();
}
} // namespace smw

View File

@@ -0,0 +1,452 @@
/*!
* This module implements the intrinsics used by code emitted in the
* `wit_bindgen_gen_spidermonkey::Bindgen` trait implementation.
*/
#include <assert.h>
#include <cmath>
#include <stdlib.h>
#include "smw/abort.h"
#include "smw/cx.h"
#include "smw/logging.h"
#include "smw/wasm.h"
#include "mozilla/UniquePtr.h"
#include "jsapi.h"
#include "js/Array.h"
#include "js/Conversions.h"
#include "js/ForOfIterator.h"
#include "js/Modules.h"
#ifdef LOGGING
#include "js/friend/DumpFunctions.h"
#endif
namespace smw {
using UniqueChars = mozilla::UniquePtr<char[]>;
using PersistentRootedValueVector = JS::PersistentRooted<JS::GCVector<JS::Value>>;
// Used for general Wasm<-->JS conversions.
static PersistentRootedValueVector* OPERANDS;
// Used for holding arguments to JS calls.
static PersistentRootedValueVector* ARGS;
// Used for holding returns from Wasm calls.
static PersistentRootedValueVector* RETS;
void init_operands(JSContext* cx) {
assert(!OPERANDS && "OPERANDS must only be initialized once");
OPERANDS = new PersistentRootedValueVector(cx, cx);
if (!OPERANDS) {
abort(cx, "failed to allocate OPERANDS");
}
assert(!ARGS && "ARGS must only be initialized once");
ARGS = new PersistentRootedValueVector(cx, cx);
if (!ARGS) {
abort(cx, "failed to allocate ARGS");
}
assert(!RETS && "RETS must only be initialized once");
RETS = new PersistentRootedValueVector(cx, cx);
if (!RETS) {
abort(cx, "failed to allocate RETS");
}
}
PersistentRootedValueVector& operands() {
assert(OPERANDS && OPERANDS->initialized() && "OPERANDS must be initialized");
return *OPERANDS;
}
void save_operand(size_t dest, JS::HandleValue val) {
#if LOGGING==1
SMW_LOG("operands[%zu] = ", dest);
js::DumpValue(val, stderr);
#endif // LOGGING==1
JSContext* cx = get_js_context();
if (operands().length() <= dest) {
size_t needed_capacity = 1 + dest - operands().length();
if (!operands().reserve(needed_capacity)) {
abort("failed to reserve capacity for the OPERANDS vector");
}
if (dest == operands().length()) {
bool ok = operands().append(val);
assert(ok && "already reserved space");
return;
}
JS::RootedValue placeholder(cx, JS::UndefinedValue());
for (size_t i = 0; i < needed_capacity; i++) {
bool ok = operands().append(placeholder);
assert(ok && "already reserved space");
}
}
operands()[dest].set(val);
}
PersistentRootedValueVector& args() {
assert(ARGS && ARGS->initialized() && "ARGS must be initialized");
return *ARGS;
}
PersistentRootedValueVector& rets() {
assert(RETS && RETS->initialized() && "RETS must be initialized");
return *RETS;
}
WASM_EXPORT
void canonical_abi_free(void* ptr, size_t size, size_t align) {
(void) size;
(void) align;
free(ptr);
}
WASM_EXPORT
void* canonical_abi_realloc(void* ptr, size_t old_size, size_t align, size_t new_size) {
(void) old_size;
(void) align;
return realloc(ptr, new_size);
}
WASM_EXPORT
void SMW_fill_operands(unsigned argc, JS::Value* vp) {
SMW_LOG("SMW_fill_operands(argc = %d, vp = %p)\n", argc, vp);
JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
if (!operands().reserve(size_t(args.length()))) {
abort(get_js_context(), "failed to reserve space in the operands vector");
}
for (unsigned i = 0; i < args.length(); i++) {
#if LOGGING==1
SMW_LOG("operands[%d] = ", i);
js::DumpValue(args.get(i), stderr);
#endif // LOGGING==1
bool ok = operands().append(args.get(i));
assert(ok && "already reserved space");
}
}
WASM_EXPORT
void SMW_clear_operands() {
SMW_LOG("SMW_clear_operands\n");
operands().clear();
}
WASM_EXPORT
void SMW_push_arg(size_t i) {
SMW_LOG("SMW_push_arg(i = %zu)\n", i);
if (!args().append(operands()[i])) {
abort("failed to push arg");
}
}
WASM_EXPORT
void SMW_call(char *funcName, size_t funcNameLen, size_t numResults, size_t dest) {
#ifdef LOGGING
SMW_LOG("SMW_call(funcName = %p \"", funcName);
for (size_t i = 0; i < funcNameLen; i++) {
SMW_LOG("%c", funcName[i]);
}
SMW_LOG("\", funcNameLen = %zu, numResults = %zu, dest = %zu)\n",
funcNameLen,
numResults,
dest);
#endif
UniqueChars uniqFuncName(funcName);
JSContext *cx = get_js_context();
JS::RootedString funcNameAtom(cx, JS_AtomizeStringN(cx, uniqFuncName.get(), funcNameLen));
if (!funcNameAtom) {
abort(cx, "failed to atomize function name");
}
JS::RootedObject module(cx, get_user_module());
JS::RootedValue exportVal(cx);
bool hasExport = false;
if (!JS::GetModuleExport(cx, module, funcNameAtom, &exportVal, &hasExport)) {
abort(cx, "failed to get module export");
}
if (!hasExport) {
// TODO: include the export name in this message to help users debug
// which export they're missing.
abort(cx, "user module does not have the requested export");
}
JS::RootedFunction exportFunc(cx, JS_ValueToFunction(cx, exportVal));
if (!exportFunc) {
// TODO: include the export name in this message.
abort(cx, "exported value is not a function");
}
// XXX: we have to copy ARGS into a `JS::RootedVector<JS::Value>` because
// `JS::Call` takes a `JS::HandleValueArray` and you can't construct that
// from a `JS::PersistentRooted<JS::GCVector<JS::Value>>`, only a
// `JS::RootedVector<JS::Value>`. And we can't make `ARGS` a
// `JS::RootedVector<JS::Value>` because it is a global, not an on-stack
// RAII value as required by `JS::RootedVector<JS::Value>`. Gross!
JS::RootedVector<JS::Value> argsVector(cx);
if (!argsVector.reserve(args().length())) {
abort(cx, "failed to reserve space for arguments vector");
}
for (size_t i = 0; i < args().length(); i++) {
bool ok = argsVector.append(args()[i]);
assert(ok && "already reserved space");
}
JS::RootedObject thisObj(cx);
JS::RootedValue result(cx);
if (!JS::Call(cx, thisObj, exportFunc, argsVector, &result)) {
// TODO: include the export name in this message.
abort(cx, "calling export function failed");
}
args().clear();
if (numResults == 0) {
// Nothing to push onto the operands vector.
} else if (numResults == 1) {
save_operand(dest, result);
} else {
// Treat the "physical" return value as an iterator and unpack the
// "logical" return values from within it. This allows JS to return
// multiple WIT values as an array or any other iterable.
JS::ForOfIterator iter(cx);
if (!iter.init(result)) {
// TODO: include the export name in this message.
abort(cx, "failed to convert return value to iterable");
}
JS::RootedValue val(cx);
bool done = false;
for (size_t i = 0; i < numResults; i++) {
if (done) {
// TODO: include the export name in this message.
abort(cx, "function's returned iterator did not yield enough return values");
}
if (!iter.next(&val, &done)) {
// TODO: include the export name in this message.
abort(cx, "failed to get the next value out of the return values iterator");
}
save_operand(dest + i, val);
}
}
}
WASM_EXPORT
void SMW_push_return_value(size_t i) {
SMW_LOG("SMW_push_return_value(i = %zu)\n", i);
if (!rets().append(operands()[i])) {
abort(get_js_context(), "failed to push arg");
}
}
WASM_EXPORT
void SMW_finish_returns(unsigned argc, JS::Value* vp) {
SMW_LOG("SMW_finish_returns(argc = %d, vp = %p)\n", argc, vp);
JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
switch (rets().length()) {
case 0: {
break;
}
case 1: {
args.rval().set(rets().back());
break;
}
default: {
JSContext* cx = get_js_context();
JS::RootedVector<JS::Value> elems(cx);
if (!elems.reserve(rets().length())) {
abort(cx, "failed to reserve space for results vector");
}
for (size_t i = 0; i < rets().length(); i++) {
bool ok = elems.append(rets()[i]);
assert(ok && "already reserved space");
}
JS::RootedObject arr(cx, JS::NewArrayObject(cx, elems));
if (!arr) {
abort(cx, "failed to allocate array for function's return values");
}
args.rval().setObject(*arr.get());
break;
}
}
rets().clear();
}
WASM_EXPORT
uint32_t SMW_i32_from_u32(size_t i) {
SMW_LOG("SMW_i32_from_u32(i = %zu)\n", i);
JSContext* cx = get_js_context();
JS::RootedValue val(cx, operands()[i]);
double number = 0.0;
if (!JS::ToNumber(cx, val, &number)) {
abort(cx, "failed to convert value to number");
}
number = std::round(number);
return uint32_t(number);
}
WASM_EXPORT
void SMW_u32_from_i32(uint32_t x, size_t dest) {
SMW_LOG("SMW_u32_from_i32(x = %ull, dest = %zu)\n", x, dest);
JSContext* cx = get_js_context();
JS::RootedValue val(cx, JS::NumberValue(x));
save_operand(dest, val);
}
WASM_EXPORT
void SMW_string_canon_lower(uint32_t* ret_ptr, size_t i) {
SMW_LOG("SMW_string_canon_lower(ret_ptr = %p, i = %zu)\n", ret_ptr, i);
JSContext* cx = get_js_context();
JS::RootedValue strVal(cx, operands()[i]);
if (!strVal.isString()) {
abort(cx, "value is not a string");
}
JS::RootedString str(cx, strVal.toString());
JS::Rooted<JSLinearString*> linearStr(cx, JS_EnsureLinearString(cx, str));
if (!linearStr) {
abort(cx, "failed to linearize JS string");
}
size_t len = JS::GetDeflatedUTF8StringLength(linearStr);
char* ptr = static_cast<char*>(malloc(len));
if (!ptr) {
abort(cx, "out of memory");
}
size_t num_written = JS::DeflateStringToUTF8Buffer(linearStr, mozilla::Span(ptr, len));
assert(num_written == len);
ret_ptr[0] = reinterpret_cast<uint32_t>(ptr);
ret_ptr[1] = static_cast<uint32_t>(len);
}
WASM_EXPORT
void SMW_string_canon_lift(char* ptr, size_t len, size_t dest) {
SMW_LOG("SMW_string_canon_lift(ptr = %p, len = %zu, dest = %zu)\n", ptr, len, dest);
JSContext* cx = get_js_context();
JS::RootedString str(cx, JS_NewStringCopyUTF8N(cx, JS::UTF8Chars(ptr, len)));
if (!str) {
abort(cx, "failed to create JS string from UTF-8 buffer");
}
JS::RootedValue strVal(cx, JS::StringValue(str));
save_operand(dest, strVal);
}
WASM_EXPORT
uint32_t SMW_spread_into_array(size_t i) {
SMW_LOG("SMW_spread_into_array; i = %zu\n", i);
JSContext* cx = get_js_context();
JS::RootedValue iterable(cx, operands()[i]);
bool is_array = false;
if (!JS::IsArrayObject(cx, iterable, &is_array)) {
abort(cx, "failed to check if object is an array");
}
if (is_array) {
JS::RootedObject arr(cx, &iterable.toObject());
uint32_t length = 0;
if (!JS::GetArrayLength(cx, arr, &length)) {
abort(cx, "failed to get array length");
}
return length;
}
JS::RootedVector<JS::Value> elems(cx);
JS::ForOfIterator iter(cx);
if (!iter.init(iterable)) {
abort(cx, "failed to convert operand value to iterable");
}
JS::RootedValue val(cx);
bool done = false;
while (!done) {
if (!iter.next(&val, &done)) {
abort(cx, "failed to get the next value out of iterator");
}
if (done) {
break;
}
if (!elems.append(val)) {
abort(cx, "failed to append value to vector");
}
}
JS::RootedObject arr(cx, JS::NewArrayObject(cx, elems));
if (!arr) {
abort(cx, "failed to allocate JS array object");
}
operands()[i].setObject(*arr);
return elems.length();
}
WASM_EXPORT
void SMW_get_array_element(size_t array, size_t index, size_t dest) {
SMW_LOG("SMW_get_array_element(array = %zu, index = %zu, dest = %zu)\n", array, index, dest);
JSContext* cx = get_js_context();
JS::RootedValue array_val(cx, operands()[array]);
assert(array_val.isObject());
JS::RootedObject array_obj(cx, &array_val.toObject());
JS::RootedValue elem(cx);
if (!JS_GetElement(cx, array_obj, index, &elem)) {
abort(cx, "failed to get array element");
}
save_operand(dest, elem);
}
WASM_EXPORT
void SMW_new_array(size_t dest) {
SMW_LOG("SMW_new_array(dest = %zu)\n", dest);
JSContext* cx = get_js_context();
JS::RootedObject arr(cx, JS::NewArrayObject(cx, 0));
if (!arr) {
abort(cx, "failed to allocate a new JS array object");
}
JS::RootedValue arr_val(cx, JS::ObjectValue(*arr));
save_operand(dest, arr_val);
}
WASM_EXPORT
void SMW_array_push(size_t array, size_t elem) {
SMW_LOG("SMW_array_push(array = %zu, elem = %zu)\n", array, elem);
JSContext* cx = get_js_context();
JS::RootedValue array_val(cx, operands()[array]);
assert(array_val.isObject());
JS::RootedObject array_obj(cx, &array_val.toObject());
uint32_t length = 0;
if (!JS::GetArrayLength(cx, array_obj, &length)) {
abort(cx, "failed to get JS array object length");
}
JS::RootedValue elem_val(cx, operands()[elem]);
if (!JS_SetElement(cx, array_obj, length, elem_val)) {
abort(cx, "failed to set JS array element");
}
}
} // namespace smw

View File

@@ -0,0 +1,33 @@
#include <assert.h>
#include "smw/cx.h"
#include "jsapi.h"
namespace smw {
static JSContext* CONTEXT = nullptr;
void init_js_context(JSContext *cx) {
assert(!CONTEXT && "CONTEXT should only be initialized once");
CONTEXT = cx;
}
JSContext *get_js_context() {
assert(CONTEXT && "CONTEXT should be initialized");
return CONTEXT;
}
static JS::PersistentRooted<JSObject*> USER_MODULE;
void init_user_module(JSContext* cx, JSObject* user_module) {
assert(!USER_MODULE && "USER_MODULE should only be initialized once");
USER_MODULE.init(cx, user_module);
}
JSObject* get_user_module() {
assert(USER_MODULE && "USER_MODULE should be initialized");
return USER_MODULE;
}
} // namespace smw

View File

@@ -0,0 +1,48 @@
#include "smw/dump.h"
#include <assert.h>
#include "jsapi.h"
#include "smw/wasm.h"
namespace smw {
static JS::UniqueChars stringify_value(JSContext *cx, JS::HandleValue val) {
JS::RootedString str(cx, JS_ValueToSource(cx, val));
if (!str) {
return nullptr;
}
return JS_EncodeStringToUTF8(cx, str);
}
bool dump_value(JSContext *cx, JS::HandleValue val, FILE* fp) {
JS::UniqueChars str = stringify_value(cx, val);
if (!str) {
return false;
}
fprintf(fp, "%s\n", str.get());
return true;
}
bool dump_stack(JSContext *cx, JS::HandleObject stack, FILE* fp) {
JS::RootedString str(cx);
size_t indent = 4;
if (!JS::BuildStackString(cx, nullptr, stack, &str, indent)) {
return false;
}
JS::UniqueChars utf8 = JS_EncodeStringToUTF8(cx, str);
if (!utf8) {
return false;
}
fprintf(fp, "%s\n", utf8.get());
return true;
}
WASM_EXPORT
int32_t dump_i32(int32_t x) {
fprintf(stderr, "dump_i32: %d\n", x);
return x;
}
} // namespace smw

View File

@@ -0,0 +1,16 @@
#ifndef _smw_abort_h
#define _smw_abort_h
struct JSContext;
namespace smw {
/**
* Print the given error message and abort.
*/
void abort(const char* msg);
void abort(JSContext *cx, const char* msg);
}
#endif // _smw_abort_h

View File

@@ -0,0 +1,12 @@
#ifndef _smw_bindgen_h
#define _smw_bindgen_h
struct JSContext;
namespace smw {
void init_operands(JSContext* cx);
}
#endif // _smw_bindgen_h

View File

@@ -0,0 +1,17 @@
#ifndef _smw_cx_h
#define _smw_cx_h
struct JSContext;
class JSObject;
namespace smw {
void init_js_context(JSContext* cx);
JSContext* get_js_context();
void init_user_module(JSContext* cx, JSObject* user_module);
JSObject* get_user_module();
}
#endif // _smw_cx_h

View File

@@ -0,0 +1,27 @@
#ifndef _smw_dump_h
#define _smw_dump_h
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Winvalid-offsetof"
#include "js/TypeDecls.h"
#include "js/Value.h"
#pragma clang diagnostic pop
namespace smw {
/**
* Dump a human-readable representation of the given JS value to the given file.
*/
bool dump_value(JSContext *cx, JS::HandleValue val, FILE* fp);
/**
* Dump a human-readable representation of the given JS exception stack to the
* given file.
*/
bool dump_stack(JSContext *cx, JS::HandleObject stack, FILE* fp);
}
#endif // _smw_dump_h

View File

@@ -0,0 +1,15 @@
#ifndef _smw_logging_h
#define _smw_logging_h
#if LOGGING==1
#include <stdio.h>
#define SMW_LOG(msg, ...) fprintf(stderr, msg, ##__VA_ARGS__)
#else // LOGGING==1
#define SMW_LOG(msg, ...) do { } while(false)
#endif // LOGGING==1
#endif // _smw_logging_h

View File

@@ -0,0 +1,18 @@
#ifndef _smw_wasm_h
#define _smw_wasm_h
/**
* An attribute for making a function exported from the final Wasm binary.
*
* Example usage:
*
* WASM_EXPORT
* int add(int a, int b) {
* return a + b;
* }
*/
#define WASM_EXPORT \
__attribute__((visibility("default"))) \
extern "C"
#endif // _smw_wasm_h

View File

@@ -0,0 +1,380 @@
/*!
* JS engine initialization and JS top-level evaluation.
*
* This file contains the code to start up the JS engine, define import-able
* modules from VM functions, and evaluate the user JS.
*/
#include <assert.h>
#include <stdlib.h>
#include "smw/abort.h"
#include "smw/bindgen.h"
#include "smw/cx.h"
#include "smw/wasm.h"
#include "js/AllocPolicy.h"
#include "js/CompilationAndEvaluation.h"
#include "js/GCAPI.h"
#include "js/GCVector.h"
#include "js/Initialization.h"
#include "js/Modules.h"
#include "js/Promise.h"
#include "js/Realm.h"
#include "js/SourceText.h"
#include "js/TypeDecls.h"
#include "js/Warnings.h"
#include "jsapi.h"
#include "jsfriendapi.h"
namespace smw {
using UniqueChars = mozilla::UniquePtr<char[]>;
bool INITIALIZED = false;
JS::PersistentRootedObject GLOBAL;
static JSClass global_class = {
"global",
JSCLASS_GLOBAL_FLAGS,
&JS::DefaultGlobalClassOps
};
/**
* Compile the given JS source as a module in the context of the given global.
*
* Takes ownership of `jsSource`.
*
* Does not take ownership of `jsFileName`.
*
* Sets `outModule` to the resulting source text module record object.
*/
bool compile_js_module(JSContext *cx,
const char *jsFileName,
char *jsSource,
size_t jsSourceLen,
JS::MutableHandleObject outModule) {
JS::CompileOptions copts(cx);
copts
.setFileAndLine(jsFileName, 1)
.setNoScriptRval(true)
.setForceFullParse();
JS::SourceText<mozilla::Utf8Unit> srcBuf;
if (!srcBuf.init(cx, jsSource, jsSourceLen, JS::SourceOwnership::TakeOwnership)) {
return false;
}
JS::RootedObject module(cx);
// Disabling generational GC during compilation seems to slightly reduce
// the number of pages touched post-wizening. (Whereas disabling it
// during execution meaningfully increases it, which is why this is
// scoped to just compilation.)
JS::AutoDisableGenerationalGC noGgc(cx);
module = JS::CompileModule(cx, copts, srcBuf);
if (!module) {
return false;
}
outModule.set(module);
return true;
}
/**
* A synthesized module that exports `JSNative` functions.
*/
struct SynthesizedModule {
JS::Heap<JSString*> moduleName;
JS::Heap<JSObject*> moduleObject;
SynthesizedModule(JS::HandleString moduleName, JS::HandleObject moduleObject)
: moduleName(moduleName)
, moduleObject(moduleObject)
{ }
void trace(JSTracer* tracer) {
JS::TraceEdge(tracer, &moduleObject, "SynthesizedModule.moduleObject");
}
};
JS::PersistentRooted<JS::GCVector<SynthesizedModule, 0, js::SystemAllocPolicy>> MODULES;
JSObject* module_resolve_hook(JSContext *cx,
JS::HandleValue referencing_private,
JS::HandleObject module_request) {
JS::RootedString specifier(cx, JS::GetModuleRequestSpecifier(cx, module_request));
if (!specifier) {
abort(cx, "failed to get module request specifier");
}
size_t len = MODULES.length();
for (size_t i = 0; i < len; i++) {
JS::RootedObject it_module(cx, MODULES[i].get().moduleObject);
JS::RootedString it_name(cx, MODULES[i].get().moduleName);
int32_t result = 0;
if (!JS_CompareStrings(cx, it_name, specifier, &result)) {
abort(cx, "failed to compare module specifier to registered module name");
}
if (result == 0) {
return it_module.get();
}
}
JS::UniqueChars utf8 = JS_EncodeStringToUTF8(cx, specifier);
if (!utf8) {
JS_ReportErrorASCII(cx, "failed to find module import");
return nullptr;
}
JS_ReportErrorASCII(cx, "failed to find module import: `%s`", utf8.get());
return nullptr;
}
JS::RealmOptions make_realm_options() {
JS::RealmOptions options;
options
.creationOptions()
.setStreamsEnabled(true)
.setReadableByteStreamsEnabled(true)
.setBYOBStreamReadersEnabled(true)
.setReadableStreamPipeToEnabled(true)
.setWritableStreamsEnabled(true)
.setIteratorHelpersEnabled(true)
.setWeakRefsEnabled(JS::WeakRefSpecifier::EnabledWithoutCleanupSome);
return options;
}
bool init_js(JSContext *cx) {
if (!js::UseInternalJobQueues(cx)) {
return false;
}
if (!JS::InitSelfHostedCode(cx)) {
return false;
}
JS::RealmOptions options = make_realm_options();
JS::DisableIncrementalGC(cx);
JS::RootedObject global(cx, JS_NewGlobalObject(cx, &global_class, nullptr,
JS::FireOnNewGlobalHook, options));
if (!global) {
return false;
}
JS::EnterRealm(cx, global);
if (!JS::InitRealmStandardClasses(cx)) {
return false;
}
// JS::SetPromiseRejectionTrackerCallback(cx, rejection_tracker);
JS::SetModuleResolveHook(JS_GetRuntime(cx), module_resolve_hook);
GLOBAL.init(cx, global);
return true;
}
// static void report_warning(JSContext *cx, JSErrorReport *report) {
// JS::PrintError(stderr, report, true);
// if (!report->isWarning()) {
// ::abort();
// }
// }
/**
* Initialize the JS engine and evaluate the top-level of the given JavaScript
* source.
*
* Takes ownership of its parameters.
*/
WASM_EXPORT
void SMW_initialize_engine() {
assert(!INITIALIZED);
bool ok = true;
ok = JS_Init();
assert(ok && "JS_Init failed");
JSContext *cx = JS_NewContext(JS::DefaultHeapMaxBytes);
assert(cx != nullptr && "JS_NewContext failed");
init_js_context(cx);
// JS::SetWarningReporter(cx, report_warning);
if (!init_js(cx)) {
abort(cx, "initializing the JavaScript engine failed");
}
init_operands(cx);
MODULES.init(cx);
INITIALIZED = true;
}
class ModuleBuilder {
JS::PersistentRootedString moduleName;
JS::PersistentRooted<JS::IdValueVector> exports;
public:
/**
* Construct a new `ModuleBuilder` and take ownership of `moduleName`.
*/
ModuleBuilder(JSContext *cx, JS::HandleString moduleName)
: moduleName(cx, moduleName)
, exports(cx, cx)
{
assert(moduleName && "moduleName must not be nullptr");
}
/**
* Add an exported function to this module and take ownership of `funcName`.
*/
void add_export(const char *funcName, size_t funcNameLen, JSNative func, unsigned numArgs) {
assert(funcName && "function name must not be nullptr");
assert(funcNameLen > 0 && "function name length must be greater than zero");
assert(func && "the function must not be nullptr");
JSContext *cx = get_js_context();
JS::RootedString jsFuncName(cx, JS_NewStringCopyN(cx, funcName, funcNameLen));
if (!jsFuncName) {
abort(cx, "failed to create new JS string");
}
JS::RootedId funcNameId(cx);
if (!JS_StringToId(cx, jsFuncName, &funcNameId)) {
abort(cx, "failed to convert string to id");
}
JS::RootedFunction jsFunc(cx, JS_NewFunction(cx, func, numArgs, 0, funcName));
if (!jsFunc) {
abort(cx, "failed to create new JS function");
}
JS::RootedObject jsFuncObj(cx, JS_GetFunctionObject(jsFunc));
assert(jsFuncObj && "getting function object is infallible");
JS::RootedValue jsFuncVal(cx, JS::ObjectValue(*jsFuncObj));
if (!exports.append(JS::IdValuePair(funcNameId, jsFuncVal))) {
abort(cx, "failed to append export to exports list");
}
}
void finish() {
JSContext *cx = get_js_context();
JS::RootedObject module(cx, JS::CreateModule(cx, exports));
if (!module) {
abort(cx, "failed to create synthetic module");
}
if (!MODULES.append(SynthesizedModule(moduleName, module))) {
abort(cx, "failed to append to MODULES");
}
delete this;
}
};
WASM_EXPORT
ModuleBuilder *SMW_new_module_builder(char *module_name, size_t module_name_len) {
auto unique_module_name = UniqueChars(module_name);
JSContext *cx = get_js_context();
JS::RootedString js_module_name(cx, JS_NewStringCopyN(cx, unique_module_name.get(), module_name_len));
if (!js_module_name) {
abort(cx, "failed to allocate JS string");
}
auto b = new ModuleBuilder(cx, js_module_name);
if (!b) {
abort(cx, "failed to create new ModuleBuilder");
}
return b;
}
WASM_EXPORT
void SMW_module_builder_add_export(ModuleBuilder *builder,
char *funcName,
size_t funcNameLen,
JSNative func,
unsigned numArgs) {
assert(builder && "builder must not be nullptr");
assert(funcName && "funcName must not be nullptr");
assert(funcNameLen > 0 && "funcNameLen must be greater than 0");
assert(func && "func must not be nullptr");
auto uniqFuncName = UniqueChars(funcName);
builder->add_export(uniqFuncName.get(), funcNameLen, func, numArgs);
}
WASM_EXPORT
void SMW_finish_module_builder(ModuleBuilder *builder) {
builder->finish();
}
WASM_EXPORT
void SMW_eval_module(char *jsFileName, char *jsSource, size_t jsSourceLen) {
JSContext *cx = get_js_context();
assert(GLOBAL && "GLOBAL should be initialized");
JS::RootedObject global(cx, GLOBAL);
JSAutoRealm autoRealm(cx, global);
JS::RootedObject module(cx);
if (!compile_js_module(cx, jsFileName, jsSource, jsSourceLen, &module)) {
abort(cx, "module compilation failed");
}
if (!JS::ModuleInstantiate(cx, module)) {
abort(cx, "failed to instantiate module");
}
JS::RootedValue result(cx);
if (!JS::ModuleEvaluate(cx, module, &result)) {
abort(cx, "failed to evaluate module");
}
// TODO: if `result` is a promise because of top-level await, then don't
// return until the micro task queue is empty.
if (result.isObject()) {
JS::RootedObject resultObj(cx, &result.toObject());
if (!JS::IsPromiseObject(resultObj)) {
goto done_handling_promise;
}
switch (JS::GetPromiseState(resultObj)) {
case JS::PromiseState::Fulfilled: {
JS::RootedValue promiseResolution(cx, JS::GetPromiseResult(resultObj));
break;
}
case JS::PromiseState::Rejected: {
JS::RootedValue promiseRejection(cx, JS::GetPromiseResult(resultObj));
JS_SetPendingException(cx, promiseRejection);
abort(cx, "module evaluation failed");
}
case JS::PromiseState::Pending: {
abort(cx, "module evaluation returned a pending promise, but top-level await isn't enabled yet");
}
default:
abort(cx, "module evaluation returned a promise in an unknown promise state");
}
}
done_handling_promise:
init_user_module(cx, module);
JS::PrepareForFullGC(cx);
JS::NonIncrementalGC(cx, JS::GCOptions::Shrink, JS::GCReason::API);
free(jsFileName);
}
} // namespace smw

View File

@@ -0,0 +1,18 @@
#include <stdint.h>
#include <stdlib.h>
#include "smw/abort.h"
#include "smw/wasm.h"
namespace smw {
WASM_EXPORT
void* SMW_malloc(size_t size) {
auto p = malloc(size);
if (p == nullptr) {
abort("out of memory");
}
return p;
}
} // namespace smw

View File

@@ -0,0 +1,27 @@
WASI_SDK="$(pwd)/../wasi-sdk-12.0"
CC="$WASI_SDK/bin/clang --sysroot=$WASI_SDK/share/wasi-sysroot"
CXX="$WASI_SDK/bin/clang++ --sysroot=$WASI_SDK/share/wasi-sysroot"
AR="$WASI_SDK/bin/ar"
HOST_CC=gcc
HOST_CXX=g++
ac_add_options --enable-application=js
ac_add_options --target=wasm32-unknown-wasi
ac_add_options --without-system-zlib
ac_add_options --without-intl-api
ac_add_options --disable-jit
ac_add_options --disable-shared-js
ac_add_options --disable-shared-memory
# Comment out `--enable-optimize` and `--disable-debug` and then uncomment
# `--enable-debug` to switch to debug builds of SpiderMonkey. Also update
# `DEFINE=` in the `Makefile`.
ac_add_options --enable-optimize
ac_add_options --disable-debug
# ac_add_options --enable-debug
mk_add_options AUTOCLOBBER=1

View File

@@ -0,0 +1,72 @@
//! Assigning static locations for data segments we will emit in the glue Wasm
//! module.
use std::convert::TryFrom;
#[derive(Debug)]
pub struct DataSegments {
data: wasm_encoder::DataSection,
next_offset: u32,
memory: u32,
}
impl DataSegments {
/// Create a new collection of data segments for the given memory.
pub fn new(memory: u32) -> DataSegments {
DataSegments {
data: wasm_encoder::DataSection::new(),
next_offset: 0,
memory,
}
}
/// Add a new segment to this `DataSegments`, returning the assigned offset
/// in memory.
pub fn add<S>(&mut self, segment: S) -> u32
where
S: IntoIterator<Item = u8>,
S::IntoIter: ExactSizeIterator,
{
let segment = segment.into_iter();
let offset = self.reserve_space(u32::try_from(segment.len()).unwrap());
self.data.active(
self.memory,
&wasm_encoder::Instruction::I32Const(offset as i32),
segment,
);
offset
}
/// Reserve space in memory but don't emit any data segment to initialize
/// it.
///
/// This effectively lets you add zero-initialized data segments, reserve
/// space for return pointer areas, or define shadow stack regions.
pub fn reserve_space(&mut self, num_bytes: u32) -> u32 {
// Leave an empty byte between each data segment. This helps when
// staring at disassemblies and heap dumps.
self.next_offset += 1;
let offset = self.next_offset;
self.next_offset += num_bytes;
offset
}
/// Get the memory type required to hold these data segments.
pub fn memory_type(&self) -> wasm_encoder::MemoryType {
const WASM_PAGE_SIZE: u32 = 65_536;
wasm_encoder::MemoryType {
minimum: ((self.next_offset + WASM_PAGE_SIZE - 1) / WASM_PAGE_SIZE).into(),
maximum: None,
memory64: false,
}
}
/// Take the constructed data section.
///
/// No more data segments should be added after this is called.
pub fn take_data(&mut self) -> wasm_encoder::DataSection {
std::mem::replace(&mut self.data, wasm_encoder::DataSection::new())
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,31 @@
use std::path::Path;
mod imports {
test_helpers::codegen_spidermonkey_import!(
// TODO: should support more of the `*.wit` test suite
"strings.wit"
"simple-lists.wit"
"simple-functions.wit"
);
}
mod exports {
test_helpers::codegen_spidermonkey_export!(
// TODO: should support more of the `*.wit` test suite
"strings.wit"
"simple-lists.wit"
"simple-functions.wit"
);
}
fn verify(dir: &str, _name: &str) {
let wasm = std::fs::read(Path::new(dir).join("foo.wasm")).unwrap();
let mut validator = wasmparser::Validator::new();
validator.wasm_features(wasmparser::WasmFeatures {
bulk_memory: true,
module_linking: true,
multi_memory: true,
..wasmparser::WasmFeatures::default()
});
validator.validate_all(&wasm).expect("wasm isn't valid");
}