/* -*- 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/. */ /** * A common base class for representing WebIDL callback function and * callback interface types in C++. * * This class implements common functionality like lifetime * management, initialization with the JS object, and setup of the * call environment. Subclasses are responsible for providing methods * that do the call into JS as needed. */ #ifndef mozilla_dom_CallbackObject_h #define mozilla_dom_CallbackObject_h #include #include #include #include "js/RootingAPI.h" #include "js/Wrapper.h" #include "jsapi.h" #include "mozilla/AlreadyAddRefed.h" #include "mozilla/Assertions.h" #include "mozilla/Attributes.h" #include "mozilla/HoldDropJSObjects.h" #include "mozilla/Maybe.h" #include "mozilla/MemoryReporting.h" #include "mozilla/RefPtr.h" #include "mozilla/dom/BindingCallContext.h" #include "mozilla/dom/ScriptSettings.h" #include "nsCOMPtr.h" #include "nsCycleCollectionParticipant.h" #include "nsID.h" #include "nsIGlobalObject.h" #include "nsISupports.h" #include "nsISupportsUtils.h" #include "nsStringFwd.h" class JSAutoRealm; class JSObject; class JSTracer; class nsCycleCollectionTraversalCallback; struct JSContext; namespace JS { class AutoSetAsyncStackForNewCalls; class Realm; class Value; } // namespace JS namespace mozilla { class ErrorResult; class PromiseJobRunnable; template class OwningNonNull; namespace dom { #define DOM_CALLBACKOBJECT_IID \ { \ 0xbe74c190, 0x6d76, 0x4991, { \ 0x84, 0xb9, 0x65, 0x06, 0x99, 0xe6, 0x93, 0x2b \ } \ } class CallbackObject : public nsISupports { public: NS_DECLARE_STATIC_IID_ACCESSOR(DOM_CALLBACKOBJECT_IID) NS_DECL_CYCLE_COLLECTING_ISUPPORTS NS_DECL_CYCLE_COLLECTION_SKIPPABLE_SCRIPT_HOLDER_CLASS(CallbackObject) // The caller may pass a global object which will act as an override for the // incumbent script settings object when the callback is invoked (overriding // the entry point computed from aCallback). If no override is required, the // caller should pass null. |aCx| is used to capture the current // stack, which is later used as an async parent when the callback // is invoked. aCx can be nullptr, in which case no stack is // captured. explicit CallbackObject(JSContext* aCx, JS::Handle aCallback, JS::Handle aCallbackGlobal, nsIGlobalObject* aIncumbentGlobal) { if (aCx && JS::IsAsyncStackCaptureEnabledForRealm(aCx)) { JS::RootedObject stack(aCx); if (!JS::CaptureCurrentStack(aCx, &stack)) { JS_ClearPendingException(aCx); } Init(aCallback, aCallbackGlobal, stack, aIncumbentGlobal); } else { Init(aCallback, aCallbackGlobal, nullptr, aIncumbentGlobal); } } // Instead of capturing the current stack to use as an async parent when the // callback is invoked, the caller can use this overload to pass in a stack // for that purpose. explicit CallbackObject(JSObject* aCallback, JSObject* aCallbackGlobal, JSObject* aAsyncStack, nsIGlobalObject* aIncumbentGlobal) { Init(aCallback, aCallbackGlobal, aAsyncStack, aIncumbentGlobal); } // This is guaranteed to be non-null from the time the CallbackObject is // created until JavaScript has had a chance to run. It will only return null // after a JavaScript caller has called nukeSandbox on a Sandbox object and // the cycle collector has had a chance to run, unless Reset() is explicitly // called (see below). // // This means that any native callee which receives a CallbackObject as an // argument can safely rely on the callback being non-null so long as it // doesn't trigger any scripts before it accesses it. JSObject* CallbackOrNull() const { mCallback.exposeToActiveJS(); return CallbackPreserveColor(); } JSObject* CallbackGlobalOrNull() const { mCallbackGlobal.exposeToActiveJS(); return mCallbackGlobal; } // Like CallbackOrNull(), but will return a new dead proxy object in the // caller's realm if the callback is null. JSObject* Callback(JSContext* aCx); JSObject* GetCreationStack() const { return mCreationStack; } void MarkForCC() { mCallback.exposeToActiveJS(); mCallbackGlobal.exposeToActiveJS(); mCreationStack.exposeToActiveJS(); } /* * This getter does not change the color of the JSObject meaning that the * object returned is not guaranteed to be kept alive past the next CC. */ JSObject* CallbackPreserveColor() const { return mCallback.unbarrieredGet(); } JSObject* CallbackGlobalPreserveColor() const { return mCallbackGlobal.unbarrieredGet(); } /* * If the callback is known to be non-gray, then this method can be * used instead of CallbackOrNull() to avoid the overhead of * ExposeObjectToActiveJS(). */ JSObject* CallbackKnownNotGray() const { JS::AssertObjectIsNotGray(mCallback); return CallbackPreserveColor(); } nsIGlobalObject* IncumbentGlobalOrNull() const { return mIncumbentGlobal; } enum ExceptionHandling { // Report any exception and don't throw it to the caller code. eReportExceptions, // Throw any exception to the caller code and don't report it. eRethrowExceptions, // Throw an exception to the caller code if the thrown exception is a // binding object for a DOMException from the caller's scope, otherwise // report it. eRethrowContentExceptions }; // Append a UTF-8 string to aOutString that describes the callback function, // for use in logging or profiler markers. // The string contains the function name and its source location, if // available, in the following format: // " (:)" void GetDescription(nsACString& aOutString); size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const { return aMallocSizeOf(this); } // Used for cycle collection optimization. Should return true only if all our // outgoing edges are to known-live objects. In that case, there's no point // traversing our edges to them, because we know they can't be collected // anyway. bool IsBlackForCC() const { // Play it safe in case this gets called after unlink. return (!mCallback || !JS::ObjectIsMarkedGray(mCallback)) && (!mCallbackGlobal || !JS::ObjectIsMarkedGray(mCallbackGlobal)) && (!mCreationStack || !JS::ObjectIsMarkedGray(mCreationStack)) && (!mIncumbentJSGlobal || !JS::ObjectIsMarkedGray(mIncumbentJSGlobal)) && // mIncumbentGlobal is known-live if we have a known-live // mIncumbentJSGlobal, since mIncumbentJSGlobal will keep a ref to // it. At this point if mIncumbentJSGlobal is not null, it's // known-live. (!mIncumbentGlobal || mIncumbentJSGlobal); } protected: virtual ~CallbackObject() { mozilla::DropJSObjects(this); } explicit CallbackObject(CallbackObject* aCallbackObject) { Init(aCallbackObject->mCallback, aCallbackObject->mCallbackGlobal, aCallbackObject->mCreationStack, aCallbackObject->mIncumbentGlobal); } bool operator==(const CallbackObject& aOther) const { JSObject* wrappedThis = CallbackPreserveColor(); JSObject* wrappedOther = aOther.CallbackPreserveColor(); if (!wrappedThis || !wrappedOther) { return this == &aOther; } JSObject* thisObj = js::UncheckedUnwrap(wrappedThis); JSObject* otherObj = js::UncheckedUnwrap(wrappedOther); return thisObj == otherObj; } class JSObjectsDropper final { public: explicit JSObjectsDropper(CallbackObject* aHolder) : mHolder(aHolder) {} ~JSObjectsDropper() { mHolder->ClearJSObjects(); } private: RefPtr mHolder; }; private: inline void InitNoHold(JSObject* aCallback, JSObject* aCallbackGlobal, JSObject* aCreationStack, nsIGlobalObject* aIncumbentGlobal) { MOZ_ASSERT(aCallback && !mCallback); MOZ_ASSERT(aCallbackGlobal); MOZ_DIAGNOSTIC_ASSERT(JS::GetCompartment(aCallback) == JS::GetCompartment(aCallbackGlobal)); MOZ_ASSERT(JS_IsGlobalObject(aCallbackGlobal)); mCallback = aCallback; mCallbackGlobal = aCallbackGlobal; mCreationStack = aCreationStack; if (aIncumbentGlobal) { mIncumbentGlobal = aIncumbentGlobal; // We don't want to expose to JS here (change the color). If someone ever // reads mIncumbentJSGlobal, that will expose. If not, no need to expose // here. mIncumbentJSGlobal = aIncumbentGlobal->GetGlobalJSObjectPreserveColor(); } } inline void Init(JSObject* aCallback, JSObject* aCallbackGlobal, JSObject* aCreationStack, nsIGlobalObject* aIncumbentGlobal) { // Set script objects before we hold, on the off chance that a GC could // somehow happen in there... (which would be pretty odd, granted). InitNoHold(aCallback, aCallbackGlobal, aCreationStack, aIncumbentGlobal); mozilla::HoldJSObjects(this); } // Provide a way to clear this object's pointers to GC things after the // callback has been run. Note that CallbackOrNull() will return null after // this point. This should only be called if the object is known not to be // used again, and no handles (e.g. those returned by CallbackPreserveColor) // are in use. void Reset() { ClearJSReferences(); } friend class mozilla::PromiseJobRunnable; inline void ClearJSReferences() { mCallback = nullptr; mCallbackGlobal = nullptr; mCreationStack = nullptr; mIncumbentJSGlobal = nullptr; } CallbackObject(const CallbackObject&) = delete; CallbackObject& operator=(const CallbackObject&) = delete; protected: void ClearJSObjects() { MOZ_ASSERT_IF(mIncumbentJSGlobal, mCallback); if (mCallback) { ClearJSReferences(); } } // For use from subclasses that want to be usable with Rooted. void Trace(JSTracer* aTracer); // For use from subclasses that want to be traced for a bit then possibly // switch to HoldJSObjects and do other slow JS-related init work we might do. // If we have more than one owner, this will HoldJSObjects and do said slow // init work; otherwise it will just forget all our JS references. void FinishSlowJSInitIfMoreThanOneOwner(JSContext* aCx); // Struct used as a way to force a CallbackObject constructor to not call // HoldJSObjects. We're putting it here so that CallbackObject subclasses will // have access to it, but outside code will not. // // Places that use this need to ensure that the callback is traced (e.g. via a // Rooted) until the HoldJSObjects call happens. struct FastCallbackConstructor {}; // Just like the public version without the FastCallbackConstructor argument, // except for not calling HoldJSObjects and not capturing async stacks (on the // assumption that we will do that last whenever we decide to actually // HoldJSObjects; see FinishSlowJSInitIfMoreThanOneOwner). If you use this, // you MUST ensure that the object is traced until the HoldJSObjects happens! CallbackObject(JSObject* aCallback, JSObject* aCallbackGlobal, const FastCallbackConstructor&) { InitNoHold(aCallback, aCallbackGlobal, nullptr, nullptr); } // mCallback is not unwrapped, so it can be a cross-compartment-wrapper. // This is done to ensure that, if JS code can't call a callback f(), or get // its members, directly itself, this code won't call f(), or get its members, // on the code's behalf. JS::Heap mCallback; // mCallbackGlobal is the global that we were in when we created the // callback. In particular, it is guaranteed to be same-compartment with // aCallback. We store it separately, because we have no way to recover the // global if mCallback is a cross-compartment wrapper. JS::Heap mCallbackGlobal; JS::Heap mCreationStack; // Ideally, we'd just hold a reference to the nsIGlobalObject, since that's // what we need to pass to AutoIncumbentScript. Unfortunately, that doesn't // hold the actual JS global alive. So we maintain an additional pointer to // the JS global itself so that we can trace it. // // At some point we should consider trying to make native globals hold their // scripted global alive, at which point we can get rid of the duplication // here. nsCOMPtr mIncumbentGlobal; JS::TenuredHeap mIncumbentJSGlobal; class MOZ_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: // If aExceptionHandling == eRethrowContentExceptions then aRealm // needs to be set to the realm in which exceptions will be rethrown. // // If aExceptionHandling == eRethrowExceptions then aRealm may be set // to the realm in which exceptions will be rethrown. In that case // they will only be rethrown if that realm's principal subsumes the // principal of our (unwrapped) callback. CallSetup(CallbackObject* aCallback, ErrorResult& aRv, const char* aExecutionReason, ExceptionHandling aExceptionHandling, JS::Realm* aRealm = nullptr, bool aIsJSImplementedWebIDL = false); MOZ_CAN_RUN_SCRIPT ~CallSetup(); JSContext* GetContext() const { return mCx; } // Safe to call this after the constructor has run without throwing on the // ErrorResult it was handed. BindingCallContext& GetCallContext() { return *mCallContext; } private: // We better not get copy-constructed CallSetup(const CallSetup&) = delete; bool ShouldRethrowException(JS::Handle aException); // Members which can go away whenever JSContext* mCx; // Caller's realm. This will only have a sensible value if // mExceptionHandling == eRethrowContentExceptions. JS::Realm* mRealm; // And now members whose construction/destruction order we need to control. Maybe mAutoEntryScript; Maybe mAutoIncumbentScript; Maybe> mRootedCallable; // The global of mRootedCallable. Maybe> mRootedCallableGlobal; // Members which are used to set the async stack. Maybe> mAsyncStack; Maybe mAsyncStackSetter; // Can't construct a JSAutoRealm without a JSContext either. Also, // Put mAr after mAutoEntryScript so that we exit the realm before we // pop the script settings stack. Though in practice we'll often manually // order those two things. Maybe mAr; // Our BindingCallContext. This is a Maybe so we can avoid constructing it // until after we have a JSContext to construct it with. Maybe mCallContext; // An ErrorResult to possibly re-throw exceptions on and whether // we should re-throw them. ErrorResult& mErrorResult; const ExceptionHandling mExceptionHandling; const bool mIsMainThread; }; }; template class CallbackObjectHolder; template void ImplCycleCollectionUnlink(CallbackObjectHolder& aField); class CallbackObjectHolderBase { protected: // Returns null on all failures already_AddRefed ToXPCOMCallback(CallbackObject* aCallback, const nsIID& aIID) const; }; template class CallbackObjectHolder : CallbackObjectHolderBase { /** * A class which stores either a WebIDLCallbackT* or an XPCOMCallbackT*. Both * types must inherit from nsISupports. The pointer that's stored can be * null. * * When storing a WebIDLCallbackT*, mPtrBits is set to the pointer value. * When storing an XPCOMCallbackT*, mPtrBits is the pointer value with low bit * set. */ public: explicit CallbackObjectHolder(WebIDLCallbackT* aCallback) : mPtrBits(reinterpret_cast(aCallback)) { NS_IF_ADDREF(aCallback); } explicit CallbackObjectHolder(XPCOMCallbackT* aCallback) : mPtrBits(reinterpret_cast(aCallback) | XPCOMCallbackFlag) { NS_IF_ADDREF(aCallback); } CallbackObjectHolder(CallbackObjectHolder&& aOther) : mPtrBits(aOther.mPtrBits) { aOther.mPtrBits = 0; static_assert(sizeof(CallbackObjectHolder) == sizeof(void*), "This object is expected to be as small as a pointer, and it " "is currently passed by value in various places. If it is " "bloating, we may want to pass it by reference then."); } CallbackObjectHolder(const CallbackObjectHolder& aOther) = delete; CallbackObjectHolder() : mPtrBits(0) {} ~CallbackObjectHolder() { UnlinkSelf(); } void operator=(WebIDLCallbackT* aCallback) { UnlinkSelf(); mPtrBits = reinterpret_cast(aCallback); NS_IF_ADDREF(aCallback); } void operator=(XPCOMCallbackT* aCallback) { UnlinkSelf(); mPtrBits = reinterpret_cast(aCallback) | XPCOMCallbackFlag; NS_IF_ADDREF(aCallback); } void operator=(CallbackObjectHolder&& aOther) { UnlinkSelf(); mPtrBits = aOther.mPtrBits; aOther.mPtrBits = 0; } void operator=(const CallbackObjectHolder& aOther) = delete; void Reset() { UnlinkSelf(); } nsISupports* GetISupports() const { return reinterpret_cast(mPtrBits & ~XPCOMCallbackFlag); } already_AddRefed Forget() { // This can be called from random threads. Make sure to not refcount things // in here! nsISupports* supp = GetISupports(); mPtrBits = 0; return dont_AddRef(supp); } // Boolean conversion operator so people can use this in boolean tests explicit operator bool() const { return GetISupports(); } CallbackObjectHolder Clone() const { CallbackObjectHolder result; result.mPtrBits = mPtrBits; NS_IF_ADDREF(GetISupports()); return result; } // Even if HasWebIDLCallback returns true, GetWebIDLCallback() might still // return null. bool HasWebIDLCallback() const { return !(mPtrBits & XPCOMCallbackFlag); } WebIDLCallbackT* GetWebIDLCallback() const { MOZ_ASSERT(HasWebIDLCallback()); return reinterpret_cast(mPtrBits); } XPCOMCallbackT* GetXPCOMCallback() const { MOZ_ASSERT(!HasWebIDLCallback()); return reinterpret_cast(mPtrBits & ~XPCOMCallbackFlag); } bool operator==(WebIDLCallbackT* aOtherCallback) const { if (!aOtherCallback) { // If other is null, then we must be null to be equal. return !GetISupports(); } if (!HasWebIDLCallback() || !GetWebIDLCallback()) { // If other is non-null, then we can't be equal if we have a // non-WebIDL callback or a null callback. return false; } return *GetWebIDLCallback() == *aOtherCallback; } bool operator==(XPCOMCallbackT* aOtherCallback) const { return (!aOtherCallback && !GetISupports()) || (!HasWebIDLCallback() && GetXPCOMCallback() == aOtherCallback); } bool operator==(const CallbackObjectHolder& aOtherCallback) const { if (aOtherCallback.HasWebIDLCallback()) { return *this == aOtherCallback.GetWebIDLCallback(); } return *this == aOtherCallback.GetXPCOMCallback(); } // Try to return an XPCOMCallbackT version of this object. already_AddRefed ToXPCOMCallback() const { if (!HasWebIDLCallback()) { RefPtr callback = GetXPCOMCallback(); return callback.forget(); } nsCOMPtr supp = CallbackObjectHolderBase::ToXPCOMCallback( GetWebIDLCallback(), NS_GET_TEMPLATE_IID(XPCOMCallbackT)); if (supp) { // ToXPCOMCallback already did the right QI for us. return supp.forget().downcast(); } return nullptr; } // Try to return a WebIDLCallbackT version of this object. already_AddRefed ToWebIDLCallback() const { if (HasWebIDLCallback()) { RefPtr callback = GetWebIDLCallback(); return callback.forget(); } return nullptr; } private: static const uintptr_t XPCOMCallbackFlag = 1u; friend void ImplCycleCollectionUnlink( CallbackObjectHolder& aField); void UnlinkSelf() { // NS_IF_RELEASE because we might have been unlinked before nsISupports* ptr = GetISupports(); // Clear mPtrBits before the release to prevent reentrance. mPtrBits = 0; NS_IF_RELEASE(ptr); } uintptr_t mPtrBits; }; NS_DEFINE_STATIC_IID_ACCESSOR(CallbackObject, DOM_CALLBACKOBJECT_IID) template inline void ImplCycleCollectionTraverse( nsCycleCollectionTraversalCallback& aCallback, CallbackObjectHolder& aField, const char* aName, uint32_t aFlags = 0) { if (aField) { CycleCollectionNoteChild(aCallback, aField.GetISupports(), aName, aFlags); } } template void ImplCycleCollectionUnlink(CallbackObjectHolder& aField) { aField.UnlinkSelf(); } // T is expected to be a RefPtr or OwningNonNull around a CallbackObject // subclass. This class is used in bindings to safely handle Fast* callbacks; // it ensures that the callback is traced, and that if something is holding onto // the callback when we're done with it HoldJSObjects is called. // // Since we effectively hold a ref to a refcounted thing (like RefPtr or // OwningNonNull), we are also MOZ_IS_SMARTPTR_TO_REFCOUNTED for static analysis // purposes. template class MOZ_RAII MOZ_IS_SMARTPTR_TO_REFCOUNTED RootedCallback : public JS::Rooted { public: explicit RootedCallback(JSContext* cx) : JS::Rooted(cx), mCx(cx) {} // We need a way to make assignment from pointers (how we're normally used) // work. template void operator=(S* arg) { this->get().operator=(arg); } // But nullptr can't use the above template, because it doesn't know which S // to select. So we need a special overload for nullptr. void operator=(decltype(nullptr) arg) { this->get().operator=(arg); } // Codegen relies on being able to do CallbackOrNull() and Callback() on us. JSObject* CallbackOrNull() const { return this->get()->CallbackOrNull(); } JSObject* Callback(JSContext* aCx) const { return this->get()->Callback(aCx); } ~RootedCallback() { // Ensure that our callback starts holding on to its own JS objects as // needed. We really do need to check that things are initialized even when // T is OwningNonNull, because we might be running before the OwningNonNull // ever got assigned to! if (IsInitialized(this->get())) { this->get()->FinishSlowJSInitIfMoreThanOneOwner(mCx); } } private: template static bool IsInitialized(U& aArg); // Not implemented template static bool IsInitialized(RefPtr& aRefPtr) { return aRefPtr; } template static bool IsInitialized(OwningNonNull& aOwningNonNull) { return aOwningNonNull.isInitialized(); } JSContext* mCx; }; } // namespace dom } // namespace mozilla #endif // mozilla_dom_CallbackObject_h