Files
simple-rust-tests/__wasm/wit-bindgen-sample/wit-bindgen/crates/gen-spidermonkey/spidermonkey-wasm/initialize.cpp

381 lines
11 KiB
C++

/*!
* 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