Bug 1457988 - Implement XPCOMUtils.defineLazyProxy. r=kmag
authorFelipe Gomes <felipc@gmail.com>
Tue, 15 May 2018 19:49:17 -0300
changeset 418453 62451c9687b29739b90830ce876c21f158298247
parent 418452 d813fae62728c6edf2dbcf7ec4e7c0c3e888a6f9
child 418454 84ec88aaa796b4b86bede156bf7f43fe82870955
push id34001
push userebalazs@mozilla.com
push dateWed, 16 May 2018 10:01:23 +0000
treeherdermozilla-central@3c9d69736f4a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskmag
bugs1457988
milestone62.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1457988 - Implement XPCOMUtils.defineLazyProxy. r=kmag This implements a new kind of lazy getter in XPCOMUtils that creates an object (implemented as a JS Proxy) that is resilient to be passed around as references to other functions, and will only evaluate the getter when it really needs to be used
js/xpconnect/loader/XPCOMUtils.jsm
js/xpconnect/tests/unit/test_lazyproxy.js
js/xpconnect/tests/unit/xpcshell.ini
--- a/js/xpconnect/loader/XPCOMUtils.jsm
+++ b/js/xpconnect/loader/XPCOMUtils.jsm
@@ -552,18 +552,198 @@ var XPCOMUtils = {
    */
   defineConstant: function XPCOMUtils__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: function XPCOMUtils__defineLazyProxy(aObject, aName, aInitFuncOrResource,
+                                                        aStubProperties, aUntrapCallback) {
+    let initFunc = aInitFuncOrResource;
+
+    if (typeof(aInitFuncOrResource) == "string") {
+      initFunc = function () {
+        let tmp = {};
+        ChromeUtils.import(aInitFuncOrResource, tmp);
+        return tmp[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;
+  },
 };
 
+/**
+ * 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([Ci.nsIObserver, Ci.nsISupportsWeakReference]);
 
 ChromeUtils.defineModuleGetter(this, "Services",
                                "resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(XPCOMUtils, "categoryManager",
                                    "@mozilla.org/categorymanager;1",
                                    "nsICategoryManager");
new file mode 100644
--- /dev/null
+++ b/js/xpconnect/tests/unit/test_lazyproxy.js
@@ -0,0 +1,113 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests the method defineLazyProxy from XPCOMUtils.jsm.
+ */
+
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+add_task(function test_lazy_proxy() {
+  let tmp = {};
+  let realObject = {
+    "prop1": "value1",
+    "prop2": "value2",
+  };
+
+  let evaluated = false;
+  let untrapCalled = false;
+
+  let lazyProxy = XPCOMUtils.defineLazyProxy(
+    tmp,
+    "myLazyProxy",
+
+    // Initiliazer function
+    function init() {
+      evaluated = true;
+      return realObject;
+    },
+
+    // Stub properties
+    {
+      "prop1": "stub"
+    },
+
+    // Untrap callback
+    function untrapCallback(obj) {
+      Assert.equal(obj, realObject, "The underlying object can be obtained in the untrap callback");
+      untrapCalled = true;
+    }
+  );
+
+  // Check that the proxy returned and the one
+  // defined in tmp are the same.
+  //
+  // Note: Assert.strictEqual can't be used here
+  // because it wants to stringify the two objects
+  // compared, which defeats the lazy proxy.
+  Assert.ok(lazyProxy === tmp.myLazyProxy, "Return value and object defined are the same");
+
+  Assert.ok(Cu.isProxy(lazyProxy), "Returned value is in fact a proxy");
+
+  // Check that just using the proxy above didn't
+  // trigger the lazy getter evaluation.
+  Assert.ok(!evaluated, "The lazy proxy hasn't been evaluated yet");
+  Assert.ok(!untrapCalled, "The untrap callback hasn't been called yet");
+
+  // Accessing a stubbed property returns the stub
+  // value and doesn't trigger evaluation.
+  Assert.equal(lazyProxy.prop1, "stub", "Accessing a stubbed property returns the stubbed value");
+
+  Assert.ok(!evaluated, "The access to the stubbed property above didn't evaluate the lazy proxy");
+  Assert.ok(!untrapCalled, "The untrap callback hasn't been called yet");
+
+  // Now the access to another property will trigger
+  // the evaluation, as expected.
+  Assert.equal(lazyProxy.prop2, "value2", "Property access is correctly forwarded to the underlying object");
+
+  Assert.ok(evaluated, "Accessing a non-stubbed property triggered the proxy evaluation");
+  Assert.ok(untrapCalled, "The untrap callback was called");
+
+  // The value of prop1 is now the real value and not the stub value.
+  Assert.equal(lazyProxy.prop1, "value1", "The  value of prop1 is now the real value and not the stub one");
+});
+
+add_task(function test_module_version() {
+  // Test that passing a string instead of an initialization function
+  // makes this behave like a lazy module getter.
+  const NET_UTIL_URI = "resource://gre/modules/NetUtil.jsm";
+  let underlyingObject;
+
+  Cu.unload(NET_UTIL_URI);
+
+  let lazyProxy = XPCOMUtils.defineLazyProxy(
+    null,
+    "NetUtil",
+    NET_UTIL_URI,
+    null, /* no stubs */
+    function untrapCallback(object) {
+      underlyingObject = object;
+    }
+  );
+
+  Assert.ok(!Cu.isModuleLoaded(NET_UTIL_URI), "The NetUtil module was not loaded by the lazy proxy definition");
+
+  // Access the object, which will evaluate the proxy.
+  lazyProxy.foo = "bar";
+
+  // Module was loaded.
+  Assert.ok(Cu.isModuleLoaded(NET_UTIL_URI), "The NetUtil module was loaded");
+
+  let { NetUtil } = ChromeUtils.import(NET_UTIL_URI, {});
+
+  // Avoids a gigantic stringification in the logs.
+  Assert.ok(NetUtil === underlyingObject, "The module loaded is the same as the one directly obtained by ChromeUtils.import");
+
+  // Proxy correctly passed the setter to the underlying object.
+  Assert.equal(NetUtil.foo, "bar", "Proxy correctly passed the setter to the underlying object");
+
+  delete lazyProxy.foo;
+
+  // Proxy correctly passed the delete operation to the underlying object.
+  Assert.ok(!NetUtil.hasOwnProperty("foo"), "Proxy correctly passed the delete operation to the underlying object");
+});
--- a/js/xpconnect/tests/unit/xpcshell.ini
+++ b/js/xpconnect/tests/unit/xpcshell.ini
@@ -88,16 +88,17 @@ head = head_ongc.js
 head = head_ongc.js
 [test_onGarbageCollection-05.js]
 head = head_ongc.js
 [test_reflect_parse.js]
 [test_localeCompare.js]
 [test_recursive_import.js]
 [test_xpcomutils.js]
 [test_unload.js]
+[test_lazyproxy.js]
 [test_attributes.js]
 [test_params.js]
 [test_tearoffs.js]
 [test_want_components.js]
 [test_components.js]
 [test_allowedDomains.js]
 [test_allowedDomainsXHR.js]
 [test_nuke_sandbox.js]