Bug 1456035: Part 1 - Add helper to generate native QueryInterface callbacks. r?bz draft
authorKris Maglione <maglione.k@gmail.com>
Sun, 22 Apr 2018 20:32:11 -0700
changeset 786391 5ac61de74016b7de90a3b83534b53d2d2315523b
parent 786386 78216eca6b530cb1dcb23931f3632a77c7795a2c
child 786392 53d1951499c93c9afd416ad827b62603752c7f62
push id107444
push usermaglione.k@gmail.com
push dateMon, 23 Apr 2018 04:22:11 +0000
reviewersbz
bugs1456035
milestone61.0a1
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,140 @@
+/* -*- 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 = 16;
+
+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() || !xpc::IsReflector(&val.toObject())) {
+      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<JSObject*> aResult,
+                              ErrorResult& aRv) const
+{
+  if (!QueriesTo(*aIID->GetID())) {
+    aRv.Throw(NS_ERROR_NO_INTERFACE);
+  } else if (!thisv.isObject()) {
+    aRv.Throw(NS_ERROR_UNEXPECTED);
+  } else {
+    aResult.set(&thisv.toObject());
+  }
+}
+
+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,39 @@
+/* -*- 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
+
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/ChromeUtilsBinding.h"
+#include "mozilla/ErrorResult.h"
+
+namespace mozilla {
+namespace dom {
+
+class MozQueryInterface final : public NonRefcountedDOMObject
+{
+public:
+  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<JSObject*> 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
@@ -607,16 +607,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,14 +1,20 @@
 /* -*- 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/.
  */
 
+[ChromeOnly, Exposed=(Window,System,Worker)]
+interface MozQueryInterface {
+  [Throws]
+  legacycaller object (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 {
   /**
@@ -194,16 +200,24 @@ 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.
+   */
+  [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) {
+    return ({
+      QueryInterface: ChromeUtils.generateQI(interfaces),
+    }).QueryInterface(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);
+
+  // Test failure scenarios.
+  Assert.throws(() => ChromeUtils.generateQI(["toString"]),
+                e => e.result == Cr.NS_ERROR_INVALID_ARG);
+
+  // No `this` object.
+  Assert.throws(() => ChromeUtils.generateQI([])(Ci.nsISupports),
+                e => e.result == Cr.NS_ERROR_UNEXPECTED);
+
+  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