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