/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ // SPDX-License-Identifier: MIT OR LGPL-2.0-or-later // SPDX-FileCopyrightText: 2008 litl, LLC // SPDX-FileCopyrightText: 2009 Red Hat, Inc. // SPDX-FileCopyrightText: 2017 Philip Chimento // SPDX-FileCopyrightText: 2020 Evan Welsh #include #include // for size_t #include #include // for CallArgs, CallArgsFromVp #include // for JS_EncodeStringToUTF8 #include #include #include #include // for JS_DefineDebuggerObject #include // for CurrentGlobalOrNull, JS_NewGlobalObject #include #include #include #include #include // for JSPROP_PERMANENT, JSPROP_RE... #include #include // for GetObjectRealmOrNull, SetRealmPrivate #include #include #include #include #include // for UniqueChars #include // for JS_IdToValue, JS_InitReflectParse #include "gjs/atoms.h" #include "gjs/context-private.h" #include "gjs/engine.h" #include "gjs/global.h" #include "gjs/internal.h" #include "gjs/jsapi-util.h" #include "gjs/macros.h" #include "gjs/native.h" namespace mozilla { union Utf8Unit; } class GjsBaseGlobal { static JSObject* base(JSContext* cx, const JSClass* clasp, JS::RealmCreationOptions options) { // Enable WeakRef without the cleanupSome specification // Re-evaluate if cleanupSome is standardized // See: https://github.com/tc39/proposal-cleanup-some options.setWeakRefsEnabled( JS::WeakRefSpecifier::EnabledWithoutCleanupSome); JS::RealmBehaviors behaviors; JS::RealmOptions compartment_options(options, behaviors); JS::RootedObject global( cx, JS_NewGlobalObject(cx, clasp, nullptr, JS::FireOnNewGlobalHook, compartment_options)); if (!global) return nullptr; JSAutoRealm ac(cx, global); if (!JS_InitReflectParse(cx, global) || !JS_DefineDebuggerObject(cx, global)) return nullptr; return global; } protected: [[nodiscard]] static JSObject* create( JSContext* cx, const JSClass* clasp, JS::RealmCreationOptions options = JS::RealmCreationOptions()) { options.setNewCompartmentAndZone(); return base(cx, clasp, options); } [[nodiscard]] static JSObject* create_with_compartment( JSContext* cx, JS::HandleObject existing, const JSClass* clasp, JS::RealmCreationOptions options = JS::RealmCreationOptions()) { options.setExistingCompartment(existing); return base(cx, clasp, options); } GJS_JSAPI_RETURN_CONVENTION static bool run_bootstrap(JSContext* cx, const char* bootstrap_script, JS::HandleObject global) { GjsAutoChar uri = g_strdup_printf( "resource:///org/gnome/gjs/modules/script/_bootstrap/%s.js", bootstrap_script); JSAutoRealm ar(cx, global); JS::CompileOptions options(cx); options.setFileAndLine(uri, 1).setSourceIsLazy(true); char* script; size_t script_len; if (!gjs_load_internal_source(cx, uri, &script, &script_len)) return false; JS::SourceText source; if (!source.init(cx, script, script_len, JS::SourceOwnership::TakeOwnership)) return false; JS::RootedValue ignored(cx); return JS::Evaluate(cx, options, source, &ignored); } GJS_JSAPI_RETURN_CONVENTION static bool load_native_module(JSContext* m_cx, unsigned argc, JS::Value* vp) { JS::CallArgs argv = JS::CallArgsFromVp(argc, vp); // This function should never be directly exposed to user code, so we // can be strict. g_assert(argc == 1); g_assert(argv[0].isString()); JS::RootedString str(m_cx, argv[0].toString()); JS::UniqueChars id(JS_EncodeStringToUTF8(m_cx, str)); if (!id) return false; JS::RootedObject native_obj(m_cx); if (!Gjs::NativeModuleRegistry::get().load(m_cx, id.get(), &native_obj)) { gjs_throw(m_cx, "Failed to load native module: %s", id.get()); return false; } argv.rval().setObject(*native_obj); return true; } }; const JSClassOps defaultclassops = JS::DefaultGlobalClassOps; class GjsGlobal : GjsBaseGlobal { static constexpr JSClass klass = { // Jasmine depends on the class name "GjsGlobal" to detect GJS' global // object. "GjsGlobal", JSCLASS_GLOBAL_FLAGS_WITH_SLOTS( static_cast(GjsGlobalSlot::LAST)), &defaultclassops, }; // clang-format off static constexpr JSPropertySpec static_props[] = { JS_STRING_SYM_PS(toStringTag, "GjsGlobal", JSPROP_READONLY), JS_PS_END}; // clang-format on static constexpr JSFunctionSpec static_funcs[] = { JS_FS_END}; public: [[nodiscard]] static JSObject* create(JSContext* cx) { return GjsBaseGlobal::create(cx, &klass); } [[nodiscard]] static JSObject* create_with_compartment( JSContext* cx, JS::HandleObject cmp_global) { return GjsBaseGlobal::create_with_compartment(cx, cmp_global, &klass); } GJS_JSAPI_RETURN_CONVENTION static bool define_properties(JSContext* cx, JS::HandleObject global, const char* realm_name, const char* bootstrap_script) { const GjsAtoms& atoms = GjsContextPrivate::atoms(cx); if (!JS_DefinePropertyById(cx, global, atoms.window(), global, JSPROP_READONLY | JSPROP_PERMANENT) || !JS_DefineFunctions(cx, global, GjsGlobal::static_funcs) || !JS_DefineProperties(cx, global, GjsGlobal::static_props)) return false; JS::Realm* realm = JS::GetObjectRealmOrNull(global); g_assert(realm && "Global object must be associated with a realm"); // const_cast is allowed here if we never free the realm data JS::SetRealmPrivate(realm, const_cast(realm_name)); JS::RootedObject native_registry(cx, JS::NewMapObject(cx)); if (!native_registry) return false; gjs_set_global_slot(global, GjsGlobalSlot::NATIVE_REGISTRY, JS::ObjectValue(*native_registry)); JS::RootedObject module_registry(cx, JS::NewMapObject(cx)); if (!module_registry) return false; gjs_set_global_slot(global, GjsGlobalSlot::MODULE_REGISTRY, JS::ObjectValue(*module_registry)); JS::Value v_importer = gjs_get_global_slot(global, GjsGlobalSlot::IMPORTS); g_assert(((void) "importer should be defined before passing null " "importer to GjsGlobal::define_properties", v_importer.isObject())); JS::RootedObject root_importer(cx, &v_importer.toObject()); // Wrapping is a no-op if the importer is already in the same realm. if (!JS_WrapObject(cx, &root_importer) || !JS_DefinePropertyById(cx, global, atoms.imports(), root_importer, GJS_MODULE_PROP_FLAGS)) return false; if (bootstrap_script) { if (!run_bootstrap(cx, bootstrap_script, global)) return false; } return true; } }; class GjsDebuggerGlobal : GjsBaseGlobal { static constexpr JSClass klass = { "GjsDebuggerGlobal", JSCLASS_GLOBAL_FLAGS_WITH_SLOTS( static_cast(GjsDebuggerGlobalSlot::LAST)), &defaultclassops, }; static constexpr JSFunctionSpec static_funcs[] = { JS_FN("loadNative", &load_native_module, 1, 0), JS_FS_END}; public: [[nodiscard]] static JSObject* create(JSContext* cx) { JS::RealmCreationOptions options; options.setToSourceEnabled(true); // debugger uses uneval() return GjsBaseGlobal::create(cx, &klass, options); } [[nodiscard]] static JSObject* create_with_compartment( JSContext* cx, JS::HandleObject cmp_global) { return GjsBaseGlobal::create_with_compartment(cx, cmp_global, &klass); } static bool define_properties(JSContext* cx, JS::HandleObject global, const char* realm_name, const char* bootstrap_script) { const GjsAtoms& atoms = GjsContextPrivate::atoms(cx); if (!JS_DefinePropertyById(cx, global, atoms.window(), global, JSPROP_READONLY | JSPROP_PERMANENT) || !JS_DefineFunctions(cx, global, GjsDebuggerGlobal::static_funcs)) return false; JS::Realm* realm = JS::GetObjectRealmOrNull(global); g_assert(realm && "Global object must be associated with a realm"); // const_cast is allowed here if we never free the realm data JS::SetRealmPrivate(realm, const_cast(realm_name)); if (bootstrap_script) { if (!run_bootstrap(cx, bootstrap_script, global)) return false; } return true; } }; class GjsInternalGlobal : GjsBaseGlobal { static constexpr JSFunctionSpec static_funcs[] = { JS_FN("compileModule", gjs_internal_compile_module, 2, 0), JS_FN("compileInternalModule", gjs_internal_compile_internal_module, 2, 0), JS_FN("getRegistry", gjs_internal_get_registry, 1, 0), JS_FN("loadResourceOrFile", gjs_internal_load_resource_or_file, 1, 0), JS_FN("loadResourceOrFileAsync", gjs_internal_load_resource_or_file_async, 1, 0), JS_FN("parseURI", gjs_internal_parse_uri, 1, 0), JS_FN("resolveRelativeResourceOrFile", gjs_internal_resolve_relative_resource_or_file, 2, 0), JS_FN("setGlobalModuleLoader", gjs_internal_set_global_module_loader, 2, 0), JS_FN("setModulePrivate", gjs_internal_set_module_private, 2, 0), JS_FN("uriExists", gjs_internal_uri_exists, 1, 0), JS_FS_END}; static constexpr JSClass klass = { "GjsInternalGlobal", JSCLASS_GLOBAL_FLAGS_WITH_SLOTS( static_cast(GjsInternalGlobalSlot::LAST)), &defaultclassops, }; public: [[nodiscard]] static JSObject* create(JSContext* cx) { return GjsBaseGlobal::create(cx, &klass); } [[nodiscard]] static JSObject* create_with_compartment( JSContext* cx, JS::HandleObject cmp_global) { return GjsBaseGlobal::create_with_compartment(cx, cmp_global, &klass); } static bool define_properties(JSContext* cx, JS::HandleObject global, const char* realm_name, const char* bootstrap_script [[maybe_unused]]) { JS::Realm* realm = JS::GetObjectRealmOrNull(global); g_assert(realm && "Global object must be associated with a realm"); // const_cast is allowed here if we never free the realm data JS::SetRealmPrivate(realm, const_cast(realm_name)); JSAutoRealm ar(cx, global); JS::RootedObject native_registry(cx, JS::NewMapObject(cx)); if (!native_registry) return false; gjs_set_global_slot(global, GjsGlobalSlot::NATIVE_REGISTRY, JS::ObjectValue(*native_registry)); JS::RootedObject module_registry(cx, JS::NewMapObject(cx)); if (!module_registry) return false; gjs_set_global_slot(global, GjsGlobalSlot::MODULE_REGISTRY, JS::ObjectValue(*module_registry)); return JS_DefineFunctions(cx, global, static_funcs); } }; /** * gjs_create_global_object: * @cx: a #JSContext * * Creates a global object, and initializes it with the default API. * * Returns: the created global object on success, nullptr otherwise, in which * case an exception is pending on @cx */ JSObject* gjs_create_global_object(JSContext* cx, GjsGlobalType global_type, JS::HandleObject current_global) { if (current_global) { switch (global_type) { case GjsGlobalType::DEFAULT: return GjsGlobal::create_with_compartment(cx, current_global); case GjsGlobalType::DEBUGGER: return GjsDebuggerGlobal::create_with_compartment( cx, current_global); case GjsGlobalType::INTERNAL: return GjsInternalGlobal::create_with_compartment( cx, current_global); default: return nullptr; } } switch (global_type) { case GjsGlobalType::DEFAULT: return GjsGlobal::create(cx); case GjsGlobalType::DEBUGGER: return GjsDebuggerGlobal::create(cx); case GjsGlobalType::INTERNAL: return GjsInternalGlobal::create(cx); default: return nullptr; } } /** * gjs_global_is_type: * * @param cx the current #JSContext * @param type the global type to test for * * @returns whether the current global is the same type as #type */ bool gjs_global_is_type(JSContext* cx, GjsGlobalType type) { JSObject* global = JS::CurrentGlobalOrNull(cx); g_assert(global && "gjs_global_is_type called before a realm was entered."); JS::Value global_type = gjs_get_global_slot(global, GjsBaseGlobalSlot::GLOBAL_TYPE); g_assert(global_type.isInt32()); return static_cast(global_type.toInt32()) == type; } GjsGlobalType gjs_global_get_type(JSContext* cx) { auto global = JS::CurrentGlobalOrNull(cx); g_assert(global && "gjs_global_get_type called before a realm was entered."); JS::Value global_type = gjs_get_global_slot(global, GjsBaseGlobalSlot::GLOBAL_TYPE); g_assert(global_type.isInt32()); return static_cast(global_type.toInt32()); } GjsGlobalType gjs_global_get_type(JSObject* global) { JS::Value global_type = gjs_get_global_slot(global, GjsBaseGlobalSlot::GLOBAL_TYPE); g_assert(global_type.isInt32()); return static_cast(global_type.toInt32()); } /** * gjs_global_registry_set: * * @brief This function inserts a module object into a global registry. * Global registries are JS Map objects for easy reuse and access * within internal JS. This function will assert if a module has * already been inserted at the given key. * @param cx the current #JSContext * @param registry a JS Map object * @param key a module identifier, typically a string or symbol * @param module a module object */ bool gjs_global_registry_set(JSContext* cx, JS::HandleObject registry, JS::PropertyKey key, JS::HandleObject module) { JS::RootedValue v_key(cx); if (!JS_IdToValue(cx, key, &v_key)) return false; bool has_key; if (!JS::MapHas(cx, registry, v_key, &has_key)) return false; g_assert(!has_key && "Module key already exists in the registry"); JS::RootedValue v_value(cx, JS::ObjectValue(*module)); return JS::MapSet(cx, registry, v_key, v_value); } /** * gjs_global_registry_get: * * @brief This function inserts a module object into a global registry. * Global registries are JS Map objects for easy reuse and access * within internal JS. This function will assert if a module has * already been inserted at the given key. * @param cx the current #JSContext * @param registry a JS Map object * @param key a module identifier, typically a string or symbol * @param module a module object */ bool gjs_global_registry_get(JSContext* cx, JS::HandleObject registry, JS::PropertyKey key, JS::MutableHandleObject module_out) { JS::RootedValue v_key(cx), v_value(cx); if (!JS_IdToValue(cx, key, &v_key) || !JS::MapGet(cx, registry, v_key, &v_value)) return false; g_assert((v_value.isUndefined() || v_value.isObject()) && "Invalid value in module registry"); if (v_value.isObject()) { module_out.set(&v_value.toObject()); return true; } module_out.set(nullptr); return true; } /** * gjs_define_global_properties: * @cx: a #JSContext * @global: a JS global object that has not yet been passed to this function * @realm_name: (nullable): name of the realm, for debug output * @bootstrap_script: (nullable): name of a bootstrap script (found at * resource://org/gnome/gjs/modules/script/_bootstrap/@bootstrap_script) or * %NULL for none * * Defines properties on the global object such as 'window' and 'imports', and * runs a bootstrap JS script on the global object to define any properties * that can be defined from JS. * This function completes the initialization of a new global object, but it * is separate from gjs_create_global_object() because all globals share the * same root importer. * The code creating the main global for the JS context needs to create the * root importer in between calling gjs_create_global_object() and * gjs_define_global_properties(). * * The caller of this function should be in the realm for @global. * If the root importer object belongs to a different realm, this function will * create a wrapper for it. * * Returns: true on success, false otherwise, in which case an exception is * pending on @cx */ bool gjs_define_global_properties(JSContext* cx, JS::HandleObject global, GjsGlobalType global_type, const char* realm_name, const char* bootstrap_script) { gjs_set_global_slot(global.get(), GjsBaseGlobalSlot::GLOBAL_TYPE, JS::Int32Value(static_cast(global_type))); switch (global_type) { case GjsGlobalType::DEFAULT: return GjsGlobal::define_properties(cx, global, realm_name, bootstrap_script); case GjsGlobalType::DEBUGGER: return GjsDebuggerGlobal::define_properties(cx, global, realm_name, bootstrap_script); case GjsGlobalType::INTERNAL: return GjsInternalGlobal::define_properties(cx, global, realm_name, bootstrap_script); } // Global type does not handle define_properties g_assert_not_reached(); } void detail::set_global_slot(JSObject* global, uint32_t slot, JS::Value value) { JS::SetReservedSlot(global, JSCLASS_GLOBAL_SLOT_COUNT + slot, value); } JS::Value detail::get_global_slot(JSObject* global, uint32_t slot) { return JS::GetReservedSlot(global, JSCLASS_GLOBAL_SLOT_COUNT + slot); } decltype(GjsGlobal::klass) constexpr GjsGlobal::klass; decltype(GjsGlobal::static_funcs) constexpr GjsGlobal::static_funcs; decltype(GjsGlobal::static_props) constexpr GjsGlobal::static_props; decltype(GjsDebuggerGlobal::klass) constexpr GjsDebuggerGlobal::klass; decltype( GjsDebuggerGlobal::static_funcs) constexpr GjsDebuggerGlobal::static_funcs; decltype(GjsInternalGlobal::klass) constexpr GjsInternalGlobal::klass; decltype( GjsInternalGlobal::static_funcs) constexpr GjsInternalGlobal::static_funcs;