Skip to content

Commit ae802e2

Browse files
committed
feat: implement ES2021 WeakRef and FinalizationRegistry
- Add NativeWeakRef implementation with deref() method - Add NativeFinalizationRegistry with register(), unregister(), and cleanupSome() - Enable and update test262 test suites for both features - Test results: 102,862 tests completed, 64 failed (99% success rate)
1 parent 0c5f5be commit ae802e2

File tree

9 files changed

+1453
-5
lines changed

9 files changed

+1453
-5
lines changed
Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
2+
*
3+
* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6+
7+
package org.mozilla.javascript;
8+
9+
import java.lang.ref.ReferenceQueue;
10+
import java.lang.ref.WeakReference;
11+
import java.util.Set;
12+
import java.util.concurrent.ConcurrentHashMap;
13+
14+
/**
15+
* Implementation of the ES2021 FinalizationRegistry constructor and prototype.
16+
*
17+
* <p>FinalizationRegistry allows registering cleanup callbacks to be called when objects are
18+
* garbage collected. This is useful for resource cleanup, cache management, or any scenario where
19+
* you need to perform cleanup when objects are no longer reachable.
20+
*
21+
* <p>The FinalizationRegistry object provides two methods: register() to register an object for
22+
* cleanup, and unregister() to remove a registration using a token.
23+
*
24+
* @see <a href="https://tc39.es/ecma262/#sec-finalization-registry-objects">ECMAScript
25+
* FinalizationRegistry Objects</a>
26+
*/
27+
public class NativeFinalizationRegistry extends ScriptableObject {
28+
private static final long serialVersionUID = 1L;
29+
private static final String CLASS_NAME = "FinalizationRegistry";
30+
31+
private final Function cleanupCallback;
32+
private final ReferenceQueue<Scriptable> referenceQueue;
33+
private final ConcurrentHashMap<FinalizationWeakReference, RegistrationRecord> registrations;
34+
private final ConcurrentHashMap<Object, Set<FinalizationWeakReference>> tokenMap;
35+
36+
private class FinalizationWeakReference extends WeakReference<Scriptable> {
37+
FinalizationWeakReference(Scriptable referent) {
38+
super(referent, referenceQueue);
39+
}
40+
}
41+
42+
private static class RegistrationRecord {
43+
final Object heldValue;
44+
final Object unregisterToken;
45+
46+
RegistrationRecord(Object heldValue, Object unregisterToken) {
47+
this.heldValue = heldValue;
48+
this.unregisterToken = unregisterToken;
49+
}
50+
}
51+
52+
public static Object init(Context cx, Scriptable scope, boolean sealed) {
53+
LambdaConstructor constructor = createConstructor(scope);
54+
configurePrototype(constructor, scope);
55+
56+
if (sealed) {
57+
sealConstructor(constructor);
58+
}
59+
return constructor;
60+
}
61+
62+
private static LambdaConstructor createConstructor(Scriptable scope) {
63+
LambdaConstructor constructor =
64+
new LambdaConstructor(
65+
scope,
66+
CLASS_NAME,
67+
1,
68+
LambdaConstructor.CONSTRUCTOR_NEW,
69+
NativeFinalizationRegistry::constructor);
70+
constructor.setPrototypePropertyAttributes(DONTENUM | READONLY | PERMANENT);
71+
return constructor;
72+
}
73+
74+
private static void configurePrototype(LambdaConstructor constructor, Scriptable scope) {
75+
constructor.definePrototypeMethod(
76+
scope,
77+
"register",
78+
2,
79+
NativeFinalizationRegistry::register,
80+
DONTENUM,
81+
DONTENUM | READONLY);
82+
83+
constructor.definePrototypeMethod(
84+
scope,
85+
"unregister",
86+
1,
87+
NativeFinalizationRegistry::unregister,
88+
DONTENUM,
89+
DONTENUM | READONLY);
90+
91+
// cleanupSome - Optional method for synchronous cleanup
92+
// Not exposed in browser environments per spec, but useful for server-side
93+
constructor.definePrototypeMethod(
94+
scope,
95+
"cleanupSome",
96+
0,
97+
NativeFinalizationRegistry::cleanupSome,
98+
DONTENUM,
99+
DONTENUM | READONLY);
100+
101+
constructor.definePrototypeProperty(
102+
SymbolKey.TO_STRING_TAG, CLASS_NAME, DONTENUM | READONLY);
103+
}
104+
105+
private static void sealConstructor(LambdaConstructor constructor) {
106+
constructor.sealObject();
107+
ScriptableObject prototype = (ScriptableObject) constructor.getPrototypeProperty();
108+
if (prototype != null) {
109+
prototype.sealObject();
110+
}
111+
}
112+
113+
private static Scriptable constructor(Context cx, Scriptable scope, Object[] args) {
114+
if (args.length < 1 || !(args[0] instanceof Function)) {
115+
throw ScriptRuntime.typeErrorById("msg.finalization.registry.no.callback");
116+
}
117+
return new NativeFinalizationRegistry((Function) args[0]);
118+
}
119+
120+
private NativeFinalizationRegistry(Function cleanupCallback) {
121+
this.cleanupCallback = cleanupCallback;
122+
this.referenceQueue = new ReferenceQueue<>();
123+
this.registrations = new ConcurrentHashMap<>();
124+
this.tokenMap = new ConcurrentHashMap<>();
125+
}
126+
127+
private static Object register(
128+
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
129+
NativeFinalizationRegistry registry = ensureFinalizationRegistry(thisObj);
130+
return registry.registerTarget(args);
131+
}
132+
133+
private static Object unregister(
134+
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
135+
NativeFinalizationRegistry registry = ensureFinalizationRegistry(thisObj);
136+
return registry.unregisterToken(args);
137+
}
138+
139+
private static Object cleanupSome(
140+
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
141+
NativeFinalizationRegistry registry = ensureFinalizationRegistry(thisObj);
142+
143+
Function callback = null;
144+
if (args.length > 0) {
145+
if (!Undefined.isUndefined(args[0]) && !(args[0] instanceof Function)) {
146+
throw ScriptRuntime.typeErrorById(
147+
"msg.isnt.function",
148+
ScriptRuntime.toString(args[0]),
149+
ScriptRuntime.typeof(args[0]));
150+
}
151+
if (args[0] instanceof Function) {
152+
callback = (Function) args[0];
153+
}
154+
}
155+
156+
registry.performCleanupSome(cx, callback);
157+
return Undefined.instance;
158+
}
159+
160+
private static NativeFinalizationRegistry ensureFinalizationRegistry(Scriptable thisObj) {
161+
return LambdaConstructor.convertThisObject(thisObj, NativeFinalizationRegistry.class);
162+
}
163+
164+
private Object registerTarget(Object[] args) {
165+
if (args.length < 2) {
166+
throw ScriptRuntime.typeErrorById("msg.finalization.registry.register.not.object");
167+
}
168+
169+
Object target = args[0];
170+
Object heldValue = args[1];
171+
Object unregisterToken =
172+
args.length > 2 && !Undefined.isUndefined(args[2]) ? args[2] : null;
173+
174+
// Per spec: target must be an object (not a symbol)
175+
if (!isValidTarget(target)) {
176+
throw ScriptRuntime.typeErrorById("msg.finalization.registry.register.not.object");
177+
}
178+
179+
// Per spec: if unregisterToken is provided, it must be able to be held weakly
180+
if (unregisterToken != null && !canBeHeldWeakly(unregisterToken)) {
181+
throw ScriptRuntime.typeErrorById("msg.finalization.registry.register.not.object");
182+
}
183+
184+
// Per spec: SameValue(target, heldValue) must be false
185+
if (ScriptRuntime.same(target, heldValue)) {
186+
throw ScriptRuntime.typeErrorById("msg.finalization.registry.register.same.target");
187+
}
188+
189+
// Process any pending cleanups before registering new ones
190+
processPendingCleanups();
191+
192+
Scriptable scriptableTarget = (Scriptable) target;
193+
FinalizationWeakReference weakRef = new FinalizationWeakReference(scriptableTarget);
194+
RegistrationRecord record = new RegistrationRecord(heldValue, unregisterToken);
195+
196+
registrations.put(weakRef, record);
197+
198+
if (unregisterToken != null) {
199+
tokenMap.computeIfAbsent(unregisterToken, k -> ConcurrentHashMap.newKeySet())
200+
.add(weakRef);
201+
}
202+
203+
return Undefined.instance;
204+
}
205+
206+
private Object unregisterToken(Object[] args) {
207+
// Per spec, if no arguments return false
208+
if (args.length < 1 || Undefined.isUndefined(args[0])) {
209+
return Boolean.FALSE;
210+
}
211+
212+
Object token = args[0];
213+
214+
// Per spec, check if token can be held weakly
215+
if (!canBeHeldWeakly(token)) {
216+
return Boolean.FALSE;
217+
}
218+
219+
Set<FinalizationWeakReference> refs = tokenMap.remove(token);
220+
221+
if (refs == null || refs.isEmpty()) {
222+
return Boolean.FALSE;
223+
}
224+
225+
boolean unregistered = false;
226+
for (FinalizationWeakReference ref : refs) {
227+
if (registrations.remove(ref) != null) {
228+
unregistered = true;
229+
}
230+
}
231+
232+
return Boolean.valueOf(unregistered);
233+
}
234+
235+
private void processPendingCleanups() {
236+
@SuppressWarnings("unchecked")
237+
FinalizationWeakReference ref;
238+
while ((ref = (FinalizationWeakReference) referenceQueue.poll()) != null) {
239+
processCleanup(ref);
240+
}
241+
}
242+
243+
private void processCleanup(FinalizationWeakReference ref) {
244+
// Remove registration record and get cleanup data atomically
245+
RegistrationRecord record = registrations.remove(ref);
246+
if (record == null) {
247+
return;
248+
}
249+
250+
// Remove from token map atomically to prevent race conditions
251+
if (record.unregisterToken != null) {
252+
tokenMap.computeIfPresent(
253+
record.unregisterToken,
254+
(token, refs) -> {
255+
refs.remove(ref);
256+
return refs.isEmpty() ? null : refs;
257+
});
258+
}
259+
260+
executeCleanupCallback(record.heldValue);
261+
}
262+
263+
private void performCleanupSome(Context cx, Function callback) {
264+
Function callbackToUse = (callback != null) ? callback : this.cleanupCallback;
265+
if (callbackToUse == null) {
266+
return;
267+
}
268+
269+
int cleanupCount = 0;
270+
int maxCleanups = 100;
271+
272+
@SuppressWarnings("unchecked")
273+
FinalizationWeakReference ref;
274+
while (cleanupCount < maxCleanups
275+
&& (ref = (FinalizationWeakReference) referenceQueue.poll()) != null) {
276+
RegistrationRecord record = registrations.remove(ref);
277+
if (record != null) {
278+
if (record.unregisterToken != null) {
279+
final FinalizationWeakReference finalRef = ref;
280+
tokenMap.computeIfPresent(
281+
record.unregisterToken,
282+
(token, refs) -> {
283+
refs.remove(finalRef);
284+
return refs.isEmpty() ? null : refs;
285+
});
286+
}
287+
288+
try {
289+
Scriptable scope = callbackToUse.getParentScope();
290+
callbackToUse.call(cx, scope, scope, new Object[] {record.heldValue});
291+
cleanupCount++;
292+
} catch (RhinoException e) {
293+
Context.reportWarning(
294+
"FinalizationRegistry cleanup callback error: " + e.getMessage());
295+
}
296+
}
297+
}
298+
}
299+
300+
private void executeCleanupCallback(Object heldValue) {
301+
if (cleanupCallback == null) {
302+
return;
303+
}
304+
305+
Context cx = Context.getCurrentContext();
306+
if (cx == null) {
307+
try (Context enteredCx = Context.enter()) {
308+
callCleanupCallback(enteredCx, heldValue);
309+
}
310+
} else {
311+
callCleanupCallback(cx, heldValue);
312+
}
313+
}
314+
315+
private void callCleanupCallback(Context cx, Object heldValue) {
316+
try {
317+
Scriptable scope = cleanupCallback.getParentScope();
318+
cleanupCallback.call(cx, scope, scope, new Object[] {heldValue});
319+
} catch (RhinoException e) {
320+
Context.reportWarning("FinalizationRegistry cleanup callback error: " + e.getMessage());
321+
}
322+
}
323+
324+
private static boolean isValidTarget(Object target) {
325+
return ScriptRuntime.isObject(target);
326+
}
327+
328+
private static boolean canBeHeldWeakly(Object value) {
329+
// ES2021: Only objects can be held weakly as unregister tokens
330+
return ScriptRuntime.isObject(value);
331+
}
332+
333+
@Override
334+
public String getClassName() {
335+
return CLASS_NAME;
336+
}
337+
}

0 commit comments

Comments
 (0)