1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207
|
/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
// SPDX-FileCopyrightText: 2008 litl, LLC
// SPDX-FileCopyrightText: 2021 Canonical Ltd.
// SPDX-FileContributor: Marco Trevisan <marco.trevisan@canonical.com>
#include <config.h>
#include <glib.h> // for g_assert
#include <js/CallAndConstruct.h>
#include <js/Realm.h>
#include <js/RootingAPI.h>
#include <js/TypeDecls.h>
#include <js/Value.h>
#include "gi/closure.h"
#include "cjs/context-private.h"
#include "cjs/jsapi-util-root.h"
#include "cjs/jsapi-util.h"
#include "cjs/mem-private.h"
#include "util/log.h"
namespace Gjs {
Closure::Closure(JSContext* cx, JSObject* callable, bool root,
const char* description GJS_USED_VERBOSE_GCLOSURE)
: m_cx(cx) {
GJS_INC_COUNTER(closure);
GClosureNotify closure_notify;
if (root) {
// Fully manage closure lifetime if so asked
auto* gjs = GjsContextPrivate::from_cx(cx);
g_assert(cx == gjs->context());
m_callable.root(cx, callable);
gjs->register_notifier(global_context_notifier_cb, this);
closure_notify = [](void*, GClosure* closure) {
static_cast<Closure*>(closure)->closure_invalidated();
};
} else {
// Only mark the closure as invalid if memory is managed
// outside (i.e. by object.c for signals)
m_callable = callable;
closure_notify = [](void*, GClosure* closure) {
static_cast<Closure*>(closure)->closure_set_invalid();
};
}
g_closure_add_invalidate_notifier(this, nullptr, closure_notify);
gjs_debug_closure("Create closure %p which calls callable %p '%s'", this,
m_callable.debug_addr(), description);
}
/*
* Memory management of closures is "interesting" because we're keeping around
* a JSContext* and then trying to use it spontaneously from the main loop.
* I don't think that's really quite kosher, and perhaps the problem is that
* (in xulrunner) we just need to save a different context.
*
* Or maybe the right fix is to create our own context just for this?
*
* But for the moment, we save the context that was used to create the closure.
*
* Here's the problem: this context can be destroyed. AFTER the
* context is destroyed, or at least potentially after, the objects in
* the context's global object may be garbage collected. Remember that
* JSObject* belong to a runtime, not a context.
*
* There is apparently no robust way to track context destruction in
* SpiderMonkey, because the context can be destroyed without running
* the garbage collector, and xulrunner takes over the JS_SetContextCallback()
* callback. So there's no callback for us.
*
* So, when we go to use our context, we iterate the contexts in the runtime
* and see if ours is still in the valid list, and decide to invalidate
* the closure if it isn't.
*
* The closure can thus be destroyed in several cases:
* - invalidation by unref, e.g. when a signal is disconnected, closure is unref'd
* - invalidation because we were invoked while the context was dead
* - invalidation through finalization (we were garbage collected)
*
* These don't have to happen in the same order; garbage collection can
* be either before, or after, context destruction.
*
*/
void Closure::unset_context() {
if (!m_cx)
return;
if (m_callable && m_callable.rooted()) {
auto* gjs = GjsContextPrivate::from_cx(m_cx);
gjs->unregister_notifier(global_context_notifier_cb, this);
}
m_cx = nullptr;
}
void Closure::global_context_finalized() {
gjs_debug_closure(
"Context global object destroy notifier on closure %p which calls "
"callable %p",
this, m_callable.debug_addr());
if (m_callable) {
// Manually unset the context as we don't need to unregister the
// notifier here, or we'd end up touching a vector we're iterating
m_cx = nullptr;
reset();
// Notify any closure reference holders they
// may want to drop references.
g_closure_invalidate(this);
}
}
/* Invalidation is like "dispose" - it is guaranteed to happen at
* finalize, but may happen before finalize. Normally, g_closure_invalidate()
* is called when the "target" of the closure becomes invalid, so that the
* source (the signal connection, say can be removed.) The usage above
* in global_context_finalized() is typical. Since the target of the closure
* is under our control, it's unlikely that g_closure_invalidate() will ever
* be called by anyone else, but in case it ever does, it's slightly better
* to remove the "keep alive" here rather than in the finalize notifier.
*
* Unlike "dispose" invalidation only happens once.
*/
void Closure::closure_invalidated() {
GJS_DEC_COUNTER(closure);
gjs_debug_closure("Invalidating closure %p which calls callable %p", this,
m_callable.debug_addr());
if (!m_callable) {
gjs_debug_closure(" (closure %p already dead, nothing to do)", this);
return;
}
/* The context still exists, remove our destroy notifier. Otherwise we
* would call the destroy notifier on an already-freed closure.
*
* This happens in the normal case, when the closure is
* invalidated for some reason other than destruction of the
* JSContext.
*/
gjs_debug_closure(
" (closure %p's context was alive, "
"removing our destroy notifier on global object)",
this);
reset();
}
void Closure::closure_set_invalid() {
gjs_debug_closure("Invalidating signal closure %p which calls callable %p",
this, m_callable.debug_addr());
m_callable.prevent_collection();
reset();
GJS_DEC_COUNTER(closure);
}
bool Closure::invoke(JS::HandleObject this_obj,
const JS::HandleValueArray& args,
JS::MutableHandleValue retval) {
if (!m_callable) {
/* We were destroyed; become a no-op */
reset();
return false;
}
JSAutoRealm ar{m_cx, m_callable.get()};
if (gjs_log_exception(m_cx)) {
gjs_debug_closure(
"Exception was pending before invoking callback??? "
"Not expected - closure %p",
this);
}
JS::RootedValue v_callable{m_cx, JS::ObjectValue(*m_callable.get())};
bool ok = JS::Call(m_cx, this_obj, v_callable, args, retval);
GjsContextPrivate* gjs = GjsContextPrivate::from_cx(m_cx);
if (!ok) {
/* Exception thrown... */
gjs_debug_closure(
"Closure invocation failed (exception should have been thrown) "
"closure %p callable %p",
this, m_callable.debug_addr());
return false;
}
if (gjs_log_exception_uncaught(m_cx)) {
gjs_debug_closure(
"Closure invocation succeeded but an exception was set"
" - closure %p",
m_cx);
}
gjs->schedule_gc_if_needed();
return true;
}
} // namespace Gjs
|