Skip to content

Commit 315c240

Browse files
Add support for running initialization scripts before content (#222)
This adds the ability to run a script in a separate global before content gets run. This code has access to additional abilities, notably including the `defineBuiltinModule` function. Building on this will allow us to change ComponentizeJS to support content using imports from `wasi:package-name` module names without rewriting the code first. That in turn will enable support for non-bundled content code.
1 parent 255d563 commit 315c240

File tree

13 files changed

+142
-15
lines changed

13 files changed

+142
-15
lines changed

componentize.sh.in

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ aot=@AOT@
99
preopen_dir="${PREOPEN_DIR:-}"
1010

1111
usage() {
12-
echo "Usage: $(basename "$0") [--verbose] [--legacy-script] [input.js] [-o output.wasm]"
12+
echo "Usage: $(basename "$0") [--verbose] [-i,--initializer-script-path] [--legacy-script] [input.js] [-o output.wasm]"
1313
echo " Providing an input file but no output uses the input base name with a .wasm extension"
1414
echo " Providing an output file but no input creates a component without running any top-level script"
15+
echo " Specifying '--verbose' causes the detailed output during initialization and execution"
16+
echo " Specifying '-i' or '--initializer-script-path' allows specifying an initializer script"
1517
echo " Specifying '--legacy-script' causes evaluation as a legacy JS script instead of a module"
1618
exit 1
1719
}
@@ -24,6 +26,7 @@ fi
2426
IN_FILE=""
2527
OUT_FILE=""
2628
LEGACY_SCRIPT_PARAM=""
29+
STARLING_ARGS=""
2730
VERBOSE=0
2831

2932
while [ $# -gt 0 ]
@@ -38,7 +41,12 @@ do
3841
OUT_FILE="$2"
3942
shift 2
4043
;;
44+
-i|--initializer-script-path)
45+
STARLING_ARGS="$STARLING_ARGS $1 $2"
46+
shift 2
47+
;;
4148
-v|--verbose)
49+
STARLING_ARGS="$1 $STARLING_ARGS"
4250
VERBOSE=1
4351
shift
4452
;;
@@ -76,9 +84,8 @@ if [[ -n "$IN_FILE" ]]; then
7684
fi
7785
echo "Componentizing $IN_FILE into $OUT_FILE"
7886

79-
STARLING_ARGS="$LEGACY_SCRIPT_PARAM$IN_FILE"
87+
STARLING_ARGS="$STARLING_ARGS $LEGACY_SCRIPT_PARAM$IN_FILE"
8088
if [[ $VERBOSE -ne 0 ]]; then
81-
STARLING_ARGS="--verbose $STARLING_ARGS"
8289
echo "Componentizing with args $STARLING_ARGS"
8390
fi
8491

