Skip to content

Commit 8474c9f

Browse files
authored
Merge pull request #75 from mozilla-spidermonkey/weakref-example
WeakRef and FinalizationRegistry example
2 parents c4211bc + 0c798ca commit 8474c9f

2 files changed

Lines changed: 303 additions & 0 deletions

File tree

examples/weakref.cpp

Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
#include <stdio.h>
2+
3+
#include <jsapi.h>
4+
#include <jsfriendapi.h>
5+
#include <js/CompilationAndEvaluation.h>
6+
#include <js/Initialization.h>
7+
#include <js/Promise.h>
8+
#include <js/Realm.h>
9+
#include <js/SourceText.h>
10+
#include <mozilla/Unused.h>
11+
12+
#include "boilerplate.h"
13+
14+
// This example illustrates what you have to do in your embedding to make
15+
// WeakRef and FinalizationRegistry work. Without notifying SpiderMonkey when to
16+
// clear out WeakRefs and run FinalizationRegistry callbacks, they will appear
17+
// not to work correctly.
18+
//
19+
// See 'boilerplate.cpp' for the parts of this example that are reused in many
20+
// simple embedding examples.
21+
22+
// This function silently ignores errors in a way that production code probably
23+
// wouldn't.
24+
static void LogPendingException(JSContext* cx) {
25+
// Nothing we can do about uncatchable exceptions.
26+
if (!JS_IsExceptionPending(cx)) return;
27+
28+
JS::ExceptionStack exnStack{cx};
29+
if (!JS::StealPendingExceptionStack(cx, &exnStack)) return;
30+
31+
JS::ErrorReportBuilder builder{cx};
32+
if (!builder.init(cx, exnStack, JS::ErrorReportBuilder::NoSideEffects)) {
33+
return;
34+
}
35+
JS::PrintError(stderr, builder, /* reportWarnings = */ false);
36+
}
37+
38+
// This example integrates the FinalizationRegistry job queue together with the
39+
// Promise job handling, since that's a logical place that you might put it in
40+
// your embedding.
41+
//
42+
// However, it's not necessary to use JS::JobQueue and it's not necessary to
43+
// handle Promise jobs, in order to have FinalizationRegistry work. You do need
44+
// to have some kind of job queue, but it can be very minimal. It doesn't have
45+
// to be based on JS::JobQueue. The only requirement is that the enqueued
46+
// cleanup functions must be run "some time in the future".
47+
//
48+
// To approximate a minimal job queue, you might remove m_queue from this class
49+
// and remove the inheritance from JS::JobQueue and its overridden methods.
50+
class CustomJobQueue : public JS::JobQueue {
51+
public:
52+
explicit CustomJobQueue(JSContext* cx)
53+
: m_queue(cx, js::SystemAllocPolicy{}),
54+
m_finalizationRegistryCallbacks(cx),
55+
m_draining(false) {}
56+
~CustomJobQueue() = default;
57+
58+
// JS::JobQueue override
59+
JSObject* getIncumbentGlobal(JSContext* cx) override {
60+
return JS::CurrentGlobalOrNull(cx);
61+
}
62+
63+
// JS::JobQueue override
64+
bool enqueuePromiseJob(JSContext* cx, JS::HandleObject promise,
65+
JS::HandleObject job, JS::HandleObject allocationSite,
66+
JS::HandleObject incumbentGlobal) override {
67+
if (!m_queue.append(job)) {
68+
JS_ReportOutOfMemory(cx);
69+
return false;
70+
}
71+
72+
JS::JobQueueMayNotBeEmpty(cx);
73+
return true;
74+
}
75+
76+
// JS::JobQueue override
77+
void runJobs(JSContext* cx) override {
78+
// Ignore nested calls of runJobs.
79+
if (m_draining) {
80+
return;
81+
}
82+
83+
m_draining = true;
84+
85+
JS::Rooted<JSObject*> job{cx};
86+
JS::Rooted<JS::Value> unused_rval{cx};
87+
88+
while (true) {
89+
// Execute jobs in a loop until we've reached the end of the queue.
90+
while (!m_queue.empty()) {
91+
job = m_queue[0];
92+
m_queue.erase(m_queue.begin()); // In production code, use a FIFO queue
93+
94+
// If the next job is the last job in the job queue, allow skipping the
95+
// standard job queuing behavior.
96+
if (m_queue.empty()) {
97+
JS::JobQueueIsEmpty(cx);
98+
}
99+
100+
JSAutoRealm ar{cx, job};
101+
if (!JS::Call(cx, JS::UndefinedHandleValue, job,
102+
JS::HandleValueArray::empty(), &unused_rval)) {
103+
// We can't throw the exception here, because there is nowhere to
104+
// catch it. So, log it.
105+
LogPendingException(cx);
106+
}
107+
}
108+
109+
// FinalizationRegistry callbacks may queue more jobs, so only stop
110+
// running jobs if there were no FinalizationRegistry callbacks to run.
111+
if (!maybeRunFinalizationRegistryCallbacks(cx)) break;
112+
}
113+
114+
m_draining = false;
115+
m_queue.clear();
116+
}
117+
118+
// JS::JobQueue override
119+
bool empty() const override { return m_queue.empty(); }
120+
121+
void queueFinalizationRegistryCallback(JSFunction* callback) {
122+
mozilla::Unused << m_finalizationRegistryCallbacks.append(callback);
123+
}
124+
125+
private:
126+
using JobQueueStorage = JS::GCVector<JSObject*, 0, js::SystemAllocPolicy>;
127+
JS::PersistentRooted<JobQueueStorage> m_queue;
128+
129+
using FunctionVector = JS::GCVector<JSFunction*, 0, js::SystemAllocPolicy>;
130+
JS::PersistentRooted<FunctionVector> m_finalizationRegistryCallbacks;
131+
132+
// True if we are in the midst of draining jobs from this queue. We use this
133+
// to avoid re-entry (nested calls simply return immediately).
134+
bool m_draining : 1;
135+
136+
class SavedQueue : public JobQueue::SavedJobQueue {
137+
public:
138+
SavedQueue(JSContext* cx, CustomJobQueue* jobQueue)
139+
: m_jobQueue(jobQueue),
140+
m_saved(cx, std::move(jobQueue->m_queue.get())),
141+
m_draining(jobQueue->m_draining) {}
142+
143+
~SavedQueue() {
144+
m_jobQueue->m_queue = std::move(m_saved.get());
145+
m_jobQueue->m_draining = m_draining;
146+
}
147+
148+
private:
149+
CustomJobQueue* m_jobQueue;
150+
JS::PersistentRooted<JobQueueStorage> m_saved;
151+
bool m_draining : 1;
152+
};
153+
154+
// JS::JobQueue override
155+
js::UniquePtr<JS::JobQueue::SavedJobQueue> saveJobQueue(
156+
JSContext* cx) override {
157+
auto saved = js::MakeUnique<SavedQueue>(cx, this);
158+
if (!saved) {
159+
// When MakeUnique's allocation fails, the SavedQueue constructor is never
160+
// called, so this->queue is still initialized. (The move doesn't occur
161+
// until the constructor gets called.)
162+
JS_ReportOutOfMemory(cx);
163+
return nullptr;
164+
}
165+
166+
m_queue.clear();
167+
m_draining = false;
168+
return saved;
169+
}
170+
171+
bool maybeRunFinalizationRegistryCallbacks(JSContext* cx) {
172+
bool ranCallbacks = false;
173+
174+
JS::Rooted<FunctionVector> callbacks{cx};
175+
std::swap(callbacks.get(), m_finalizationRegistryCallbacks.get());
176+
for (JSFunction* f : callbacks) {
177+
JS::ExposeObjectToActiveJS(JS_GetFunctionObject(f));
178+
179+
JSAutoRealm ar{cx, JS_GetFunctionObject(f)};
180+
JS::Rooted<JSFunction*> func{cx, f};
181+
JS::Rooted<JS::Value> unused_rval{cx};
182+
if (!JS_CallFunction(cx, nullptr, func, JS::HandleValueArray::empty(),
183+
&unused_rval)) {
184+
LogPendingException(cx);
185+
}
186+
187+
ranCallbacks = true;
188+
}
189+
190+
return ranCallbacks;
191+
}
192+
};
193+
194+
static void CleanupFinalizationRegistry(JSFunction* callback,
195+
JSObject* incumbent_global
196+
[[maybe_unused]],
197+
void* user_data) {
198+
// Queue a cleanup task to run after each job has been run.
199+
// We only have one global so ignore the incumbent global parameter.
200+
auto* jobQueue = static_cast<CustomJobQueue*>(user_data);
201+
jobQueue->queueFinalizationRegistryCallback(callback);
202+
}
203+
204+
static bool GC(JSContext* cx, unsigned argc, JS::Value* vp) {
205+
JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
206+
207+
JS_GC(cx, JS::GCReason::API);
208+
209+
args.rval().setUndefined();
210+
return true;
211+
}
212+
213+
static bool RunJobs(JSContext* cx, unsigned argc, JS::Value* vp) {
214+
JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
215+
216+
// This calls JS::ClearKeptObjects() after draining the job queue. If you're
217+
// not using js::RunJobs(), you'll have to call it yourself -- otherwise, the
218+
// WeakRefs will never be emptied.
219+
js::RunJobs(cx);
220+
221+
args.rval().setUndefined();
222+
return true;
223+
}
224+
225+
static bool ExecuteCode(JSContext* cx, const char* code) {
226+
JS::CompileOptions options{cx};
227+
options.setFileAndLine("noname", 1);
228+
229+
JS::SourceText<mozilla::Utf8Unit> source;
230+
if (!source.init(cx, code, strlen(code), JS::SourceOwnership::Borrowed)) {
231+
return false;
232+
}
233+
234+
JS::Rooted<JS::Value> rval{cx};
235+
return JS::Evaluate(cx, options, source, &rval);
236+
}
237+
238+
static bool WeakRefExample(JSContext* cx) {
239+
// Using WeakRefs and FinalizationRegistry requires a job queue. The built-in
240+
// job queue used in repl.cpp is not sufficient, because it does not provide
241+
// any way to queue FinalizationRegistry cleanup callbacks.
242+
CustomJobQueue jobQueue{cx};
243+
JS::SetJobQueue(cx, &jobQueue);
244+
245+
// Without this, FinalizationRegistry callbacks will never be called. The
246+
// embedding has to decide when to schedule them.
247+
JS::SetHostCleanupFinalizationRegistryCallback(
248+
cx, CleanupFinalizationRegistry, &jobQueue);
249+
250+
JS::RealmOptions options;
251+
options.creationOptions().setWeakRefsEnabled(
252+
JS::WeakRefSpecifier::EnabledWithoutCleanupSome);
253+
254+
static JSClass GlobalClass = {"WeakRefsGlobal", JSCLASS_GLOBAL_FLAGS,
255+
&JS::DefaultGlobalClassOps};
256+
257+
JS::Rooted<JSObject*> global{
258+
cx, JS_NewGlobalObject(cx, &GlobalClass, nullptr, JS::FireOnNewGlobalHook,
259+
options)};
260+
if (!global) return false;
261+
262+
JSAutoRealm ar{cx, global};
263+
264+
if (!JS_DefineFunction(cx, global, "gc", &GC, 0, 0) ||
265+
!JS_DefineFunction(cx, global, "runJobs", &RunJobs, 0, 0)) {
266+
boilerplate::ReportAndClearException(cx);
267+
return false;
268+
}
269+
270+
if (!ExecuteCode(cx, R"js(
271+
let valueFinalized;
272+
const registry = new FinalizationRegistry(
273+
heldValue => (valueFinalized = heldValue));
274+
let obj = {};
275+
const weakRef = new WeakRef(obj);
276+
registry.register(obj, "marker");
277+
278+
obj = null;
279+
280+
runJobs(); // Makes weakRef eligible for clearing
281+
gc(); // Clears weakRef, collects obj which is no longer live, and
282+
// enqueues finalization registry cleanup
283+
284+
if (weakRef.deref() !== undefined) throw new Error("WeakRef");
285+
286+
runJobs(); // Runs finalization registry cleanup
287+
288+
if (valueFinalized !== "marker") throw new Error("FinalizationRegistry");
289+
)js")) {
290+
boilerplate::ReportAndClearException(cx);
291+
return false;
292+
}
293+
294+
return true;
295+
}
296+
297+
int main(int argc, const char* argv[]) {
298+
if (!boilerplate::RunExample(WeakRefExample)) {
299+
return 1;
300+
}
301+
return 0;
302+
}

meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,5 @@ executable('repl', 'examples/repl.cpp', 'examples/boilerplate.cpp', dependencies
7171
executable('tracing', 'examples/tracing.cpp', 'examples/boilerplate.cpp', dependencies: spidermonkey)
7272
executable('resolve', 'examples/resolve.cpp', 'examples/boilerplate.cpp', dependencies: [spidermonkey, zlib])
7373
executable('modules', 'examples/modules.cpp', 'examples/boilerplate.cpp', dependencies: [spidermonkey])
74+
executable('weakref', 'examples/weakref.cpp', 'examples/boilerplate.cpp', dependencies: spidermonkey)
7475
executable('worker', 'examples/worker.cpp', 'examples/boilerplate.cpp', dependencies: spidermonkey)

0 commit comments

Comments
 (0)