Bug 1456035: Part 1 - Add helper to generate native QueryInterface callbacks. r=bz
authorKris Maglione <maglione.k@gmail.com>
Sun, 22 Apr 2018 20:32:11 -0700
changeset 471764 a442c0b9dbbe4266c14928aad8e04b9b08e0429b
parent 471763 c6fea87acb491d99fdae69335dbb902c9ba6684a
child 471765 0102a61e38ae9c52d35b8239ccb24f48718a2b5b
push id1728
push userjlund@mozilla.com
push dateMon, 18 Jun 2018 21:12:27 +0000
treeherdermozilla-release@c296fde26f5f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbz
bugs1456035
milestone61.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 1456035: Part 1 - Add helper to generate native QueryInterface callbacks. r=bz MozReview-Commit-ID: JpV6zYOdvHu
dom/base/ChromeUtils.h
dom/base/MozQueryInterface.cpp
dom/base/MozQueryInterface.h
dom/base/moz.build
dom/bindings/Bindings.conf
dom/chrome-webidl/ChromeUtils.webidl
js/xpconnect/tests/unit/test_generateQI.js
js/xpconnect/tests/unit/xpcshell.ini
--- a/dom/base/ChromeUtils.h
+++ b/dom/base/ChromeUtils.h
@@ -18,16 +18,17 @@ namespace devtools {
 class HeapSnapshot;
 } // namespace devtools
 
 namespace dom {
 
 class ArrayBufferViewOrArrayBuffer;
 class IdleRequestCallback;
 struct IdleRequestOptions;
+class MozQueryInterface;
 class PrecompiledScript;
 class Promise;
 
 class ChromeUtils
 {
 private:
   // Implemented in devtools/shared/heapsnapshot/HeapSnapshot.cpp
   static void SaveHeapSnapshotShared(GlobalObject& global,
@@ -118,16 +119,20 @@ public:
 
   // Implemented in js/xpconnect/loader/ChromeScriptLoader.cpp
   static already_AddRefed<Promise>
   CompileScript(GlobalObject& aGlobal,
                 const nsAString& aUrl,
                 const dom::CompileScriptOptionsDictionary& aOptions,
                 ErrorResult& aRv);
 
+  static MozQueryInterface*
+  GenerateQI(const GlobalObject& global, const Sequence<OwningStringOrIID>& interfaces,
+             ErrorResult& aRv);
+
   static void WaiveXrays(GlobalObject& aGlobal,
                          JS::HandleValue aVal,
                          JS::MutableHandleValue aRetval,
                          ErrorResult& aRv);
 
   static void UnwaiveXrays(GlobalObject& aGlobal,
                            JS::HandleValue aVal,
                            JS::MutableHandleValue aRetval,
new file mode 100644
--- /dev/null
+++ b/dom/base/MozQueryInterface.cpp
@@ -0,0 +1,141 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "ChromeUtils.h"
+#include "MozQueryInterface.h"
+
+#include <string.h>
+
+#include "jsapi.h"
+
+#include "xpcpublic.h"
+#include "xpcjsid.h"
+
+namespace mozilla {
+namespace dom {
+
+constexpr size_t IID_SIZE = sizeof(nsIID);
+
+static_assert(IID_SIZE == 16,
+              "Size of nsID struct changed. Please ensure this code is still valid.");
+
+static int
+CompareIIDs(const nsIID& aA, const nsIID &aB)
+{
+  return memcmp((void*)&aA.m0, (void*)&aB.m0, IID_SIZE);
+}
+
+struct IIDComparator
+{
+  bool
+  LessThan(const nsIID& aA, const nsIID &aB) const
+  {
+    return CompareIIDs(aA, aB) < 0;
+  }
+
+  bool
+  Equals(const nsIID& aA, const nsIID &aB) const
+  {
+    return aA.Equals(aB);
+  }
+};
+
+/* static */
+MozQueryInterface*
+ChromeUtils::GenerateQI(const GlobalObject& aGlobal, const Sequence<OwningStringOrIID>& aInterfaces, ErrorResult& aRv)
+{
+  JSContext* cx = aGlobal.Context();
+  JS::RootedObject xpcIfaces(cx);
+
+  nsTArray<nsIID> ifaces;
+
+  JS::RootedValue val(cx);
+  for (auto& iface : aInterfaces) {
+    if (iface.IsIID()) {
+      ifaces.AppendElement(*iface.GetAsIID()->GetID());
+      continue;
+    }
+
+    // If we have a string value, we need to look up the interface name. The
+    // simplest and most efficient way to do this is to just grab the "Ci"
+    // object from the global scope.
+    if (!xpcIfaces) {
+      JS::RootedObject global(cx, aGlobal.Get());
+      if (!JS_GetProperty(cx, global, "Ci", &val)) {
+        aRv.NoteJSContextException(cx);
+        return nullptr;
+      }
+      if (!val.isObject()) {
+        aRv.Throw(NS_ERROR_UNEXPECTED);
+        return nullptr;
+      }
+      xpcIfaces = &val.toObject();
+    }
+
+    auto& name = iface.GetAsString();
+    if (!JS_GetUCProperty(cx, xpcIfaces, name.get(), name.Length(), &val)) {
+      aRv.NoteJSContextException(cx);
+      return nullptr;
+    }
+
+    if (val.isNullOrUndefined()) {
+      continue;
+    }
+    if (!val.isObject()) {
+      aRv.Throw(NS_ERROR_INVALID_ARG);
+      return nullptr;
+    }
+
+    nsCOMPtr<nsISupports> base = xpc::UnwrapReflectorToISupports(&val.toObject());
+    nsCOMPtr<nsIJSID> iid = do_QueryInterface(base);
+    if (!iid) {
+      aRv.Throw(NS_ERROR_INVALID_ARG);
+      return nullptr;
+    }
+    ifaces.AppendElement(*iid->GetID());
+  }
+
+  MOZ_ASSERT(!ifaces.Contains(NS_GET_IID(nsISupports), IIDComparator()));
+  ifaces.AppendElement(NS_GET_IID(nsISupports));
+
+  ifaces.Sort(IIDComparator());
+
+  return new MozQueryInterface(Move(ifaces));
+}
+
+bool
+MozQueryInterface::QueriesTo(const nsIID& aIID) const
+{
+  // We use BinarySearchIf here because nsTArray::ContainsSorted requires
+  // twice as many comparisons.
+  size_t result;
+  return BinarySearchIf(mInterfaces, 0, mInterfaces.Length(),
+                        [&] (const nsIID& aOther) { return CompareIIDs(aIID, aOther); },
+                        &result);
+}
+
+void
+MozQueryInterface::LegacyCall(JSContext* cx, JS::Handle<JS::Value> thisv,
+                              nsIJSID* aIID,
+                              JS::MutableHandle<JS::Value> aResult,
+                              ErrorResult& aRv) const
+{
+  if (!QueriesTo(*aIID->GetID())) {
+    aRv.Throw(NS_ERROR_NO_INTERFACE);
+  } else {
+    aResult.set(thisv);
+  }
+}
+
+bool
+MozQueryInterface::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto, JS::MutableHandle<JSObject*> aReflector)
+{
+  return MozQueryInterfaceBinding::Wrap(aCx, this, aGivenProto, aReflector);
+}
+
+} // namespace dom
+} // namespace mozilla
+
new file mode 100644
--- /dev/null
+++ b/dom/base/MozQueryInterface.h
@@ -0,0 +1,51 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_dom_MozQueryInterface
+#define mozilla_dom_MozQueryInterface
+
+/**
+ * This class implements an optimized QueryInterface method for
+ * XPConnect-wrapped JS objects.
+ *
+ * For JavaScript callers, it behaves as an ordinary QueryInterface method,
+ * returning its `this` object or throwing depending on the interface it was
+ * passed.
+ *
+ * For native XPConnect callers, we bypass JSAPI entirely, and directly check
+ * whether the queried interface is in the interfaces list.
+ */
+
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/ChromeUtilsBinding.h"
+#include "mozilla/ErrorResult.h"
+
+namespace mozilla {
+namespace dom {
+
+class MozQueryInterface final : public NonRefcountedDOMObject
+{
+public:
+  explicit MozQueryInterface(nsTArray<nsIID>&& aInterfaces)
+    : mInterfaces(Move(aInterfaces))
+  {}
+
+  bool QueriesTo(const nsIID& aIID) const;
+
+  void LegacyCall(JSContext* cx, JS::Handle<JS::Value> thisv, nsIJSID* aIID, JS::MutableHandle<JS::Value> aResult, ErrorResult& aRv) const;
+
+  nsISupports* GetParentObject() const { return nullptr; }
+
+  bool WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto, JS::MutableHandle<JSObject*> aReflector);
+
+private:
+  nsTArray<nsIID> mInterfaces;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_MozQueryInterface
--- a/dom/base/moz.build
+++ b/dom/base/moz.build
@@ -190,16 +190,17 @@ EXPORTS.mozilla.dom += [
     'ImageEncoder.h',
     'ImageTracker.h',
     'IntlUtils.h',
     'Link.h',
     'Location.h',
     'MessageListenerManager.h',
     'MessageManagerGlobal.h',
     'MessageSender.h',
+    'MozQueryInterface.h',
     'NameSpaceConstants.h',
     'Navigator.h',
     'NodeInfo.h',
     'NodeInfoInlines.h',
     'NodeIterator.h',
     'Pose.h',
     'ProcessGlobal.h',
     'ResponsiveImageSelector.h',
@@ -270,16 +271,17 @@ UNIFIED_SOURCES += [
     'ImageEncoder.cpp',
     'ImageTracker.cpp',
     'IntlUtils.cpp',
     'Link.cpp',
     'Location.cpp',
     'MessageListenerManager.cpp',
     'MessageManagerGlobal.cpp',
     'MessageSender.cpp',
+    'MozQueryInterface.cpp',
     'Navigator.cpp',
     'NodeInfo.cpp',
     'NodeIterator.cpp',
     'nsAttrAndChildArray.cpp',
     'nsAttrValue.cpp',
     'nsAttrValueOrString.cpp',
     'nsCCUncollectableMarker.cpp',
     'nsContentAreaDragDrop.cpp',
--- a/dom/bindings/Bindings.conf
+++ b/dom/bindings/Bindings.conf
@@ -603,16 +603,20 @@ DOMInterfaces = {
     'nativeType': 'mozilla::storage::StatementParams',
 },
 
 'MozStorageStatementRow': {
     'headerFile': 'mozilla/storage/mozStorageStatementRow.h',
     'nativeType': 'mozilla::storage::StatementRow',
 },
 
+'MozQueryInterface': {
+    'wrapperCache': False,
+},
+
 'MutationObserver': {
     'nativeType': 'nsDOMMutationObserver',
 },
 
 'MutationRecord': {
     'nativeType': 'nsDOMMutationRecord',
     'headerFile': 'nsDOMMutationObserver.h',
 },
--- a/dom/chrome-webidl/ChromeUtils.webidl
+++ b/dom/chrome-webidl/ChromeUtils.webidl
@@ -1,15 +1,31 @@
 /* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* 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/.
  */
 
 /**
+ * An optimized QueryInterface method, generated by generateQI.
+ *
+ * For JS callers, this behaves like a normal QueryInterface function. When
+ * called with a supported interface, it returns its `this` object. When
+ * called with an unsupported interface, it throws NS_ERROR_NO_INTERFACE.
+ *
+ * C++ callers use a fast path, and never call the JSAPI or WebIDL methods of
+ * this object.
+ */
+[ChromeOnly, Exposed=(Window,System)]
+interface MozQueryInterface {
+  [Throws]
+  legacycaller any (IID aIID);
+};
+
+/**
  * A collection of static utility methods that are only exposed to system code.
  * This is exposed in all the system globals where we can expose stuff by
  * default, so should only include methods that are **thread-safe**.
  */
 [ChromeOnly, Exposed=(Window,System,Worker)]
 namespace ChromeUtils {
   /**
    * Serialize a snapshot of the heap graph, as seen by |JS::ubi::Node| and
@@ -190,16 +206,33 @@ partial namespace ChromeUtils {
    * which may be used to execute it repeatedly, in different globals, without
    * re-parsing.
    */
   [NewObject]
   Promise<PrecompiledScript>
   compileScript(DOMString url, optional CompileScriptOptionsDictionary options);
 
   /**
+   * Returns an optimized QueryInterface method which, when called from
+   * JavaScript, acts as an ordinary QueryInterface function call, and when
+   * called from XPConnect, circumvents JSAPI entirely.
+   *
+   * The list of interfaces may include a mix of nsIJSID objects and interface
+   * name strings. Strings for nonexistent interface names are silently
+   * ignored, as long as they don't refer to any non-IID property of the Ci
+   * global. Any non-IID value is implicitly coerced to a string, and treated
+   * as an interface name.
+   *
+   * nsISupports is implicitly supported, and must not be included in the
+   * interface list.
+   */
+  [Affects=Nothing, NewObject, Throws]
+  MozQueryInterface generateQI(sequence<(DOMString or IID)> interfaces);
+
+  /**
    * Waive Xray on a given value. Identity op for primitives.
    */
   [Throws]
   any waiveXrays(any val);
 
   /**
    * Strip off Xray waivers on a given value. Identity op for primitives.
    */
new file mode 100644
--- /dev/null
+++ b/js/xpconnect/tests/unit/test_generateQI.js
@@ -0,0 +1,32 @@
+"use strict";
+
+add_task(async function test_generateQI() {
+  function checkQI(interfaces, iface) {
+    let obj = {
+      QueryInterface: ChromeUtils.generateQI(interfaces),
+    };
+    equal(obj.QueryInterface(iface), obj,
+         `Correct return value for query to ${iface}`);
+  }
+
+  // Test success scenarios.
+  checkQI([], Ci.nsISupports);
+
+  checkQI([Ci.nsIPropertyBag, "nsIPropertyBag2"], Ci.nsIPropertyBag);
+  checkQI([Ci.nsIPropertyBag, "nsIPropertyBag2"], Ci.nsIPropertyBag2);
+
+  checkQI([Ci.nsIPropertyBag, "nsIPropertyBag2", "nsINotARealInterface"], Ci.nsIPropertyBag2);
+
+  // Non-IID values get stringified, and don't cause any errors as long
+  // as there isn't a non-IID property with the same name on Ci.
+  checkQI([Ci.nsIPropertyBag, "nsIPropertyBag2", null, Object], Ci.nsIPropertyBag2);
+
+  ChromeUtils.generateQI([])(Ci.nsISupports);
+
+  // Test failure scenarios.
+  Assert.throws(() => ChromeUtils.generateQI(["toString"]),
+                e => e.result == Cr.NS_ERROR_INVALID_ARG);
+
+  Assert.throws(() => checkQI([], Ci.nsIPropertyBag),
+                e => e.result == Cr.NS_ERROR_NO_INTERFACE);
+});
--- a/js/xpconnect/tests/unit/xpcshell.ini
+++ b/js/xpconnect/tests/unit/xpcshell.ini
@@ -68,16 +68,17 @@ support-files =
 [test_compileScript.js]
 [test_deepFreezeClone.js]
 [test_defineModuleGetter.js]
 [test_file.js]
 [test_blob.js]
 [test_blob2.js]
 [test_file2.js]
 [test_getCallerLocation.js]
+[test_generateQI.js]
 [test_import.js]
 [test_import_fail.js]
 [test_isModuleLoaded.js]
 [test_js_weak_references.js]
 [test_onGarbageCollection-01.js]
 head = head_ongc.js
 [test_onGarbageCollection-02.js]
 head = head_ongc.js