Bug 779048 part 1. Implement a parent class for C++ reflections of callback functions in WebIDL. r=bholley,smaug
authorBoris Zbarsky <bzbarsky@mit.edu>
Fri, 09 Nov 2012 07:43:57 -0800
changeset 121471 172241f160f1f4db55b4ea2705be6282e816273a
parent 121470 61d0e897a9f7461ad9f8c16f2eea125c3ae21916
child 121472 c348f36c317ba4e414f6de7d83aeacbf0b9d525d
push id273
push userlsblakk@mozilla.com
push dateThu, 14 Feb 2013 23:19:38 +0000
treeherdermozilla-release@c5e807a3f8b8 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbholley, smaug
bugs779048
milestone19.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 779048 part 1. Implement a parent class for C++ reflections of callback functions in WebIDL. r=bholley,smaug
caps/src/nsScriptSecurityManager.cpp
dom/base/nsJSEnvironment.cpp
dom/base/nsJSEnvironment.h
dom/base/nsJSUtils.cpp
dom/base/nsJSUtils.h
dom/bindings/CallbackFunction.cpp
dom/bindings/CallbackFunction.h
dom/bindings/Makefile.in
--- a/caps/src/nsScriptSecurityManager.cpp
+++ b/caps/src/nsScriptSecurityManager.cpp
@@ -1645,16 +1645,21 @@ nsScriptSecurityManager::CheckFunctionAc
     bool result;
     rv = CanExecuteScripts(aCx, subject, &result);
     if (NS_FAILED(rv))
       return rv;
 
     if (!result)
       return NS_ERROR_DOM_SECURITY_ERR;
 
+    if (!aTargetObj) {
+        // We're done here
+        return NS_OK;
+    }
+
     /*
     ** Get origin of subject and object and compare.
     */
     JSObject* obj = (JSObject*)aTargetObj;
     nsIPrincipal* object = doGetObjectPrincipal(obj);
 
     if (!object)
         return NS_ERROR_FAILURE;        
--- a/dom/base/nsJSEnvironment.cpp
+++ b/dom/base/nsJSEnvironment.cpp
@@ -3610,23 +3610,18 @@ nsJSContext::DropScriptObject(void* aScr
 
   ::JS_UnlockGCThingRT(nsJSRuntime::sRuntime, aScriptObject);
   return NS_OK;
 }
 
 void
 nsJSContext::ReportPendingException()
 {
-  // set aside the frame chain, since it has nothing to do with the
-  // exception we're reporting.
-  if (mIsInitialized && ::JS_IsExceptionPending(mContext)) {
-    bool saved = ::JS_SaveFrameChain(mContext);
-    ::JS_ReportPendingException(mContext);
-    if (saved)
-        ::JS_RestoreFrameChain(mContext);
+  if (mIsInitialized) {
+    nsJSUtils::ReportPendingException(mContext);
   }
 }
 
 /**********************************************************************
  * nsJSRuntime implementation
  *********************************************************************/
 
 // QueryInterface implementation for nsJSRuntime
--- a/dom/base/nsJSEnvironment.h
+++ b/dom/base/nsJSEnvironment.h
@@ -213,18 +213,20 @@ protected:
 private:
   void DestroyJSContext();
 
   nsrefcnt GetCCRefcnt();
 
   JSContext *mContext;
   bool mActive;
 
+  // Public so we can use it from CallbackFunction
+public:
+  struct TerminationFuncHolder;
 protected:
-  struct TerminationFuncHolder;
   friend struct TerminationFuncHolder;
   
   struct TerminationFuncClosure
   {
     TerminationFuncClosure(nsScriptTerminationFunc aFunc,
                            nsISupports* aArg,
                            TerminationFuncClosure* aNext) :
       mTerminationFunc(aFunc),
@@ -237,16 +239,18 @@ protected:
       delete mNext;
     }
     
     nsScriptTerminationFunc mTerminationFunc;
     nsCOMPtr<nsISupports> mTerminationFuncArg;
     TerminationFuncClosure* mNext;
   };
 
