Bug 1960461 - make it possible to have non-refcounted CallbackObject(Base)s, r=mccr8

Differential Revision: https://phabricator.services.mozilla.com/D245759
This commit is contained in:
Olli Pettay
2025-04-28 12:30:29 +00:00
parent 05265244cb
commit b45bbc19f4
2 changed files with 150 additions and 138 deletions

View File

@@ -87,7 +87,7 @@ NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(CallbackObject)
// If a new member is added here, don't forget to update IsBlackForCC. // If a new member is added here, don't forget to update IsBlackForCC.
NS_IMPL_CYCLE_COLLECTION_TRACE_END NS_IMPL_CYCLE_COLLECTION_TRACE_END
void CallbackObject::Trace(JSTracer* aTracer) { void CallbackObjectBase::Trace(JSTracer* aTracer) {
JS::TraceEdge(aTracer, &mCallback, "CallbackObject.mCallback"); JS::TraceEdge(aTracer, &mCallback, "CallbackObject.mCallback");
JS::TraceEdge(aTracer, &mCallbackGlobal, "CallbackObject.mCallbackGlobal"); JS::TraceEdge(aTracer, &mCallbackGlobal, "CallbackObject.mCallbackGlobal");
JS::TraceEdge(aTracer, &mCreationStack, "CallbackObject.mCreationStack"); JS::TraceEdge(aTracer, &mCreationStack, "CallbackObject.mCreationStack");
@@ -119,7 +119,7 @@ void CallbackObject::FinishSlowJSInitIfMoreThanOneOwner(JSContext* aCx) {
} }
} }
JSObject* CallbackObject::Callback(JSContext* aCx) { JSObject* CallbackObjectBase::Callback(JSContext* aCx) {
JSObject* callback = CallbackOrNull(); JSObject* callback = CallbackOrNull();
if (!callback) { if (!callback) {
callback = JS_NewDeadWrapper(aCx); callback = JS_NewDeadWrapper(aCx);
@@ -129,7 +129,7 @@ JSObject* CallbackObject::Callback(JSContext* aCx) {
return callback; return callback;
} }
void CallbackObject::GetDescription(nsACString& aOutString) { void CallbackObjectBase::GetDescription(nsACString& aOutString) {
JSObject* wrappedCallback = CallbackOrNull(); JSObject* wrappedCallback = CallbackOrNull();
if (!wrappedCallback) { if (!wrappedCallback) {
aOutString.Append("<callback from a nuked compartment>"); aOutString.Append("<callback from a nuked compartment>");
@@ -188,12 +188,12 @@ void CallbackObject::GetDescription(nsACString& aOutString) {
aOutString.Append(")"); aOutString.Append(")");
} }
CallbackObject::CallSetup::CallSetup(CallbackObject* aCallback, CallbackObjectBase::CallSetup::CallSetup(CallbackObjectBase* aCallback,
ErrorResult& aRv, ErrorResult& aRv,
const char* aExecutionReason, const char* aExecutionReason,
ExceptionHandling aExceptionHandling, ExceptionHandling aExceptionHandling,
JS::Realm* aRealm, JS::Realm* aRealm,
bool aIsJSImplementedWebIDL) bool aIsJSImplementedWebIDL)
: mCx(nullptr), : mCx(nullptr),
mRealm(aRealm), mRealm(aRealm),
mErrorResult(aRv), mErrorResult(aRv),
@@ -319,7 +319,7 @@ CallbackObject::CallSetup::CallSetup(CallbackObject* aCallback,
mCallContext.emplace(cx, nullptr); mCallContext.emplace(cx, nullptr);
} }
bool CallbackObject::CallSetup::ShouldRethrowException( bool CallbackObjectBase::CallSetup::ShouldRethrowException(
JS::Handle<JS::Value> aException) { JS::Handle<JS::Value> aException) {
if (mExceptionHandling == eRethrowExceptions) { if (mExceptionHandling == eRethrowExceptions) {
MOZ_ASSERT(!mRealm); MOZ_ASSERT(!mRealm);
@@ -340,7 +340,7 @@ bool CallbackObject::CallSetup::ShouldRethrowException(
return js::GetNonCCWObjectRealm(obj) == mRealm; return js::GetNonCCWObjectRealm(obj) == mRealm;
} }
CallbackObject::CallSetup::~CallSetup() { CallbackObjectBase::CallSetup::~CallSetup() {
// To get our nesting right we have to destroy our JSAutoRealm first. // To get our nesting right we have to destroy our JSAutoRealm first.
// In particular, we want to do this before we try reporting any exceptions, // In particular, we want to do this before we try reporting any exceptions,
// so we end up reporting them while in the realm of our entry point, // so we end up reporting them while in the realm of our entry point,

View File

@@ -63,48 +63,15 @@ class OwningNonNull;
namespace dom { namespace dom {
#define DOM_CALLBACKOBJECT_IID \ #define DOM_CALLBACKOBJECT_IID \
{ \ {0xbe74c190, 0x6d76, 0x4991, {0x84, 0xb9, 0x65, 0x06, 0x99, 0xe6, 0x93, 0x2b}}
0xbe74c190, 0x6d76, 0x4991, { \
0x84, 0xb9, 0x65, 0x06, 0x99, 0xe6, 0x93, 0x2b \
} \
}
class CallbackObject : public nsISupports, public JSHolderBase { class CallbackObjectBase {
public: public:
NS_DECLARE_STATIC_IID_ACCESSOR(DOM_CALLBACKOBJECT_IID) CallbackObjectBase() = default;
CallbackObjectBase(JSObject* aCallback, JSObject* aCallbackGlobal,
NS_DECL_CYCLE_COLLECTING_ISUPPORTS JSObject* aAsyncStack, nsIGlobalObject* aIncumbentGlobal) {
NS_DECL_CYCLE_COLLECTION_SKIPPABLE_SCRIPT_HOLDER_CLASS(CallbackObject) InitNoHold(aCallback, aCallbackGlobal, aAsyncStack, aIncumbentGlobal);
// 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<JSObject*> aCallback,
JS::Handle<JSObject*> aCallbackGlobal,
nsIGlobalObject* aIncumbentGlobal) {
if (aCx && JS::IsAsyncStackCaptureEnabledForRealm(aCx)) {
JS::Rooted<JSObject*> 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 // This is guaranteed to be non-null from the time the CallbackObject is
@@ -177,10 +144,6 @@ class CallbackObject : public nsISupports, public JSHolderBase {
// "<functionName> (<sourceURL>:<lineNumber>)" // "<functionName> (<sourceURL>:<lineNumber>)"
void GetDescription(nsACString& aOutString); 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 // 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 // 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 // traversing our edges to them, because we know they can't be collected
@@ -200,36 +163,24 @@ class CallbackObject : public nsISupports, public JSHolderBase {
} }
protected: protected:
virtual ~CallbackObject() { mozilla::DropJSObjectsWithKey(this); } virtual ~CallbackObjectBase() = default;
explicit CallbackObject(CallbackObject* aCallbackObject) { // Provide a way to clear this object's pointers to GC things after the
Init(aCallbackObject->mCallback, aCallbackObject->mCallbackGlobal, // callback has been run. Note that CallbackOrNull() will return null after
aCallbackObject->mCreationStack, aCallbackObject->mIncumbentGlobal); // 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;
} }
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<CallbackObject> mHolder;
};
private:
inline void InitNoHold(JSObject* aCallback, JSObject* aCallbackGlobal, inline void InitNoHold(JSObject* aCallback, JSObject* aCallbackGlobal,
JSObject* aCreationStack, JSObject* aCreationStack,
nsIGlobalObject* aIncumbentGlobal) { nsIGlobalObject* aIncumbentGlobal) {
@@ -250,37 +201,6 @@ class CallbackObject : public nsISupports, public JSHolderBase {
} }
} }
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::HoldJSObjectsWithKey(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();
mozilla::DropJSObjectsWithKey(this);
}
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() { void ClearJSObjects() {
MOZ_ASSERT_IF(mIncumbentJSGlobal, mCallback); MOZ_ASSERT_IF(mIncumbentJSGlobal, mCallback);
if (mCallback) { if (mCallback) {
@@ -291,30 +211,6 @@ class CallbackObject : public nsISupports, public JSHolderBase {
// For use from subclasses that want to be usable with Rooted. // For use from subclasses that want to be usable with Rooted.
void Trace(JSTracer* aTracer); 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. // 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 // 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, // its members, directly itself, this code won't call f(), or get its members,
@@ -352,7 +248,7 @@ class CallbackObject : public nsISupports, public JSHolderBase {
// to the realm in which exceptions will be rethrown. In that case // to the realm in which exceptions will be rethrown. In that case
// they will only be rethrown if that realm's principal subsumes the // they will only be rethrown if that realm's principal subsumes the
// principal of our (unwrapped) callback. // principal of our (unwrapped) callback.
CallSetup(CallbackObject* aCallback, ErrorResult& aRv, CallSetup(CallbackObjectBase* aCallback, ErrorResult& aRv,
const char* aExecutionReason, const char* aExecutionReason,
ExceptionHandling aExceptionHandling, JS::Realm* aRealm = nullptr, ExceptionHandling aExceptionHandling, JS::Realm* aRealm = nullptr,
bool aIsJSImplementedWebIDL = false); bool aIsJSImplementedWebIDL = false);
@@ -407,6 +303,122 @@ class CallbackObject : public nsISupports, public JSHolderBase {
}; };
}; };
class CallbackObject : public nsISupports,
public CallbackObjectBase,
public JSHolderBase {
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<JSObject*> aCallback,
JS::Handle<JSObject*> aCallbackGlobal,
nsIGlobalObject* aIncumbentGlobal) {
if (aCx && JS::IsAsyncStackCaptureEnabledForRealm(aCx)) {
JS::Rooted<JSObject*> 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);
}
size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const {
return aMallocSizeOf(this);
}
void Reset() {
CallbackObjectBase::Reset();
mozilla::DropJSObjectsWithKey(this);
}
protected:
virtual ~CallbackObject() { mozilla::DropJSObjectsWithKey(this); }
explicit CallbackObject(CallbackObject* aCallbackObject) {
Init(aCallbackObject->mCallback, aCallbackObject->mCallbackGlobal,
aCallbackObject->mCreationStack, aCallbackObject->mIncumbentGlobal);
}
// 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);
}
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<CallbackObject> mHolder;
};
private:
CallbackObject(const CallbackObject&) = delete;
CallbackObject& operator=(const CallbackObject&) = delete;
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::HoldJSObjectsWithKey(this);
}
};
template <class WebIDLCallbackT, class XPCOMCallbackT> template <class WebIDLCallbackT, class XPCOMCallbackT>
class CallbackObjectHolder; class CallbackObjectHolder;