diff options
Diffstat (limited to 'js/xpconnect/loader/XPCOMUtils.sys.mjs')
-rw-r--r-- | js/xpconnect/loader/XPCOMUtils.sys.mjs | 580 |
1 files changed, 580 insertions, 0 deletions
diff --git a/js/xpconnect/loader/XPCOMUtils.sys.mjs b/js/xpconnect/loader/XPCOMUtils.sys.mjs new file mode 100644 index 0000000000..403b17e2be --- /dev/null +++ b/js/xpconnect/loader/XPCOMUtils.sys.mjs @@ -0,0 +1,580 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=2 ts=2 sts=2 et filetype=javascript + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; + +let global = Cu.getGlobalForObject({}); + +// Some global imports expose additional symbols; for example, +// `Cu.importGlobalProperties(["MessageChannel"])` imports `MessageChannel` +// and `MessagePort`. This table maps those extra symbols to the main +// import name. +const EXTRA_GLOBAL_NAME_TO_IMPORT_NAME = { + MessagePort: "MessageChannel", +}; + +/** + * Redefines the given property on the given object with the given + * value. This can be used to redefine getter properties which do not + * implement setters. + */ +function redefine(object, prop, value) { + Object.defineProperty(object, prop, { + configurable: true, + enumerable: true, + value, + writable: true, + }); + return value; +} + +export var XPCOMUtils = { + /** + * Defines a getter on a specified object that will be created upon first use. + * + * @param aObject + * The object to define the lazy getter on. + * @param aName + * The name of the getter to define on aObject. + * @param aLambda + * A function that returns what the getter should return. This will + * only ever be called once. + */ + defineLazyGetter(aObject, aName, aLambda) { + ChromeUtils.defineLazyGetter(aObject, aName, aLambda); + }, + + /** + * Defines a getter on a specified object for a script. The script will not + * be loaded until first use. + * + * @param aObject + * The object to define the lazy getter on. + * @param aNames + * The name of the getter to define on aObject for the script. + * This can be a string if the script exports only one symbol, + * or an array of strings if the script can be first accessed + * from several different symbols. + * @param aResource + * The URL used to obtain the script. + */ + defineLazyScriptGetter(aObject, aNames, aResource) { + if (!Array.isArray(aNames)) { + aNames = [aNames]; + } + for (let name of aNames) { + Object.defineProperty(aObject, name, { + get() { + XPCOMUtils._scriptloader.loadSubScript(aResource, aObject); + return aObject[name]; + }, + set(value) { + redefine(aObject, name, value); + }, + configurable: true, + enumerable: true, + }); + } + }, + + /** + * Overrides the scriptloader definition for tests to help with globals + * tracking. Should only be used for tests. + * + * @param {object} aObject + * The alternative script loader object to use. + */ + overrideScriptLoaderForTests(aObject) { + Cu.crashIfNotInAutomation(); + delete this._scriptloader; + this._scriptloader = aObject; + }, + + /** + * Defines a getter property on the given object for each of the given + * global names as accepted by Cu.importGlobalProperties. These + * properties are imported into the shared JSM module global, and then + * copied onto the given object, no matter which global the object + * belongs to. + * + * @param {object} aObject + * The object on which to define the properties. + * @param {string[]} aNames + * The list of global properties to define. + */ + defineLazyGlobalGetters(aObject, aNames) { + for (let name of aNames) { + this.defineLazyGetter(aObject, name, () => { + if (!(name in global)) { + let importName = EXTRA_GLOBAL_NAME_TO_IMPORT_NAME[name] || name; + // eslint-disable-next-line mozilla/reject-importGlobalProperties, no-unused-vars + Cu.importGlobalProperties([importName]); + } + return global[name]; + }); + } + }, + + /** + * Defines a getter on a specified object for a service. The service will not + * be obtained until first use. + * + * @param aObject + * The object to define the lazy getter on. + * @param aName + * The name of the getter to define on aObject for the service. + * @param aContract + * The contract used to obtain the service. + * @param aInterfaceName + * The name of the interface to query the service to. + */ + defineLazyServiceGetter(aObject, aName, aContract, aInterfaceName) { + this.defineLazyGetter(aObject, aName, () => { + if (aInterfaceName) { + return Cc[aContract].getService(Ci[aInterfaceName]); + } + return Cc[aContract].getService().wrappedJSObject; + }); + }, + + /** + * Defines a lazy service getter on a specified object for each + * property in the given object. + * + * @param aObject + * The object to define the lazy getter on. + * @param aServices + * An object with a property for each service to be + * imported, where the property name is the name of the + * symbol to define, and the value is a 1 or 2 element array + * containing the contract ID and, optionally, the interface + * name of the service, as passed to defineLazyServiceGetter. + */ + defineLazyServiceGetters(aObject, aServices) { + for (let [name, service] of Object.entries(aServices)) { + // Note: This is hot code, and cross-compartment array wrappers + // are not JIT-friendly to destructuring or spread operators, so + // we need to use indexed access instead. + this.defineLazyServiceGetter( + aObject, + name, + service[0], + service[1] || null + ); + } + }, + + /** + * Defines a getter on a specified object for a module. The module will not + * be imported until first use. The getter allows to execute setup and + * teardown code (e.g. to register/unregister to services) and accepts + * a proxy object which acts on behalf of the module until it is imported. + * + * @param aObject + * The object to define the lazy getter on. + * @param aName + * The name of the getter to define on aObject for the module. + * @param aResource + * The URL used to obtain the module. + * @param aSymbol + * The name of the symbol exported by the module. + * This parameter is optional and defaults to aName. + * @param aPreLambda + * A function that is executed when the proxy is set up. + * This will only ever be called once. + * @param aPostLambda + * A function that is executed when the module has been imported to + * run optional teardown procedures on the proxy object. + * This will only ever be called once. + * @param aProxy + * An object which acts on behalf of the module to be imported until + * the module has been imported. + */ + defineLazyModuleGetter( + aObject, + aName, + aResource, + aSymbol, + aPreLambda, + aPostLambda, + aProxy + ) { + if (arguments.length == 3) { + ChromeUtils.defineModuleGetter(aObject, aName, aResource); + return; + } + + let proxy = aProxy || {}; + + if (typeof aPreLambda === "function") { + aPreLambda.apply(proxy); + } + + this.defineLazyGetter(aObject, aName, () => { + var temp = {}; + try { + temp = ChromeUtils.import(aResource); + + if (typeof aPostLambda === "function") { + aPostLambda.apply(proxy); + } + } catch (ex) { + console.error("Failed to load module " + aResource + "."); + throw ex; + } + return temp[aSymbol || aName]; + }); + }, + + /** + * Defines a lazy module getter on a specified object for each + * property in the given object. + * + * @param aObject + * The object to define the lazy getter on. + * @param aModules + * An object with a property for each module property to be + * imported, where the property name is the name of the + * imported symbol and the value is the module URI. + */ + defineLazyModuleGetters(aObject, aModules) { + for (let [name, module] of Object.entries(aModules)) { + ChromeUtils.defineModuleGetter(aObject, name, module); + } + }, + + /** + * Defines a getter on a specified object for preference value. The + * preference is read the first time that the property is accessed, + * and is thereafter kept up-to-date using a preference observer. + * + * @param aObject + * The object to define the lazy getter on. + * @param aName + * The name of the getter property to define on aObject. + * @param aPreference + * The name of the preference to read. + * @param aDefaultPrefValue + * The default value to use, if the preference is not defined. + * This is the default value of the pref, before applying aTransform. + * @param aOnUpdate + * A function to call upon update. Receives as arguments + * `(aPreference, previousValue, newValue)` + * @param aTransform + * An optional function to transform the value. If provided, + * this function receives the new preference value as an argument + * and its return value is used by the getter. + */ + defineLazyPreferenceGetter( + aObject, + aName, + aPreference, + aDefaultPrefValue = null, + aOnUpdate = null, + aTransform = val => val + ) { + if (AppConstants.DEBUG && aDefaultPrefValue !== null) { + let prefType = Services.prefs.getPrefType(aPreference); + if (prefType != Ci.nsIPrefBranch.PREF_INVALID) { + // The pref may get defined after the lazy getter is called + // at which point the code here won't know the expected type. + let prefTypeForDefaultValue = { + boolean: Ci.nsIPrefBranch.PREF_BOOL, + number: Ci.nsIPrefBranch.PREF_INT, + string: Ci.nsIPrefBranch.PREF_STRING, + }[typeof aDefaultPrefValue]; + if (prefTypeForDefaultValue != prefType) { + throw new Error( + `Default value does not match preference type (Got ${prefTypeForDefaultValue}, expected ${prefType}) for ${aPreference}` + ); + } + } + } + + // Note: We need to keep a reference to this observer alive as long + // as aObject is alive. This means that all of our getters need to + // explicitly close over the variable that holds the object, and we + // cannot define a value in place of a getter after we read the + // preference. + let observer = { + QueryInterface: XPCU_lazyPreferenceObserverQI, + + value: undefined, + + observe(subject, topic, data) { + if (data == aPreference) { + if (aOnUpdate) { + let previous = this.value; + + // Fetch and cache value. + this.value = undefined; + let latest = lazyGetter(); + aOnUpdate(data, previous, latest); + } else { + // Empty cache, next call to the getter will cause refetch. + this.value = undefined; + } + } + }, + }; + + let defineGetter = get => { + Object.defineProperty(aObject, aName, { + configurable: true, + enumerable: true, + get, + }); + }; + + function lazyGetter() { + if (observer.value === undefined) { + let prefValue; + switch (Services.prefs.getPrefType(aPreference)) { + case Ci.nsIPrefBranch.PREF_STRING: + prefValue = Services.prefs.getStringPref(aPreference); + break; + + case Ci.nsIPrefBranch.PREF_INT: + prefValue = Services.prefs.getIntPref(aPreference); + break; + + case Ci.nsIPrefBranch.PREF_BOOL: + prefValue = Services.prefs.getBoolPref(aPreference); + break; + + case Ci.nsIPrefBranch.PREF_INVALID: + prefValue = aDefaultPrefValue; + break; + + default: + // This should never happen. + throw new Error( + `Error getting pref ${aPreference}; its value's type is ` + + `${Services.prefs.getPrefType(aPreference)}, which I don't ` + + `know how to handle.` + ); + } + + observer.value = aTransform(prefValue); + } + return observer.value; + } + + defineGetter(() => { + Services.prefs.addObserver(aPreference, observer, true); + + defineGetter(lazyGetter); + return lazyGetter(); + }); + }, + + /** + * Defines a non-writable property on an object. + */ + defineConstant(aObj, aName, aValue) { + Object.defineProperty(aObj, aName, { + value: aValue, + enumerable: true, + writable: false, + }); + }, + + /** + * Defines a proxy which acts as a lazy object getter that can be passed + * around as a reference, and will only be evaluated when something in + * that object gets accessed. + * + * The evaluation can be triggered by a function call, by getting or + * setting a property, calling this as a constructor, or enumerating + * the properties of this object (e.g. during an iteration). + * + * Please note that, even after evaluated, the object given to you + * remains being the proxy object (which forwards everything to the + * real object). This is important to correctly use these objects + * in pairs of add+remove listeners, for example. + * If your use case requires access to the direct object, you can + * get it through the untrap callback. + * + * @param aObject + * The object to define the lazy getter on. + * + * You can pass null to aObject if you just want to get this + * proxy through the return value. + * + * @param aName + * The name of the getter to define on aObject. + * + * @param aInitFuncOrResource + * A function or a module that defines what this object actually + * should be when it gets evaluated. This will only ever be called once. + * + * Short-hand: If you pass a string to this parameter, it will be treated + * as the URI of a module to be imported, and aName will be used as + * the symbol to retrieve from the module. + * + * @param aStubProperties + * In this parameter, you can provide an object which contains + * properties from the original object that, when accessed, will still + * prevent the entire object from being evaluated. + * + * These can be copies or simplified versions of the original properties. + * + * One example is to provide an alternative QueryInterface implementation + * to avoid the entire object from being evaluated when it's added as an + * observer (as addObserver calls object.QueryInterface(Ci.nsIObserver)). + * + * Once the object has been evaluated, the properties from the real + * object will be used instead of the ones provided here. + * + * @param aUntrapCallback + * A function that gets called once when the object has just been evaluated. + * You can use this to do some work (e.g. setting properties) that you need + * to do on this object but that can wait until it gets evaluated. + * + * Another use case for this is to use during code development to log when + * this object gets evaluated, to make sure you're not accidentally triggering + * it earlier than expected. + */ + defineLazyProxy( + aObject, + aName, + aInitFuncOrResource, + aStubProperties, + aUntrapCallback + ) { + let initFunc = aInitFuncOrResource; + + if (typeof aInitFuncOrResource == "string") { + initFunc = () => ChromeUtils.import(aInitFuncOrResource)[aName]; + } + + let handler = new LazyProxyHandler( + aName, + initFunc, + aStubProperties, + aUntrapCallback + ); + + /* + * We cannot simply create a lazy getter for the underlying + * object and pass it as the target of the proxy, because + * just passing it in `new Proxy` means it would get + * evaluated. Becase of this, a full handler needs to be + * implemented (the LazyProxyHandler). + * + * So, an empty object is used as the target, and the handler + * replaces it on every call with the real object. + */ + let proxy = new Proxy({}, handler); + + if (aObject) { + Object.defineProperty(aObject, aName, { + value: proxy, + enumerable: true, + writable: true, + }); + } + + return proxy; + }, +}; + +XPCOMUtils.defineLazyGetter(XPCOMUtils, "_scriptloader", () => { + return Services.scriptloader; +}); + +/** + * LazyProxyHandler + * This class implements the handler used + * in the proxy from defineLazyProxy. + * + * This handler forwards all calls to an underlying object, + * stored as `this.realObject`, which is obtained as the returned + * value from aInitFunc, which will be called on the first time + * time that it needs to be used (with an exception in the get() trap + * for the properties provided in the `aStubProperties` parameter). + */ + +class LazyProxyHandler { + constructor(aName, aInitFunc, aStubProperties, aUntrapCallback) { + this.pending = true; + this.name = aName; + this.initFuncOrResource = aInitFunc; + this.stubProperties = aStubProperties; + this.untrapCallback = aUntrapCallback; + } + + getObject() { + if (this.pending) { + this.realObject = this.initFuncOrResource.call(null); + + if (this.untrapCallback) { + this.untrapCallback.call(null, this.realObject); + this.untrapCallback = null; + } + + this.pending = false; + this.stubProperties = null; + } + return this.realObject; + } + + getPrototypeOf(target) { + return Reflect.getPrototypeOf(this.getObject()); + } + + setPrototypeOf(target, prototype) { + return Reflect.setPrototypeOf(this.getObject(), prototype); + } + + isExtensible(target) { + return Reflect.isExtensible(this.getObject()); + } + + preventExtensions(target) { + return Reflect.preventExtensions(this.getObject()); + } + + getOwnPropertyDescriptor(target, prop) { + return Reflect.getOwnPropertyDescriptor(this.getObject(), prop); + } + + defineProperty(target, prop, descriptor) { + return Reflect.defineProperty(this.getObject(), prop, descriptor); + } + + has(target, prop) { + return Reflect.has(this.getObject(), prop); + } + + get(target, prop, receiver) { + if ( + this.pending && + this.stubProperties && + Object.prototype.hasOwnProperty.call(this.stubProperties, prop) + ) { + return this.stubProperties[prop]; + } + return Reflect.get(this.getObject(), prop, receiver); + } + + set(target, prop, value, receiver) { + return Reflect.set(this.getObject(), prop, value, receiver); + } + + deleteProperty(target, prop) { + return Reflect.deleteProperty(this.getObject(), prop); + } + + ownKeys(target) { + return Reflect.ownKeys(this.getObject()); + } +} + +var XPCU_lazyPreferenceObserverQI = ChromeUtils.generateQI([ + "nsIObserver", + "nsISupportsWeakReference", +]); |