+  // Public so we can use it from CallbackFunction
+public:
   struct TerminationFuncHolder
   {
     TerminationFuncHolder(nsJSContext* aContext)
       : mContext(aContext),
         mTerminations(aContext->mTerminations)
     {
       aContext->mTerminations = nullptr;
     }
@@ -264,17 +268,18 @@ protected:
         cur->mNext = mContext->mTerminations;
         mContext->mTerminations = mTerminations;
       }
     }
 
     nsJSContext* mContext;
     TerminationFuncClosure* mTerminations;
   };
-  
+
+protected:
   TerminationFuncClosure* mTerminations;
 
 private:
   bool mIsInitialized;
   bool mScriptsEnabled;
   bool mGCOnDestruction;
 
   uint32_t mExecuteDepth;
--- a/dom/base/nsJSUtils.cpp
+++ b/dom/base/nsJSUtils.cpp
@@ -128,8 +128,19 @@ nsJSUtils::GetCurrentlyRunningCodeInnerW
       if (win)
         innerWindowID = win->WindowID();
     }
   }
 
   return innerWindowID;
 }
 
+void
+nsJSUtils::ReportPendingException(JSContext *aContext)
+{
+  if (JS_IsExceptionPending(aContext)) {
+    bool saved = JS_SaveFrameChain(aContext);
+    JS_ReportPendingException(aContext);
+    if (saved) {
+      JS_RestoreFrameChain(aContext);
+    }
+  }
+}
--- a/dom/base/nsJSUtils.h
+++ b/dom/base/nsJSUtils.h
@@ -42,16 +42,23 @@ public:
    * Retrieve the inner window ID based on the given JSContext.
    *
    * @param JSContext aContext
    *        The JSContext from which you want to find the inner window ID.
    *
    * @returns uint64_t the inner window ID.
    */
   static uint64_t GetCurrentlyRunningCodeInnerWindowID(JSContext *aContext);
