Skip to content

Commit 8aa233c

Browse files
phatedry
authored andcommitted
Add worker.LoadModule for loading ES Modules
1 parent e8e3a47 commit 8aa233c

File tree

4 files changed

+217
-1
lines changed

4 files changed

+217
-1
lines changed

binding.cc

+111-1
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ IN THE SOFTWARE.
2424
#include <stdlib.h>
2525
#include <string.h>
2626
#include <string>
27+
#include <map>
2728

2829
#include "binding.h"
2930
#include "libplatform/libplatform.h"
3031
#include "v8.h"
32+
#include "_cgo_export.h"
3133

3234
using namespace v8;
3335

@@ -37,6 +39,7 @@ struct worker_s {
3739
std::string last_exception;
3840
Persistent<Function> recv;
3941
Persistent<Context> context;
42+
std::map<std::string, Eternal<Module>> modules;
4043
};
4144

4245
// Extracts a C string from a V8 Utf8Value.
@@ -90,6 +93,30 @@ void ExitOnPromiseRejectCallback(PromiseRejectMessage promise_reject_message) {
9093
exit(1);
9194
}
9295

96+
MaybeLocal<Module> ResolveCallback(Local<Context> context,
97+
Local<String> specifier,
98+
Local<Module> referrer) {
99+
auto isolate = Isolate::GetCurrent();
100+
worker* w = (worker*)isolate->GetData(0);
101+
102+
HandleScope handle_scope(isolate);
103+
104+
String::Utf8Value str(specifier);
105+
const char* moduleName = *str;
106+
107+
if (w->modules.count(moduleName) == 0) {
108+
std::string out;
109+
out.append("Module (");
110+
out.append(moduleName);
111+
out.append(") has not been loaded");
112+
out.append("\n");
113+
w->last_exception = out;
114+
return MaybeLocal<Module>();
115+
}
116+
117+
return w->modules[moduleName].Get(isolate);
118+
}
119+
93120
// Exception details will be appended to the first argument.
94121
std::string ExceptionString(worker* w, TryCatch* try_catch) {
95122
std::string out;
@@ -151,7 +178,6 @@ std::string ExceptionString(worker* w, TryCatch* try_catch) {
151178
}
152179

153180
extern "C" {
154-
#include "_cgo_export.h"
155181

156182
const char* worker_version() { return V8::GetVersion(); }
157183

@@ -197,6 +223,90 @@ int worker_load(worker* w, char* name_s, char* source_s) {
197223
return 0;
198224
}
199225

226+
int worker_load_module(worker* w, char* name_s, char* source_s, int callback_index) {
227+
Locker locker(w->isolate);
228+
Isolate::Scope isolate_scope(w->isolate);
229+
HandleScope handle_scope(w->isolate);
230+
231+
Local<Context> context = Local<Context>::New(w->isolate, w->context);
232+
Context::Scope context_scope(context);
233+
234+
TryCatch try_catch(w->isolate);
235+
236+
Local<String> name = String::NewFromUtf8(w->isolate, name_s);
237+
Local<String> source_text = String::NewFromUtf8(w->isolate, source_s);
238+
239+
Local<Integer> line_offset = Integer::New(w->isolate, 0);
240+
Local<Integer> column_offset = Integer::New(w->isolate, 0);
241+
Local<Boolean> is_cross_origin = True(w->isolate);
242+
Local<Integer> script_id = Local<Integer>();
243+
Local<Value> source_map_url = Local<Value>();
244+
Local<Boolean> is_opaque = False(w->isolate);
245+
Local<Boolean> is_wasm = False(w->isolate);
246+
Local<Boolean> is_module = True(w->isolate);
247+
248+
ScriptOrigin origin(name, line_offset, column_offset, is_cross_origin,
249+
script_id, source_map_url, is_opaque, is_wasm, is_module);
250+
251+
ScriptCompiler::Source source(source_text, origin);
252+
Local<Module> module;
253+
254+
if (!ScriptCompiler::CompileModule(w->isolate, &source).ToLocal(&module)) {
255+
assert(try_catch.HasCaught());
256+
w->last_exception = ExceptionString(w, &try_catch);
257+
return 1;
258+
}
259+
260+
for (int i = 0; i < module->GetModuleRequestsLength(); i++) {
261+
Local<String> dependency = module->GetModuleRequest(i);
262+
String::Utf8Value str(dependency);
263+
char* dependencySpecifier = *str;
264+
265+
// If we've already loaded the module, skip resolving it.
266+
// TODO: Is there ever a time when the specifier would be the same
267+
// but would need to be resolved again?
268+
if (w->modules.count(dependencySpecifier) != 0) {
269+
continue;
270+
}
271+
272+
int ret = ResolveModule(dependencySpecifier, name_s, callback_index);
273+
if (ret != 0) {
274+
// TODO: Use module->GetModuleRequestLocation() to get source locations
275+
std::string out;
276+
out.append("Module (");
277+
out.append(dependencySpecifier);
278+
out.append(") has not been loaded");
279+
out.append("\n");
280+
w->last_exception = out;
281+
return ret;
282+
}
283+
}
284+
285+
Eternal<Module> persModule(w->isolate, module);
286+
w->modules[name_s] = persModule;
287+
288+
Maybe<bool> ok = module->InstantiateModule(context, ResolveCallback);
289+
290+
if (!ok.FromMaybe(false)) {
291+
// TODO: I'm not sure if this is needed
292+
if (try_catch.HasCaught()) {
293+
assert(try_catch.HasCaught());
294+
w->last_exception = ExceptionString(w, &try_catch);
295+
}
296+
return 2;
297+
}
298+
299+
MaybeLocal<Value> result = module->Evaluate(context);
300+
301+
if (result.IsEmpty()) {
302+
assert(try_catch.HasCaught());
303+
w->last_exception = ExceptionString(w, &try_catch);
304+
return 2;
305+
}
306+
307+
return 0;
308+
}
309+
200310
void Print(const FunctionCallbackInfo<Value>& args) {
201311
bool first = true;
202312
for (int i = 0; i < args.Length(); i++) {

binding.h

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ worker* worker_new(int table_index);
4444
// returns nonzero on error
4545
// get error from worker_last_exception
4646
int worker_load(worker* w, char* name_s, char* source_s);
47+
int worker_load_module(worker* w, char* name_s, char* source_s, int callback_index);
4748

4849
const char* worker_last_exception(worker* w);
4950

worker.go

+56
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ type workerTableIndex int
3838

3939
var workerTableLock sync.Mutex
4040

41+
// These are used for handling ModuleResolverCallbacks per LoadModule invocation
42+
var resolverTableLock sync.Mutex
43+
var nextResolverToken int
44+
var resolverFuncs = make(map[int]ModuleResolverCallback)
45+
4146
// This table will store all pointers to all active workers. Because we can't safely
4247
// pass pointers to Go objects to C, we instead pass a key to this table.
4348
var workerTable = make(map[workerTableIndex]*worker)
@@ -48,6 +53,9 @@ var workerTableNextAvailable workerTableIndex = 0
4853
// To receive messages from javascript.
4954
type ReceiveMessageCallback func(msg []byte) []byte
5055

56+
// To resolve modules from javascript.
57+
type ModuleResolverCallback func(moduleName, referrerName string) int
58+
5159
// Don't init V8 more than once.
5260
var initV8Once sync.Once
5361

@@ -125,6 +133,23 @@ func recvCb(buf unsafe.Pointer, buflen C.int, index workerTableIndex) C.buf {
125133
}
126134
}
127135

136+
//export ResolveModule
137+
func ResolveModule(moduleSpecifier *C.char, referrerSpecifier *C.char, resolverToken int) C.int {
138+
moduleName := C.GoString(moduleSpecifier)
139+
// TODO: Remove this when I'm not dealing with Node resolution anymore
140+
referrerName := C.GoString(referrerSpecifier)
141+
142+
resolverTableLock.Lock()
143+
resolve := resolverFuncs[resolverToken]
144+
resolverTableLock.Unlock()
145+
146+
if resolve == nil {
147+
return C.int(1)
148+
}
149+
ret := resolve(moduleName, referrerName)
150+
return C.int(ret)
151+
}
152+
128153
// Creates a new worker, which corresponds to a V8 isolate. A single threaded
129154
// standalone execution context.
130155
func New(cb ReceiveMessageCallback) *Worker {
@@ -185,6 +210,37 @@ func (w *Worker) Load(scriptName string, code string) error {
185210
return nil
186211
}
187212

213+
// LoadModule loads and executes a javascript module with filename specified by
214+
// scriptName and the contents of the module specified by the param code.
215+
// All `import` dependencies must be loaded before a script otherwise it will error.
216+
func (w *Worker) LoadModule(scriptName string, code string, resolve ModuleResolverCallback) error {
217+
scriptName_s := C.CString(scriptName)
218+
code_s := C.CString(code)
219+
defer C.free(unsafe.Pointer(scriptName_s))
220+
defer C.free(unsafe.Pointer(code_s))
221+
222+
// Register the callback before we attempt to load a module
223+
resolverTableLock.Lock()
224+
nextResolverToken++
225+
token := nextResolverToken
226+
resolverFuncs[token] = resolve
227+
resolverTableLock.Unlock()
228+
token_i := C.int(token)
229+
230+
r := C.worker_load_module(w.worker.cWorker, scriptName_s, code_s, token_i)
231+
232+
// Unregister the callback after the module is loaded
233+
resolverTableLock.Lock()
234+
delete(resolverFuncs, token)
235+
resolverTableLock.Unlock()
236+
237+
if r != 0 {
238+
errStr := C.GoString(C.worker_last_exception(w.worker.cWorker))
239+
return errors.New(errStr)
240+
}
241+
return nil
242+
}
243+
188244
// Same as Send but for []byte. $recv callback will get an ArrayBuffer.
189245
func (w *Worker) SendBytes(msg []byte) error {
190246
msg_p := C.CBytes(msg)

worker_test.go

+49
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,55 @@ func TestRequestFromJS(t *testing.T) {
217217
}
218218
}
219219

220+
func TestModules(t *testing.T) {
221+
var worker *Worker
222+
worker = New(func(msg []byte) []byte {
223+
t.Fatal("shouldn't recieve Message")
224+
return nil
225+
})
226+
err2 := worker.LoadModule("code.js", `
227+
import { test } from "dependency.js";
228+
V8Worker2.print(test);
229+
`, func(specifier string, referrer string) int {
230+
if specifier != "dependency.js" {
231+
t.Fatal(`Expected "dependency.js" specifier`)
232+
}
233+
if referrer != "code.js" {
234+
t.Fatal(`Expected "code.js" referrer`)
235+
}
236+
err1 := worker.LoadModule("dependency.js", `
237+
export const test = "ready";
238+
`, func(_, _ string) int {
239+
t.Fatal(`Expected module resolver callback to not be called`)
240+
return 1
241+
})
242+
if err1 != nil {
243+
t.Fatal(err1)
244+
}
245+
return 0
246+
})
247+
if err2 != nil {
248+
t.Fatal(err2)
249+
}
250+
}
251+
252+
func TestModulesMissingDependency(t *testing.T) {
253+
worker := New(func(msg []byte) []byte {
254+
t.Fatal("shouldn't recieve Message")
255+
return nil
256+
})
257+
err := worker.LoadModule("code.js", `
258+
import { test } from "missing.js";
259+
V8Worker2.print(test);
260+
`, func(specifier string, referrer string) int {
261+
if specifier != "missing.js" {
262+
t.Fatal(`Expected "missing.js" specifier`)
263+
}
264+
return 1
265+
})
266+
errorContains(t, err, "missing.js")
267+
}
268+
220269
// Test breaking script execution
221270
func TestWorkerBreaking(t *testing.T) {
222271
worker := New(func(msg []byte) []byte {

0 commit comments

Comments
 (0)