Branch data Line data Source code
1 : : /* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
2 : : // SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
3 : : // SPDX-FileCopyrightText: 2017 Philip Chimento <philip.chimento@gmail.com>
4 : :
5 : : #include <config.h>
6 : :
7 : : #include <stddef.h> // for size_t
8 : : #include <string.h>
9 : :
10 : : #include <gio/gio.h>
11 : : #include <glib-object.h>
12 : : #include <glib.h>
13 : :
14 : : #include <js/CallAndConstruct.h>
15 : : #include <js/CallArgs.h>
16 : : #include <js/CharacterEncoding.h> // for ConstUTF8CharsZ
17 : : #include <js/Class.h>
18 : : #include <js/CompilationAndEvaluation.h>
19 : : #include <js/CompileOptions.h>
20 : : #include <js/Conversions.h>
21 : : #include <js/ErrorReport.h> // for JS_ReportOutOfMemory
22 : : #include <js/Exception.h>
23 : : #include <js/GCVector.h> // for RootedVector
24 : : #include <js/GlobalObject.h> // for CurrentGlobalOrNull
25 : : #include <js/Id.h>
26 : : #include <js/Modules.h>
27 : : #include <js/Object.h>
28 : : #include <js/Promise.h>
29 : : #include <js/PropertyAndElement.h>
30 : : #include <js/PropertyDescriptor.h>
31 : : #include <js/RootingAPI.h>
32 : : #include <js/ScriptPrivate.h>
33 : : #include <js/SourceText.h>
34 : : #include <js/String.h>
35 : : #include <js/TypeDecls.h>
36 : : #include <js/Utility.h> // for UniqueChars
37 : : #include <js/Value.h>
38 : : #include <js/ValueArray.h>
39 : : #include <jsapi.h> // for JS_GetFunctionObject, JS_Ne...
40 : : #include <jsfriendapi.h> // for NewFunctionWithReserved
41 : : #include <mozilla/Maybe.h>
42 : :
43 : : #include "gjs/atoms.h"
44 : : #include "gjs/context-private.h"
45 : : #include "gjs/deprecation.h"
46 : : #include "gjs/global.h"
47 : : #include "gjs/jsapi-util-args.h"
48 : : #include "gjs/jsapi-util.h"
49 : : #include "gjs/macros.h"
50 : : #include "gjs/mem-private.h"
51 : : #include "gjs/module.h"
52 : : #include "gjs/native.h"
53 : : #include "util/log.h"
54 : : #include "util/misc.h"
55 : :
56 : : namespace mozilla {
57 : : union Utf8Unit;
58 : : }
59 : :
60 : : class GjsScriptModule {
61 : : GjsAutoChar m_name;
62 : :
63 : : // Reserved slots
64 : : static const size_t POINTER = 0;
65 : :
66 : 896 : GjsScriptModule(const char* name) : m_name(g_strdup(name)) {
67 : 448 : GJS_INC_COUNTER(module);
68 : 448 : }
69 : :
70 : 443 : ~GjsScriptModule() { GJS_DEC_COUNTER(module); }
71 : :
72 : : GjsScriptModule(GjsScriptModule&) = delete;
73 : : GjsScriptModule& operator=(GjsScriptModule&) = delete;
74 : :
75 : : /* Private data accessors */
76 : :
77 : 269873 : [[nodiscard]] static inline GjsScriptModule* priv(JSObject* module) {
78 : 269873 : return JS::GetMaybePtrFromReservedSlot<GjsScriptModule>(
79 : 269873 : module, GjsScriptModule::POINTER);
80 : : }
81 : :
82 : : /* Creates a JS module object. Use instead of the class's constructor */
83 : 448 : [[nodiscard]] static JSObject* create(JSContext* cx, const char* name) {
84 : 448 : JSObject* module = JS_NewObject(cx, &GjsScriptModule::klass);
85 : 448 : JS::SetReservedSlot(module, GjsScriptModule::POINTER,
86 : 448 : JS::PrivateValue(new GjsScriptModule(name)));
87 : 448 : return module;
88 : : }
89 : :
90 : : /* Defines the empty module as a property on the importer */
91 : : GJS_JSAPI_RETURN_CONVENTION
92 : : bool
93 : 448 : define_import(JSContext *cx,
94 : : JS::HandleObject module,
95 : : JS::HandleObject importer,
96 : : JS::HandleId name) const
97 : : {
98 [ - + ]: 448 : if (!JS_DefinePropertyById(cx, importer, name, module,
99 : : GJS_MODULE_PROP_FLAGS & ~JSPROP_PERMANENT)) {
100 : 0 : gjs_debug(GJS_DEBUG_IMPORTER, "Failed to define '%s' in importer",
101 : : m_name.get());
102 : 0 : return false;
103 : : }
104 : :
105 : 448 : return true;
106 : : }
107 : :
108 : : /* Carries out the actual execution of the module code */
109 : : GJS_JSAPI_RETURN_CONVENTION
110 : 448 : bool evaluate_import(JSContext* cx, JS::HandleObject module,
111 : : const char* source, size_t source_len,
112 : : const char* filename, const char* uri) {
113 : 448 : JS::SourceText<mozilla::Utf8Unit> buf;
114 [ - + ]: 448 : if (!buf.init(cx, source, source_len, JS::SourceOwnership::Borrowed))
115 : 0 : return false;
116 : :
117 : 448 : JS::RootedObjectVector scope_chain(cx);
118 [ - + ]: 448 : if (!scope_chain.append(module)) {
119 : 0 : JS_ReportOutOfMemory(cx);
120 : 0 : return false;
121 : : }
122 : :
123 : 448 : JS::CompileOptions options(cx);
124 : 448 : options.setFileAndLine(filename, 1).setNonSyntacticScope(true);
125 : :
126 : 448 : JS::RootedObject priv(cx, build_private(cx, uri));
127 [ - + ]: 448 : if (!priv)
128 : 0 : return false;
129 : :
130 : 448 : JS::RootedScript script(cx, JS::Compile(cx, options, buf));
131 [ + + ]: 448 : if (!script)
132 : 1 : return false;
133 : :
134 : 447 : JS::SetScriptPrivate(script, JS::ObjectValue(*priv));
135 : 447 : JS::RootedValue ignored_retval(cx);
136 [ + + ]: 447 : if (!JS_ExecuteScript(cx, scope_chain, script, &ignored_retval))
137 : 3 : return false;
138 : :
139 : 444 : GjsContextPrivate* gjs = GjsContextPrivate::from_cx(cx);
140 : 444 : gjs->schedule_gc_if_needed();
141 : :
142 : 444 : gjs_debug(GJS_DEBUG_IMPORTER, "Importing module %s succeeded",
143 : : m_name.get());
144 : :
145 : 444 : return true;
146 : 448 : }
147 : :
148 : : /* Loads JS code from a file and imports it */
149 : : GJS_JSAPI_RETURN_CONVENTION
150 : : bool
151 : 448 : import_file(JSContext *cx,
152 : : JS::HandleObject module,
153 : : GFile *file)
154 : : {
155 : 448 : GjsAutoError error;
156 : 448 : GjsAutoChar script;
157 : 448 : size_t script_len = 0;
158 : :
159 [ - + ]: 448 : if (!(g_file_load_contents(file, nullptr, script.out(), &script_len,
160 : : nullptr, &error)))
161 : 0 : return gjs_throw_gerror_message(cx, error);
162 : 448 : g_assert(script);
163 : :
164 : 448 : GjsAutoChar full_path = g_file_get_parse_name(file);
165 : 448 : GjsAutoChar uri = g_file_get_uri(file);
166 : 448 : return evaluate_import(cx, module, script, script_len, full_path, uri);
167 : 448 : }
168 : :
169 : : /* JSClass operations */
170 : :
171 : : GJS_JSAPI_RETURN_CONVENTION
172 : : bool
173 : 268534 : resolve_impl(JSContext *cx,
174 : : JS::HandleObject module,
175 : : JS::HandleId id,
176 : : bool *resolved)
177 : : {
178 : 268534 : JS::RootedObject lexical(cx, JS_ExtensibleLexicalEnvironment(module));
179 [ - + ]: 268534 : if (!lexical) {
180 : 0 : *resolved = false;
181 : 0 : return true; /* nothing imported yet */
182 : : }
183 : :
184 : 268534 : JS::Rooted<mozilla::Maybe<JS::PropertyDescriptor>> maybe_desc(cx);
185 : 268534 : JS::RootedObject holder(cx);
186 [ - + ]: 268534 : if (!JS_GetPropertyDescriptorById(cx, lexical, id, &maybe_desc,
187 : : &holder))
188 : 0 : return false;
189 [ + + ]: 268534 : if (maybe_desc.isNothing())
190 : 268532 : return true;
191 : :
192 : : /* The property is present in the lexical environment. This should not
193 : : * be supported according to ES6. For compatibility with earlier GJS,
194 : : * we treat it as if it were a real property, but warn about it. */
195 : :
196 : 4 : _gjs_warn_deprecated_once_per_callsite(
197 : : cx, GjsDeprecationMessageId::ModuleExportedLetOrConst,
198 : 4 : {gjs_debug_id(id).c_str(), m_name});
199 : :
200 : 2 : JS::Rooted<JS::PropertyDescriptor> desc(cx, maybe_desc.value());
201 : 2 : return JS_DefinePropertyById(cx, module, id, desc);
202 : 268534 : }
203 : :
204 : : GJS_JSAPI_RETURN_CONVENTION
205 : : static bool
206 : 268534 : resolve(JSContext *cx,
207 : : JS::HandleObject module,
208 : : JS::HandleId id,
209 : : bool *resolved)
210 : : {
211 : 268534 : return priv(module)->resolve_impl(cx, module, id, resolved);
212 : : }
213 : :
214 : 443 : static void finalize(JS::GCContext*, JSObject* module) {
215 [ + - ]: 443 : delete priv(module);
216 : 443 : }
217 : :
218 : : static constexpr JSClassOps class_ops = {
219 : : nullptr, // addProperty
220 : : nullptr, // deleteProperty
221 : : nullptr, // enumerate
222 : : nullptr, // newEnumerate
223 : : &GjsScriptModule::resolve,
224 : : nullptr, // mayResolve
225 : : &GjsScriptModule::finalize,
226 : : };
227 : :
228 : : static constexpr JSClass klass = {
229 : : "GjsScriptModule",
230 : : JSCLASS_HAS_RESERVED_SLOTS(1) | JSCLASS_BACKGROUND_FINALIZE,
231 : : &GjsScriptModule::class_ops,
232 : : };
233 : :
234 : : public:
235 : : /*
236 : : * Creates a JS object to pass to JS::SetScriptPrivate as a script's
237 : : * private.
238 : : */
239 : : GJS_JSAPI_RETURN_CONVENTION
240 : 699 : static JSObject* build_private(JSContext* cx, const char* script_uri) {
241 : 699 : JS::RootedObject priv(cx, JS_NewPlainObject(cx));
242 : 699 : const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
243 : :
244 : 699 : JS::RootedValue val(cx);
245 [ + - ]: 1398 : if (!gjs_string_from_utf8(cx, script_uri, &val) ||
246 [ - + - + ]: 1398 : !JS_SetPropertyById(cx, priv, atoms.uri(), val))
247 : 0 : return nullptr;
248 : :
249 : 699 : return priv;
250 : 699 : }
251 : :
252 : : /* Carries out the import operation */
253 : : GJS_JSAPI_RETURN_CONVENTION
254 : : static JSObject *
255 : 448 : import(JSContext *cx,
256 : : JS::HandleObject importer,
257 : : JS::HandleId id,
258 : : const char *name,
259 : : GFile *file)
260 : : {
261 : 448 : JS::RootedObject module(cx, GjsScriptModule::create(cx, name));
262 : 448 : if (!module ||
263 [ + - + - ]: 896 : !priv(module)->define_import(cx, module, importer, id) ||
264 [ + + + + ]: 896 : !priv(module)->import_file(cx, module, file))
265 : 4 : return nullptr;
266 : :
267 : 444 : return module;
268 : 448 : }
269 : : };
270 : :
271 : : /**
272 : : * gjs_script_module_build_private:
273 : : * @param cx the #JSContext
274 : : * @param uri the URI this script module is loaded from
275 : : *
276 : : * @brief To support dynamic imports from scripts, we need to provide private
277 : : * data when we compile scripts which is compatible with our module resolution
278 : : * hooks in modules/internal/loader.js
279 : : *
280 : : * @returns a JSObject which can be used for a JSScript's private data.
281 : : */
282 : 251 : JSObject* gjs_script_module_build_private(JSContext* cx, const char* uri) {
283 : 251 : return GjsScriptModule::build_private(cx, uri);
284 : : }
285 : :
286 : : /**
287 : : * gjs_module_import:
288 : : * @cx: the JS context
289 : : * @importer: the JS importer object, parent of the module to be imported
290 : : * @id: module name in the form of a jsid
291 : : * @name: module name, used for logging and identification
292 : : * @file: location of the file to import
293 : : *
294 : : * Carries out an import of a GJS module.
295 : : * Defines a property @name on @importer pointing to the module object, which
296 : : * is necessary in the case of cyclic imports.
297 : : * This property is not permanent; the caller is responsible for making it
298 : : * permanent if the import succeeds.
299 : : *
300 : : * Returns: the JS module object, or nullptr on failure.
301 : : */
302 : : JSObject *
303 : 448 : gjs_module_import(JSContext *cx,
304 : : JS::HandleObject importer,
305 : : JS::HandleId id,
306 : : const char *name,
307 : : GFile *file)
308 : : {
309 : 448 : return GjsScriptModule::import(cx, importer, id, name, file);
310 : : }
311 : :
312 : : decltype(GjsScriptModule::klass) constexpr GjsScriptModule::klass;
313 : : decltype(GjsScriptModule::class_ops) constexpr GjsScriptModule::class_ops;
314 : :
315 : : /**
316 : : * gjs_get_native_registry:
317 : : *
318 : : * @brief Retrieves a global's native registry from the NATIVE_REGISTRY slot.
319 : : * Registries are JS Map objects created with JS::NewMapObject instead
320 : : * of GCHashMaps (used elsewhere in GJS) because the objects need to be
321 : : * exposed to internal JS code and accessed from native C++ code.
322 : : *
323 : : * @param global a global #JSObject
324 : : *
325 : : * @returns the registry map as a #JSObject
326 : : */
327 : 29241 : JSObject* gjs_get_native_registry(JSObject* global) {
328 : : JS::Value native_registry =
329 : 29241 : gjs_get_global_slot(global, GjsGlobalSlot::NATIVE_REGISTRY);
330 : :
331 : 29241 : g_assert(native_registry.isObject());
332 : 29241 : return &native_registry.toObject();
333 : : }
334 : :
335 : : /**
336 : : * gjs_get_module_registry:
337 : : *
338 : : * @brief Retrieves a global's module registry from the MODULE_REGISTRY slot.
339 : : * Registries are JS Maps. See gjs_get_native_registry for more detail.
340 : : *
341 : : * @param cx the current #JSContext
342 : : * @param global a global #JSObject
343 : : *
344 : : * @returns the registry map as a #JSObject
345 : : */
346 : 5298 : JSObject* gjs_get_module_registry(JSObject* global) {
347 : : JS::Value esm_registry =
348 : 5298 : gjs_get_global_slot(global, GjsGlobalSlot::MODULE_REGISTRY);
349 : :
350 : 5298 : g_assert(esm_registry.isObject());
351 : 5298 : return &esm_registry.toObject();
352 : : }
353 : :
354 : : /**
355 : : * gjs_module_load:
356 : : *
357 : : * Loads and registers a module given a specifier and
358 : : * URI.
359 : : *
360 : : * @returns whether an error occurred while resolving the specifier.
361 : : */
362 : 364 : JSObject* gjs_module_load(JSContext* cx, const char* identifier,
363 : : const char* file_uri) {
364 : 364 : g_assert((gjs_global_is_type(cx, GjsGlobalType::DEFAULT) ||
365 : : gjs_global_is_type(cx, GjsGlobalType::INTERNAL)) &&
366 : : "gjs_module_load can only be called from module-enabled "
367 : : "globals.");
368 : :
369 : 364 : JS::RootedObject global(cx, JS::CurrentGlobalOrNull(cx));
370 : : JS::RootedValue v_loader(
371 : 364 : cx, gjs_get_global_slot(global, GjsGlobalSlot::MODULE_LOADER));
372 : 364 : g_assert(v_loader.isObject());
373 : 364 : JS::RootedObject loader(cx, &v_loader.toObject());
374 : :
375 : 364 : JS::ConstUTF8CharsZ id_chars(identifier, strlen(identifier));
376 : 364 : JS::ConstUTF8CharsZ uri_chars(file_uri, strlen(file_uri));
377 : 364 : JS::RootedString id(cx, JS_NewStringCopyUTF8Z(cx, id_chars));
378 [ - + ]: 364 : if (!id)
379 : 0 : return nullptr;
380 : 364 : JS::RootedString uri(cx, JS_NewStringCopyUTF8Z(cx, uri_chars));
381 [ - + ]: 364 : if (!uri)
382 : 0 : return nullptr;
383 : :
384 : 364 : JS::RootedValueArray<2> args(cx);
385 : 364 : args[0].setString(id);
386 : 364 : args[1].setString(uri);
387 : :
388 : 364 : gjs_debug(GJS_DEBUG_IMPORTER,
389 : : "Module resolve hook for module '%s' (%s), global %p", identifier,
390 : 364 : file_uri, global.get());
391 : :
392 : 364 : JS::RootedValue result(cx);
393 [ + + ]: 364 : if (!JS::Call(cx, loader, "moduleLoadHook", args, &result))
394 : 1 : return nullptr;
395 : :
396 : 363 : g_assert(result.isObject() && "Module hook failed to return an object!");
397 : 363 : return &result.toObject();
398 : 364 : }
399 : :
400 : : /**
401 : : * import_native_module_sync:
402 : : *
403 : : * @brief Synchronously imports native "modules" from the import global's
404 : : * native registry. This function does not do blocking I/O so it is
405 : : * safe to call it synchronously for accessing native "modules" within
406 : : * modules. This function is always called within the import global's
407 : : * realm.
408 : : *
409 : : * Compare gjs_import_native_module() for the legacy importer.
410 : : *
411 : : * @param cx the current JSContext
412 : : * @param argc
413 : : * @param vp
414 : : *
415 : : * @returns whether an error occurred while importing the native module.
416 : : */
417 : 8402 : static bool import_native_module_sync(JSContext* cx, unsigned argc,
418 : : JS::Value* vp) {
419 : 8402 : JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
420 : 8402 : JS::UniqueChars id;
421 [ - + ]: 8402 : if (!gjs_parse_call_args(cx, "importSync", args, "s", "identifier", &id))
422 : 0 : return false;
423 : :
424 : 8402 : Gjs::AutoMainRealm ar{cx};
425 : 8402 : JS::RootedObject global{cx, JS::CurrentGlobalOrNull(cx)};
426 : :
427 : 8402 : JS::AutoSaveExceptionState exc_state(cx);
428 : :
429 : 8402 : JS::RootedObject native_registry(cx, gjs_get_native_registry(global));
430 : 8402 : JS::RootedObject v_module(cx);
431 : :
432 : 8402 : JS::RootedId key(cx, gjs_intern_string_to_id(cx, id.get()));
433 [ - + ]: 8402 : if (!gjs_global_registry_get(cx, native_registry, key, &v_module))
434 : 0 : return false;
435 : :
436 [ + + ]: 8402 : if (v_module) {
437 : 8304 : args.rval().setObject(*v_module);
438 : 8304 : return true;
439 : : }
440 : :
441 : 98 : JS::RootedObject native_obj(cx);
442 [ - + ]: 98 : if (!Gjs::NativeModuleDefineFuncs::get().define(cx, id.get(),
443 : : &native_obj)) {
444 : 0 : gjs_throw(cx, "Failed to load native module: %s", id.get());
445 : 0 : return false;
446 : : }
447 : :
448 [ - + ]: 98 : if (!gjs_global_registry_set(cx, native_registry, key, native_obj))
449 : 0 : return false;
450 : :
451 : 98 : args.rval().setObject(*native_obj);
452 : 98 : return true;
453 : 8402 : }
454 : :
455 : : /**
456 : : * gjs_populate_module_meta:
457 : : *
458 : : * Hook SpiderMonkey calls to populate the import.meta object.
459 : : * Defines a property "import.meta.url", and additionally a method
460 : : * "import.meta.importSync" if this is an internal module.
461 : : *
462 : : * @param private_ref the private value for the #Module object
463 : : * @param meta the import.meta object
464 : : *
465 : : * @returns whether an error occurred while populating the module meta.
466 : : */
467 : 159 : bool gjs_populate_module_meta(JSContext* cx, JS::HandleValue private_ref,
468 : : JS::HandleObject meta) {
469 : 159 : g_assert(private_ref.isObject());
470 : 159 : JS::RootedObject module(cx, &private_ref.toObject());
471 : :
472 : 159 : gjs_debug(GJS_DEBUG_IMPORTER, "Module metadata hook for module %p",
473 : 159 : &private_ref.toObject());
474 : :
475 : 159 : const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
476 : 159 : JS::RootedValue v_uri(cx);
477 [ + - ]: 318 : if (!JS_GetPropertyById(cx, module, atoms.uri(), &v_uri) ||
478 [ - + - + ]: 318 : !JS_DefinePropertyById(cx, meta, atoms.url(), v_uri,
479 : : GJS_MODULE_PROP_FLAGS))
480 : 0 : return false;
481 : :
482 : 159 : JS::RootedValue v_internal(cx);
483 [ - + ]: 159 : if (!JS_GetPropertyById(cx, module, atoms.internal(), &v_internal))
484 : 0 : return false;
485 [ + + ]: 159 : if (JS::ToBoolean(v_internal)) {
486 : 156 : gjs_debug(GJS_DEBUG_IMPORTER, "Defining meta.importSync for module %p",
487 : 156 : &private_ref.toObject());
488 [ - + ]: 156 : if (!JS_DefineFunctionById(cx, meta, atoms.importSync(),
489 : : import_native_module_sync, 1,
490 : : GJS_MODULE_PROP_FLAGS))
491 : 0 : return false;
492 : : }
493 : :
494 : 159 : return true;
495 : 159 : }
496 : :
497 : : // Canonicalize specifier so that differently-spelled specifiers referring to
498 : : // the same module don't result in duplicate entries in the registry
499 : 3986 : static bool canonicalize_specifier(JSContext* cx,
500 : : JS::MutableHandleString specifier) {
501 : 3986 : JS::UniqueChars specifier_utf8 = JS_EncodeStringToUTF8(cx, specifier);
502 [ - + ]: 3986 : if (!specifier_utf8)
503 : 0 : return false;
504 : :
505 : 3986 : GjsAutoUnref<GFile> file = g_file_new_for_uri(specifier_utf8.get());
506 : 3986 : GjsAutoChar canonical_specifier = g_file_get_uri(file);
507 : 3986 : JS::ConstUTF8CharsZ chars{canonical_specifier, strlen(canonical_specifier)};
508 : 3986 : JS::RootedString new_specifier{cx, JS_NewStringCopyUTF8Z(cx, chars)};
509 [ - + ]: 3986 : if (!new_specifier)
510 : 0 : return false;
511 : :
512 : 3986 : specifier.set(new_specifier);
513 : 3986 : return true;
514 : 3986 : }
515 : :
516 : : /**
517 : : * gjs_module_resolve:
518 : : *
519 : : * Hook SpiderMonkey calls to resolve import specifiers.
520 : : *
521 : : * @param importingModulePriv the private value of the #Module object initiating
522 : : * the import, or a JS null value
523 : : * @param specifier the import specifier to resolve
524 : : *
525 : : * @returns whether an error occurred while resolving the specifier.
526 : : */
527 : 3955 : JSObject* gjs_module_resolve(JSContext* cx, JS::HandleValue importingModulePriv,
528 : : JS::HandleObject module_request) {
529 : 3955 : g_assert((gjs_global_is_type(cx, GjsGlobalType::DEFAULT) ||
530 : : gjs_global_is_type(cx, GjsGlobalType::INTERNAL)) &&
531 : : "gjs_module_resolve can only be called from module-enabled "
532 : : "globals.");
533 : : JS::RootedString specifier(
534 : 3955 : cx, JS::GetModuleRequestSpecifier(cx, module_request));
535 : :
536 : 3955 : JS::RootedObject global(cx, JS::CurrentGlobalOrNull(cx));
537 : : JS::RootedValue v_loader(
538 : 3955 : cx, gjs_get_global_slot(global, GjsGlobalSlot::MODULE_LOADER));
539 : 3955 : g_assert(v_loader.isObject());
540 : 3955 : JS::RootedObject loader(cx, &v_loader.toObject());
541 : :
542 [ - + ]: 3955 : if (!canonicalize_specifier(cx, &specifier))
543 : 0 : return nullptr;
544 : :
545 : 3955 : JS::RootedValueArray<2> args(cx);
546 : 3955 : args[0].set(importingModulePriv);
547 : 3955 : args[1].setString(specifier);
548 : :
549 : 7910 : gjs_debug(GJS_DEBUG_IMPORTER,
550 : : "Module resolve hook for module %s (relative to %s), global %p",
551 : 7910 : gjs_debug_string(specifier).c_str(),
552 : 7910 : gjs_debug_value(importingModulePriv).c_str(), global.get());
553 : :
554 : 3955 : JS::RootedValue result(cx);
555 [ + + ]: 3955 : if (!JS::Call(cx, loader, "moduleResolveHook", args, &result))
556 : 2 : return nullptr;
557 : :
558 : 3953 : g_assert(result.isObject() && "resolve hook failed to return an object!");
559 : 3953 : return &result.toObject();
560 : 3955 : }
561 : :
562 : : // Call JS::FinishDynamicModuleImport() with the values stashed in the function.
563 : : // Can fail in JS::FinishDynamicModuleImport(), but will assert if anything
564 : : // fails in fetching the stashed values, since that would be a serious GJS bug.
565 : : GJS_JSAPI_RETURN_CONVENTION
566 : 31 : static bool finish_import(JSContext* cx, JS::HandleObject evaluation_promise,
567 : : const JS::CallArgs& args) {
568 : 31 : GjsContextPrivate* priv = GjsContextPrivate::from_cx(cx);
569 : 31 : priv->main_loop_release();
570 : :
571 : 31 : JS::Value callback_priv = js::GetFunctionNativeReserved(&args.callee(), 0);
572 : 31 : g_assert(callback_priv.isObject() && "Wrong private value");
573 : 31 : JS::RootedObject callback_data(cx, &callback_priv.toObject());
574 : :
575 : 31 : JS::RootedValue importing_module_priv(cx);
576 : 31 : JS::RootedValue v_module_request(cx);
577 : 31 : JS::RootedValue v_internal_promise(cx);
578 : : bool ok GJS_USED_ASSERT =
579 [ + - ]: 62 : JS_GetProperty(cx, callback_data, "priv", &importing_module_priv) &&
580 [ + - + - ]: 62 : JS_GetProperty(cx, callback_data, "promise", &v_internal_promise) &&
581 : 31 : JS_GetProperty(cx, callback_data, "module_request", &v_module_request);
582 : 31 : g_assert(ok && "Wrong properties on private value");
583 : :
584 : 31 : g_assert(v_module_request.isObject() && "Wrong type for module request");
585 : 31 : g_assert(v_internal_promise.isObject() && "Wrong type for promise");
586 : :
587 : 31 : JS::RootedObject module_request(cx, &v_module_request.toObject());
588 : 31 : JS::RootedObject internal_promise(cx, &v_internal_promise.toObject());
589 : :
590 : 31 : args.rval().setUndefined();
591 : :
592 : 31 : return JS::FinishDynamicModuleImport(cx, evaluation_promise,
593 : : importing_module_priv, module_request,
594 : 31 : internal_promise);
595 : 31 : }
596 : :
597 : : // Failing a JSAPI function may result either in an exception pending on the
598 : : // context, in which case we must call JS::FinishDynamicModuleImport() to reject
599 : : // the internal promise; or in an uncatchable exception such as OOM, in which
600 : : // case we must not call JS::FinishDynamicModuleImport().
601 : : GJS_JSAPI_RETURN_CONVENTION
602 : 0 : static bool fail_import(JSContext* cx, const JS::CallArgs& args) {
603 [ # # ]: 0 : if (JS_IsExceptionPending(cx))
604 : 0 : return finish_import(cx, nullptr, args);
605 : 0 : return false;
606 : : }
607 : :
608 : : GJS_JSAPI_RETURN_CONVENTION
609 : 4 : static bool import_rejected(JSContext* cx, unsigned argc, JS::Value* vp) {
610 : 4 : JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
611 : :
612 : 4 : gjs_debug(GJS_DEBUG_IMPORTER, "Async import promise rejected");
613 : :
614 : : // Throw the value that the promise is rejected with, so that
615 : : // FinishDynamicModuleImport will reject the internal_promise with it.
616 : 4 : JS_SetPendingException(cx, args.get(0),
617 : : JS::ExceptionStackBehavior::DoNotCapture);
618 : :
619 : 4 : return finish_import(cx, nullptr, args);
620 : : }
621 : :
622 : : GJS_JSAPI_RETURN_CONVENTION
623 : 27 : static bool import_resolved(JSContext* cx, unsigned argc, JS::Value* vp) {
624 : 27 : JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
625 : :
626 : 27 : gjs_debug(GJS_DEBUG_IMPORTER, "Async import promise resolved");
627 : :
628 : 27 : Gjs::AutoMainRealm ar{cx};
629 : :
630 : 27 : g_assert(args[0].isObject());
631 : 27 : JS::RootedObject module(cx, &args[0].toObject());
632 : :
633 : 27 : JS::RootedValue evaluation_promise(cx);
634 [ + - ]: 54 : if (!JS::ModuleLink(cx, module) ||
635 [ - + - + ]: 54 : !JS::ModuleEvaluate(cx, module, &evaluation_promise))
636 : 0 : return fail_import(cx, args);
637 : :
638 : 27 : g_assert(evaluation_promise.isObject() &&
639 : : "got weird value from JS::ModuleEvaluate");
640 : : JS::RootedObject evaluation_promise_object(cx,
641 : 27 : &evaluation_promise.toObject());
642 : 27 : return finish_import(cx, evaluation_promise_object, args);
643 : 27 : }
644 : :
645 : 31 : bool gjs_dynamic_module_resolve(JSContext* cx,
646 : : JS::HandleValue importing_module_priv,
647 : : JS::HandleObject module_request,
648 : : JS::HandleObject internal_promise) {
649 : 31 : g_assert(gjs_global_is_type(cx, GjsGlobalType::DEFAULT) &&
650 : : "gjs_dynamic_module_resolve can only be called from the default "
651 : : "global.");
652 : :
653 : 31 : JS::RootedObject global(cx, JS::CurrentGlobalOrNull(cx));
654 : 31 : g_assert(global && "gjs_dynamic_module_resolve must be in a realm");
655 : :
656 : : JS::RootedValue v_loader(
657 : 31 : cx, gjs_get_global_slot(global, GjsGlobalSlot::MODULE_LOADER));
658 : 31 : g_assert(v_loader.isObject());
659 : 31 : JS::RootedObject loader(cx, &v_loader.toObject());
660 : : JS::RootedString specifier(
661 : 31 : cx, JS::GetModuleRequestSpecifier(cx, module_request));
662 : :
663 [ - + ]: 31 : if (!canonicalize_specifier(cx, &specifier))
664 : 0 : return false;
665 : :
666 : 31 : JS::RootedObject callback_data(cx, JS_NewPlainObject(cx));
667 : 31 : if (!callback_data ||
668 [ + - ]: 31 : !JS_DefineProperty(cx, callback_data, "module_request", module_request,
669 : 31 : JSPROP_PERMANENT) ||
670 [ + - ]: 31 : !JS_DefineProperty(cx, callback_data, "promise", internal_promise,
671 [ + - ]: 62 : JSPROP_PERMANENT) ||
672 [ - + - + ]: 62 : !JS_DefineProperty(cx, callback_data, "priv", importing_module_priv,
673 : : JSPROP_PERMANENT))
674 : 0 : return false;
675 : :
676 [ + + ]: 31 : if (importing_module_priv.isObject()) {
677 : 28 : gjs_debug(GJS_DEBUG_IMPORTER,
678 : : "Async module resolve hook for module %s (relative to %p), "
679 : : "global %p",
680 : 56 : gjs_debug_string(specifier).c_str(),
681 : 28 : &importing_module_priv.toObject(), global.get());
682 : : } else {
683 : 3 : gjs_debug(GJS_DEBUG_IMPORTER,
684 : : "Async module resolve hook for module %s (unknown path), "
685 : : "global %p",
686 : 6 : gjs_debug_string(specifier).c_str(), global.get());
687 : : }
688 : :
689 : 31 : JS::RootedValueArray<2> args(cx);
690 : 31 : args[0].set(importing_module_priv);
691 : 31 : args[1].setString(specifier);
692 : :
693 : 31 : JS::RootedValue result(cx);
694 [ - + ]: 31 : if (!JS::Call(cx, loader, "moduleResolveAsyncHook", args, &result))
695 : 0 : return JS::FinishDynamicModuleImport(cx, nullptr, importing_module_priv,
696 : 0 : module_request, internal_promise);
697 : :
698 : : // Release in finish_import
699 : 31 : GjsContextPrivate* priv = GjsContextPrivate::from_cx(cx);
700 : 31 : priv->main_loop_hold();
701 : :
702 : : JS::RootedObject resolved(
703 : 31 : cx, JS_GetFunctionObject(js::NewFunctionWithReserved(
704 : 31 : cx, import_resolved, 1, 0, "async import resolved")));
705 [ - + ]: 31 : if (!resolved)
706 : 0 : return false;
707 : : JS::RootedObject rejected(
708 : 31 : cx, JS_GetFunctionObject(js::NewFunctionWithReserved(
709 : 31 : cx, import_rejected, 1, 0, "async import rejected")));
710 [ - + ]: 31 : if (!rejected)
711 : 0 : return false;
712 : 31 : js::SetFunctionNativeReserved(resolved, 0, JS::ObjectValue(*callback_data));
713 : 31 : js::SetFunctionNativeReserved(rejected, 0, JS::ObjectValue(*callback_data));
714 : :
715 : 31 : JS::RootedObject promise(cx, &result.toObject());
716 : :
717 : : // Calling JS::FinishDynamicModuleImport() at the end of the resolve and
718 : : // reject handlers will also call the module resolve hook. The module will
719 : : // already have been resolved, but that is how SpiderMonkey obtains the
720 : : // module object.
721 : 31 : return JS::AddPromiseReactions(cx, promise, resolved, rejected);
722 : 31 : }
|