Skip to content

Commit

Permalink
support builtin modules
Browse files Browse the repository at this point in the history
  • Loading branch information
guybedford committed Apr 19, 2024
1 parent 9da56c2 commit 2f8019f
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 4 deletions.
14 changes: 14 additions & 0 deletions include/extension-api.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,20 @@ class Engine {
JSContext *cx();
HandleObject global();

/**
* Define a new builtin import
*
* The enumerable properties of the builtin object are used to construct
* a synthetic module namespace for the module.
*
* The enumeration and getters are called only on the first import of
* the builtin, so that lazy getters can be used to lazily initialize
* builtins.
*
* Once loaded, the instance is cached and reused as a singleton.
*/
bool define_builtin_import(const char* id, HandleValue builtin);

/**
* Treat the top-level script as a module or classic JS script.
*
Expand Down
4 changes: 4 additions & 0 deletions runtime/engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,10 @@ HandleValue api::Engine::script_value() {

void api::Engine::abort(const char *reason) { ::abort(CONTEXT, reason); }

bool api::Engine::define_builtin_import(const char* id, HandleValue builtin) {
return scriptLoader->define_builtin_import(id, builtin);
}

bool api::Engine::eval_toplevel(const char *path, MutableHandleValue result) {
JSContext *cx = CONTEXT;
RootedValue ns(cx);
Expand Down
157 changes: 153 additions & 4 deletions runtime/script_loader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
#include <cstdio>
#include <iostream>
#include <js/CompilationAndEvaluation.h>
#include <jsfriendapi.h>
#include <js/MapAndSet.h>
#include <js/Value.h>
#include <sys/stat.h>

static JSContext* CONTEXT;
static ScriptLoader* SCRIPT_LOADER;
JS::PersistentRootedObject moduleRegistry;
JS::PersistentRootedObject builtinImports;
static bool MODULE_MODE = true;
static char* BASE_PATH = nullptr;
JS::CompileOptions *COMPILE_OPTS;
Expand Down Expand Up @@ -124,7 +126,7 @@ static JSObject* get_module(JSContext* cx, const char* specifier, const char* re
return nullptr;
}

if (!JS_DefineProperty(cx, info, "path", resolved_path_val, JSPROP_ENUMERATE)) {
if (!JS_DefineProperty(cx, info, "id", resolved_path_val, JSPROP_ENUMERATE)) {
return nullptr;
}

Expand All @@ -137,6 +139,99 @@ static JSObject* get_module(JSContext* cx, const char* specifier, const char* re
return module;
}

static JSObject* get_builtin_module(JSContext* cx, HandleValue id, HandleObject builtin) {
RootedValue module_val(cx);
MOZ_ASSERT(id.isString());
if (!JS::MapGet(cx, moduleRegistry, id, &module_val)) {
return nullptr;
}
if (!module_val.isUndefined()) {
return &module_val.toObject();
}

JS::CompileOptions opts(cx, *COMPILE_OPTS);
JS::SourceText<mozilla::Utf8Unit> source;

std::string code = "const { ";
JS::RootedIdVector props(cx);
GetPropertyKeys(cx, builtin, JSITER_OWNONLY, &props);

size_t length = props.length();
bool firstValue = true;
for (size_t i = 0; i < length; ++i) {
if (firstValue) {
firstValue = false;
} else {
code += ", ";
}

code += "'";
const auto &prop = props[i];
JS::RootedValue key(cx, js::IdToValue(prop));
if (!key.isString()) {
return nullptr;
}
auto key_str = core::encode(cx, key);
code += std::string_view(key_str.ptr.get(), key_str.len);
code += "': ";

code += "e";
code += std::to_string(i);
}

code += " } = import.meta.builtin;\nexport { ";

firstValue = true;
for (size_t i = 0; i < length; ++i) {
if (firstValue) {
firstValue = false;
} else {
code += ", ";
}

code += "e";
code += std::to_string(i);

code += " as '";
const auto &prop = props[i];
JS::RootedValue key(cx, js::IdToValue(prop));
if (!key.isString()) {
return nullptr;
}
auto key_str = core::encode(cx, key);
code += std::string_view(key_str.ptr.get(), key_str.len);
code += "'";
}
code += " }\n";

if (!source.init(cx, code.c_str(), strlen(code.c_str()), JS::SourceOwnership::Borrowed)) {
return nullptr;
}

RootedObject module(cx, JS::CompileModule(cx, opts, source));
if (!module) {
return nullptr;
}
module_val.setObject(*module);

RootedObject info(cx, JS_NewPlainObject(cx));
if (!info) {
return nullptr;
}

if (!JS_DefineProperty(cx, info, "id", id, JSPROP_ENUMERATE)) {
return nullptr;
}

SetModulePrivate(module, ObjectValue(*info));

if (!MapSet(cx, moduleRegistry, id, module_val)) {
return nullptr;
}

return module;
}

JSObject* module_resolve_hook(JSContext* cx, HandleValue referencingPrivate,
HandleObject moduleRequest) {
RootedString specifier(cx, GetModuleRequestSpecifier(cx, moduleRequest));
Expand All @@ -150,9 +245,19 @@ JSObject* module_resolve_hook(JSContext* cx, HandleValue referencingPrivate,
return nullptr;
}

RootedValue builtin_val(cx);
if (!MapGet(cx, builtinImports, path_val, &builtin_val)) {
return nullptr;
}
if (!builtin_val.isUndefined()) {
RootedValue specifier_val(cx, StringValue(specifier));
RootedObject builtin_obj(cx, &builtin_val.toObject());
return get_builtin_module(cx, specifier_val, builtin_obj);
}

RootedObject info(cx, &referencingPrivate.toObject());
RootedValue parent_path_val(cx);
if (!JS_GetProperty(cx, info, "path", &parent_path_val)) {
if (!JS_GetProperty(cx, info, "id", &parent_path_val)) {
return nullptr;
}
if (!parent_path_val.isString()) {
Expand All @@ -167,15 +272,59 @@ JSObject* module_resolve_hook(JSContext* cx, HandleValue referencingPrivate,
return get_module(cx, path.get(), resolved_path, opts);
}

ScriptLoader::ScriptLoader(JSContext *cx, JS::CompileOptions *opts) {
bool module_metadata_hook(JSContext* cx, HandleValue referencingPrivate, HandleObject metaObject) {
RootedObject info(cx, &referencingPrivate.toObject());
RootedValue parent_id_val(cx);
if (!JS_GetProperty(cx, info, "id", &parent_id_val)) {
return false;
}
if (!parent_id_val.isString()) {
return false;
}
RootedValue builtin_val(cx);
if (!MapGet(cx, builtinImports, parent_id_val, &builtin_val)) {
return false;
}
if (builtin_val.isUndefined()) {
return false;
}
JS_SetProperty(cx, metaObject, "builtin", builtin_val);
return true;
}

ScriptLoader::ScriptLoader(JSContext *cx, JS::CompileOptions *opts) {
MOZ_ASSERT(!SCRIPT_LOADER);
SCRIPT_LOADER = this;
CONTEXT = cx;
COMPILE_OPTS = opts;
moduleRegistry.init(cx, JS::NewMapObject(cx));
builtinImports.init(cx, JS::NewMapObject(cx));
MOZ_RELEASE_ASSERT(moduleRegistry);
MOZ_RELEASE_ASSERT(builtinImports);
JSRuntime *rt = JS_GetRuntime(cx);
SetModuleResolveHook(rt, module_resolve_hook);
SetModuleMetadataHook(rt, module_metadata_hook);
}

bool ScriptLoader::define_builtin_import(const char* id, HandleValue builtin) {
RootedString id_str(CONTEXT, JS_NewStringCopyZ(CONTEXT, id));
if (!id_str) {
return false;
}
RootedValue module_val(CONTEXT);
RootedValue id_val(CONTEXT, StringValue(id_str));
bool already_exists;
if (!MapHas(CONTEXT, builtinImports, id_val, &already_exists)) {
return false;
}
if (already_exists) {
fprintf(stderr, "Unable to define builtin %s, as it already exists", id);
return false;
}
if (!MapSet(CONTEXT, builtinImports, id_val, builtin)) {
return false;
}
return true;
}

void ScriptLoader::enable_module_mode(bool enable) {
Expand Down Expand Up @@ -286,7 +435,7 @@ bool ScriptLoader::load_top_level_script(const char *path, MutableHandleValue re
if (!MODULE_MODE) {
return JS_ExecuteScript(cx, script, result);
}

if (!ModuleEvaluate(cx, module, tla_promise)) {
return false;
}
Expand Down
1 change: 1 addition & 0 deletions runtime/script_loader.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class ScriptLoader {
ScriptLoader(JSContext* cx, JS::CompileOptions* opts);
~ScriptLoader();

bool define_builtin_import(const char* id, HandleValue builtin);
void enable_module_mode(bool enable);
bool load_top_level_script(const char *path, MutableHandleValue result, MutableHandleValue tla_promise);
bool load_script(JSContext* cx, const char *script_path, JS::SourceText<mozilla::Utf8Unit> &script);
Expand Down

0 comments on commit 2f8019f

Please sign in to comment.