+
+  /**
+   * Report a pending exception on aContext, if any.  Note that this
+   * can be called when the context has a JS stack.  If that's the
+   * case, the stack will be set aside before reporting the exception.
+   */
+  static void ReportPendingException(JSContext *aContext);
 };
 
 
 class nsDependentJSString : public nsDependentString
 {
 public:
   /**
    * In the case of string ids, getting the string's chars is infallible, so
new file mode 100644
--- /dev/null
+++ b/dom/bindings/CallbackFunction.cpp
@@ -0,0 +1,156 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 "mozilla/dom/CallbackFunction.h"
+#include "jsfriendapi.h"
+#include "nsIScriptGlobalObject.h"
+#include "nsIXPConnect.h"
+#include "nsIScriptContext.h"
+#include "nsPIDOMWindow.h"
+#include "nsJSUtils.h"
+#include "nsIScriptSecurityManager.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CallbackFunction)
+  NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(CallbackFunction)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(CallbackFunction)
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(CallbackFunction)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(CallbackFunction)
+  tmp->DropCallback();
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(CallbackFunction)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(CallbackFunction)
+  NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mCallable)
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+CallbackFunction::CallSetup::CallSetup(JSObject* const aCallable)
+  : mCx(nullptr)
+{
+  xpc_UnmarkGrayObject(aCallable);
+
+  // We need to produce a useful JSContext here.  Ideally one that the callable
+  // is in some sense associated with, so that we can sort of treat it as a
+  // "script entry point".
+
+  // First, find the real underlying callable.
+  JSObject* realCallable = js::UnwrapObject(aCallable);
+
+  // Now get the nsIScriptGlobalObject for this callable.
+  JSContext* cx = nullptr;
+  nsIScriptContext* ctx = nullptr;
+  nsIScriptGlobalObject* sgo = nsJSUtils::GetStaticScriptGlobal(realCallable);
+  if (sgo) {
+    // Make sure that if this is a window it's the current inner, since the
+    // nsIScriptContext and hence JSContext are associated with the outer
+    // window.  Which means that if someone holds on to a function from a
+    // now-unloaded document we'd have the new document as the script entry
+    // point...
+    nsCOMPtr<nsPIDOMWindow> win = do_QueryInterface(sgo);
+    if (win) {
+      MOZ_ASSERT(win->IsInnerWindow());
+      nsPIDOMWindow* outer = win->GetOuterWindow();
+      if (!outer || win != outer->GetCurrentInnerWindow()) {
+        // Just bail out from here
+        return;
+      }
+    }
+    // if not a window at all, just press on
+
+    ctx = sgo->GetContext();
+    if (ctx) {
+      // We don't check whether scripts are enabled on ctx, because
+      // CheckFunctionAccess will do that anyway... and because we ignore them
+      // being disabled if the callee is system.
+      cx = ctx->GetNativeContext();
+    }
+  }
+
+  if (!cx) {
+    // We didn't manage to hunt down a script global to work with.  Just fall
+    // back on using the safe context.
+    cx = nsContentUtils::GetSafeJSContext();
+  }
+
+  // Victory!  We have a JSContext.  Now do the things we need a JSContext for.
+  mAr.construct(cx);
+
+  // Make sure our JSContext is pushed on the stack.
+  if (!mCxPusher.Push(cx, false)) {
+    return;
+  }
+
+  // After this point we guarantee calling ScriptEvaluated() if we
+  // have an nsIScriptContext.
+  // XXXbz Why, if, say CheckFunctionAccess fails?  I know that's how
+  // nsJSContext::CallEventHandler works, but is it required?
+  // FIXME: Bug 807369.
+  mCtx = ctx;
+
+  // Check that it's ok to run this callback at all.
+  // FIXME: Bug 807371: we want a less silly check here.
+  // Make sure to unwrap aCallable before passing it in, because
+  // getting principals from wrappers is silly.
+  nsresult rv = nsContentUtils::GetSecurityManager()->
+    CheckFunctionAccess(cx, js::UnwrapObject(aCallable), nullptr);
+
+  // Construct a termination func holder even if we're not planning to
+  // run any script.  We need this because we're going to call
+  // ScriptEvaluated even if we don't run the script...  See XXX
+  // comment above.
+  if (ctx) {
+    mTerminationFuncHolder.construct(static_cast<nsJSContext*>(ctx));
+  }
+
+  if (NS_FAILED(rv)) {
+    // Security check failed.  We're done here.
+    return;
+  }
+
+  // Enter the compartment of our callable, so we can actually call it.
+  mAc.construct(cx, aCallable);
+
+  // And now we're ready to go.
+  mCx = cx;
+}
+
+CallbackFunction::CallSetup::~CallSetup()
+{
+  // First things first: if we have a JSContext, report any pending
+  // errors on it.
+  if (mCx) {
+    nsJSUtils::ReportPendingException(mCx);
+  }
+
+  // If we have an mCtx, we need to call ScriptEvaluated() on it.  But we have
+  // to do that after we pop the JSContext stack (see bug 295983).  And to get
+  // our nesting right we have to destroy our JSAutoCompartment first.  But be
+  // careful: it might not have been constructed at all!
+  mAc.destroyIfConstructed();
+
+  // XXXbz For that matter why do we need to manually call ScriptEvaluated at
+  // all?  nsCxPusher::Pop will do that nowadays if !mScriptIsRunning, so the
+  // concerns from bug 295983 don't seem relevant anymore.  Do we want to make
+  // sure it's still called when !mScriptIsRunning?  I guess play it safe for
+  // now and do what CallEventHandler did, which is call always.
+
+  // Popping an nsCxPusher is safe even if it never got pushed.
+  mCxPusher.Pop();
+
+  if (mCtx) {
+    mCtx->ScriptEvaluated(true);
+  }
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/bindings/CallbackFunction.h
@@ -0,0 +1,142 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+/**
+ * A common base class for representing WebIDL callback function types in C++.
+ *
+ * This class implements common functionality like lifetime
+ * management, initialization with the callable, and setup of the call
+ * environment.  Subclasses corresponding to particular callback
+ * function types should provide a Call() method that actually does
+ * the call.
+ */
+
+#pragma once
+
+#include "nsISupports.h"
+#include "nsISupportsImpl.h"
+#include "nsCycleCollectionParticipant.h"
+#include "jsapi.h"
+#include "jswrapper.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Util.h"
+#include "nsContentUtils.h"
+#include "nsWrapperCache.h"
+#include "nsJSEnvironment.h"
+#include "xpcpublic.h"
+
+namespace mozilla {
+namespace dom {
+
+class CallbackFunction : public nsISupports
+{
+public:
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(CallbackFunction)
+
+  /**
+   * Create a CallbackFunction.  aCallable is the callable we're wrapping.
+   * aOwner is the object that will be receiving this CallbackFunction as a
+   * method argument, if any.  We need this so we can store our callable in the
+   * same compartment as our owner.  If *aInited is set to false, an exception
+   * has been thrown.
+   */
+  CallbackFunction(JSContext* cx, JSObject* aOwner, JSObject* aCallable,
+                   bool* aInited)
+    : mCallable(nullptr)
+  {
+    MOZ_ASSERT(JS_ObjectIsCallable(cx, aCallable));
+    // If aOwner is not null, enter the compartment of aOwner's
+    // underlying object.
+    if (aOwner) {
+      aOwner = js::UnwrapObject(aOwner);
+      JSAutoCompartment ac(cx, aOwner);
+      if (!JS_WrapObject(cx, &aCallable)) {
+        *aInited = false;
+        return;
+      }
+    }
+
+    // Set mCallable before we hold, on the off chance that a GC could somehow
+    // happen in there... (which would be pretty odd, granted).
+    mCallable = aCallable;
+    NS_HOLD_JS_OBJECTS(this, CallbackFunction);
+    *aInited = true;
+  }
+
+  virtual ~CallbackFunction()
+  {
+    DropCallback();
+  }
+
+  JSObject* Callable() const
+  {
+    xpc_UnmarkGrayObject(mCallable);
+    return mCallable;
+  }
+
+protected:
+  void DropCallback()
+  {
+    if (mCallable) {
+      NS_DROP_JS_OBJECTS(this, CallbackFunction);
+      mCallable = nullptr;
+    }
+  }
+
+  JSObject* mCallable;
+
+  class NS_STACK_CLASS CallSetup
+  {
+    /**
+     * A class that performs whatever setup we need to safely make a
+     * call while this class is on the stack, After the constructor
+     * returns, the call is safe to make if GetContext() returns
+     * non-null.
+     */
+  public:
+    CallSetup(JSObject* const aCallable);
+    ~CallSetup();
+
+    JSContext* GetContext() const
+    {
+      return mCx;
+    }
+
+  private:
+    // We better not get copy-constructed
+    CallSetup(const CallSetup&) MOZ_DELETE;
+
+    // Members which can go away whenever
+    JSContext* mCx;
+    nsCOMPtr<nsIScriptContext> mCtx;
+
+    // And now members whose construction/destruction order we need to control.
+
+    // Put our nsAutoMicrotask first, so it gets destroyed after everything else
+    // is gone
+    nsAutoMicroTask mMt;
+
+    // Can't construct an XPCAutoRequest until we have a JSContext, so
+    // this needs to be a Maybe.
+    Maybe<XPCAutoRequest> mAr;
+
+    // Can't construct a TerminationFuncHolder without an nsJSContext.  But we
+    // generally want its destructor to come after the destructor of mCxPusher.
+    Maybe<nsJSContext::TerminationFuncHolder> mTerminationFuncHolder;
+
+    nsCxPusher mCxPusher;
+
+    // Can't construct a JSAutoCompartment without a JSContext either.  Also,
+    // Put mAc after mCxPusher so that we exit the compartment before we pop the
+    // JSContext.  Though in practice we'll often manually order those two
+    // things.
+    Maybe<JSAutoCompartment> mAc;
+  };
+};
+
+} // namespace dom
+} // namespace mozilla
--- a/dom/bindings/Makefile.in
+++ b/dom/bindings/Makefile.in
@@ -40,27 +40,29 @@ globalgen_targets := \
   UnionTypes.h \
   UnionConversions.h \
   $(NULL)
 
 CPPSRCS = \
   $(linked_binding_cpp_files) \
   $(filter %.cpp, $(globalgen_targets)) \
   BindingUtils.cpp \
+  CallbackFunction.cpp \
   DOMJSProxyHandler.cpp \
   $(NULL)
 
 EXPORTS_NAMESPACES = $(binding_include_path) mozilla
 
 EXPORTS_mozilla = \
   ErrorResult.h \
   $(NULL)
 
 EXPORTS_$(binding_include_path) = \
   BindingUtils.h \
+  CallbackFunction.h \
   DOMJSClass.h \
   DOMJSProxyHandler.h \
   Errors.msg \
   NonRefcountedDOMObject.h \
   Nullable.h \
   PrimitiveConversions.h \
   PrototypeList.h \
   RegisterBindings.h \