include/config-parser.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@ class ConfigParser {
7777
config_->content_script_path.reset();
7878
i++;
7979
}
80+
}
81+
if (args[i] == "-i" || args[i] == "--initializer-script-path") {
82+
if (i + 1 < args.size()) {
83+
config_->initializer_script_path = mozilla::Some(args[i + 1]);
84+
i++;
85+
}
8086
} else if (args[i] == "-v" || args[i] == "--verbose") {
8187
config_->verbose = true;
8288
} else if (args[i] == "-d" || args[i] == "--enable-script-debugging") {

include/extension-api.h

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,19 @@ namespace api {
3535
class AsyncTask;
3636

3737
struct EngineConfig {
38-
mozilla::Maybe<std::string> content_script_path;
39-
mozilla::Maybe<std::string> content_script;
38+
mozilla::Maybe<std::string> content_script_path = mozilla::Nothing();
39+
mozilla::Maybe<std::string> content_script = mozilla::Nothing();
4040
bool module_mode = true;
4141

42+
/**
43+
* Path to the script to evaluate before the content script.
44+
*
45+
* This script is evaluated in a separate global and has access to functions not
46+
* available to content. It can be used to set up the environment for the content
47+
* script, e.g. by registering builtin modules or adding global properties.
48+
*/
49+
mozilla::Maybe<std::string> initializer_script_path = mozilla::Nothing();
50+
4251
/**
4352
* Whether to evaluate the top-level script in pre-initialization mode or not.
4453
*
@@ -102,6 +111,15 @@ class Engine {
102111
bool eval_toplevel(JS::SourceText<mozilla::Utf8Unit> &source, const char *path,
103112
MutableHandleValue result);
104113

114+
/**
115+
* Run the script set using the `-i | --initializer-script-path` option.
116+
*
117+
* This script runs in a separate global, and has access to functions not
118+
* available to content. Notably, that includes the ability to define
119+
* builtin modules, using the `defineBuiltinModule` function.
120+
*/
121+
bool run_initialization_script();
122+
105123
/**
106124
* Run the async event loop as long as there's interest registered in keeping it running.
107125
*

runtime/debugger.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ static bool print_location(JSContext *cx, FILE *fp = stdout) {
3535
return true;
3636
}
3737

38-
static bool dbg_print(JSContext *cx, unsigned argc, Value *vp) {
38+
bool content_debugger::dbg_print(JSContext *cx, unsigned argc, Value *vp) {
3939
CallArgs args = CallArgsFromVp(argc, vp);
4040

4141
if (!print_location(cx)) {
@@ -231,7 +231,7 @@ bool initialize_debugger(JSContext *cx, uint16_t port, bool content_already_init
231231
}
232232

233233
if (!JS_DefineFunction(cx, global, "setContentPath", dbg_set_content_path, 1, 0) ||
234-
!JS_DefineFunction(cx, global, "print", dbg_print, 1, 0) ||
234+
!JS_DefineFunction(cx, global, "print", content_debugger::dbg_print, 1, 0) ||
235235
!JS_DefineFunction(cx, global, "assert", dbg_assert, 1, 0)) {
236236
return false;
237237
}

runtime/debugger.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ namespace content_debugger {
2424
* @return the path to the replacement script, if any
2525
*/
2626
mozilla::Maybe<std::string_view> replacement_script_path();
27+
28+
bool dbg_print(JSContext *cx, unsigned argc, Value *vp);
2729
} // namespace content_debugger
2830

2931
#endif // DEBUGGER_H

runtime/engine.cpp

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,12 @@ Engine::Engine(std::unique_ptr<EngineConfig> config) {
425425
}
426426
#endif
427427

428+
if (config_->initializer_script_path) {
429+
if (!run_initialization_script()) {
430+
abort("running initialization script");
431+
}
432+
}
433+
428434
TRACE("Module mode: " << config_->module_mode);
429435
scriptLoader->enable_module_mode(config_->module_mode);
430436

@@ -493,9 +499,67 @@ void Engine::abort(const char *reason) {
493499
}
494500

495501
bool Engine::define_builtin_module(const char* id, HandleValue builtin) {
502+
TRACE("Defining builtin module '" << id << "'");
496503
return scriptLoader->define_builtin_module(id, builtin);
497504
}
498505

506+
static bool define_builtin_module(JSContext *cx, unsigned argc, Value *vp) {
507+
CallArgs args = CallArgsFromVp(argc, vp);
508+
auto name = core::encode(cx, args.get(0));
509+
if (!name) {
510+
return false;
511+
}
512+
if (!args.get(1).isObject()) {
513+
JS_ReportErrorUTF8(cx, "Second argument to defineBuiltinModule must be an object");
514+
return false;
515+
}
516+
if (!Engine::get(cx)->define_builtin_module(name.ptr.get(), args.get(1))) {
517+
return false;
518+
}
519+
520+
args.rval().setUndefined();
521+
return true;
522+
}
523+
524+
bool Engine::run_initialization_script() {
525+
auto cx = this->cx();
526+
527+
JS::RealmOptions options;
528+
options.creationOptions()
529+
.setStreamsEnabled(true)
530+
.setExistingCompartment(this->global());
531+
532+
static JSClass global_class = {"global", JSCLASS_GLOBAL_FLAGS, &JS::DefaultGlobalClassOps};
533+
RootedObject global(cx);
534+
global = JS_NewGlobalObject(cx, &global_class, nullptr, JS::DontFireOnNewGlobalHook, options);
535+
if (!global) {
536+
return false;
537+
}
538+
539+
JSAutoRealm ar(cx, global);
540+
541+
if (!JS_DefineFunction(cx, global, "defineBuiltinModule", ::define_builtin_module, 2, 0) ||
542+
!JS_DefineFunction(cx, global, "print", content_debugger::dbg_print, 1, 0)) {
543+
return false;
544+
}
545+
546+
auto path = config_->initializer_script_path.value();
547+
TRACE("Running initialization script from file " << path);
548+
JS::SourceText<mozilla::Utf8Unit> source;
549+
if (!scriptLoader->load_resolved_script(CONTEXT, path.c_str(), path.c_str(), source)) {
550+
return false;
551+
}
552+
auto opts = new JS::CompileOptions(cx);
553+
opts->setDiscardSource();
554+
opts->setFile(path.c_str());
555+
JS::RootedScript script(cx, Compile(cx, *opts, source));
556+
if (!script) {
557+
return false;
558+
}
559+
RootedValue result(cx);
560+
return JS_ExecuteScript(cx, script, &result);
561+
}
562+
499563
bool Engine::eval_toplevel(JS::SourceText<mozilla::Utf8Unit> &source, const char *path,
500564
MutableHandleValue result) {
501565
MOZ_ASSERT(state() > EngineState::EngineInitializing, "Engine must be done initializing");

runtime/script_loader.cpp

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,6 @@ static const char* resolve_path(const char* path, const char* base, size_t base_
132132
return resolve_extension(resolved_path);
133133
}
134134

135-
static bool load_script(JSContext *cx, const char *script_path, const char* resolved_path,
136-
JS::SourceText<mozilla::Utf8Unit> &script);
137-
138135
static JSObject* get_module(JSContext* cx, JS::SourceText<mozilla::Utf8Unit> &source,
139136
const char* resolved_path, const JS::CompileOptions &opts) {
140137
RootedObject module(cx, JS::CompileModule(cx, opts, source));
@@ -185,7 +182,7 @@ static JSObject* get_module(JSContext* cx, const char* specifier, const char* re
185182
}
186183

187184
JS::SourceText<mozilla::Utf8Unit> source;
188-
if (!load_script(cx, specifier, resolved_path, source)) {
185+
if (!SCRIPT_LOADER->load_resolved_script(cx, specifier, resolved_path, source)) {
189186
return nullptr;
190187
}
191188

@@ -387,8 +384,9 @@ void ScriptLoader::enable_module_mode(bool enable) {
387384
MODULE_MODE = enable;
388385
}
389386

390-
static bool load_script(JSContext *cx, const char *specifier, const char* resolved_path,
391-
JS::SourceText<mozilla::Utf8Unit> &script) {
387+
bool ScriptLoader::load_resolved_script(JSContext *cx, const char *specifier,
388+
const char* resolved_path,
389+
JS::SourceText<mozilla::Utf8Unit> &script) {
392390
FILE *file = fopen(resolved_path, "r");
393391
if (!file) {
394392
std::cerr << "Error opening file " << specifier << " (resolved to " << resolved_path << "): "
@@ -442,7 +440,7 @@ bool ScriptLoader::load_script(JSContext *cx, const char *script_path,
442440
resolved_path = resolve_path(script_path, BASE_PATH, strlen(BASE_PATH));
443441
}
444442

445-
return ::load_script(cx, script_path, resolved_path, script);
443+
return load_resolved_script(cx, script_path, resolved_path, script);
446444
}
447445

448446
bool ScriptLoader::eval_top_level_script(const char *path, JS::SourceText<mozilla::Utf8Unit> &source,

runtime/script_loader.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ class ScriptLoader {
2222
bool eval_top_level_script(const char *path, JS::SourceText<mozilla::Utf8Unit> &source,
2323
MutableHandleValue result, MutableHandleValue tla_promise);
2424
bool load_script(JSContext* cx, const char *script_path, JS::SourceText<mozilla::Utf8Unit> &script);
25+
26+
/**
27+
* Load a script without attempting to resolve its path relative to a base path.
28+
*
29+
* This is useful for loading ancillary scripts without interfering with, or depending on,
30+
* the script loader's state as determined by loading and running content scripts.
31+
*/
32+
bool load_resolved_script(JSContext *cx, const char *specifier, const char *resolved_path,
33+
JS::SourceText<mozilla::Utf8Unit> &script);
2534
};
2635

2736
#endif //SCRIPTLOADER_H
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
stdout [0] :: Log: foo
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { func } from "builtinMod";
2+
async function handle(event) {
3+
console.log(func());
4+
return new Response(func());
5+
}
6+
7+
//@ts-ignore
8+
addEventListener('fetch', (event) => { event.respondWith(handle(event)) });

0 commit comments

Comments
 (0)