Skip to content

Commit 11cb533

Browse files
committed
Implement ES2021 FinalizationRegistry and WeakRef
Implements FinalizationRegistry and WeakRef as defined in ES2021 spec. FinalizationRegistry: - Full implementation of register() and unregister() methods - Cleanup callbacks triggered on garbage collection - Token-based unregistration support - Comprehensive error handling and validation - White-box tests for GC-dependent cleanup methods WeakRef: - Complete implementation with deref() method - Proper weak reference semantics - Integration with JavaScript garbage collection Changes: - Add NativeFinalizationRegistry and NativeWeakRef classes - Initialize both in ScriptRuntime for ES6+ - Add error messages for validation - Add comprehensive test coverage including reflection-based tests - Update test262.properties with auto-generated format using -DupdateTest262properties=~all Test Status: - 11 failing test262 tests for FinalizationRegistry edge cases and cross-realm behavior - Core functionality fully working - cleanupSome() method not implemented (optional feature) Based on review feedback from @gbrail with all comments addressed.
1 parent 0c5f5be commit 11cb533

File tree

8 files changed

+2933
-3386
lines changed

8 files changed

+2933
-3386
lines changed
Lines changed: 355 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
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+
/** Custom WeakReference that can trigger cleanup when the referent is collected. */
37+
private class FinalizationWeakReference extends WeakReference<Scriptable> {
38+
FinalizationWeakReference(Scriptable referent) {
39+
super(referent, referenceQueue);
40+
}
41+
}
42+
43+
/** Internal record holding cleanup data for a registration. */
44+
private static class RegistrationRecord {
45+
final Object heldValue;
46+
final Object unregisterToken;
47+
48+
RegistrationRecord(Object heldValue, Object unregisterToken) {
49+
this.heldValue = heldValue;
50+
this.unregisterToken = unregisterToken;
51+
}
52+
}
53+
54+
/**
55+
* Initializes the FinalizationRegistry constructor and prototype in the given scope.
56+
*
57+
* @param cx the JavaScript context
58+
* @param scope the scope to initialize in
59+
* @param sealed whether to seal the constructor and prototype
60+
* @return the FinalizationRegistry constructor
61+
*/
62+
public static Object init(Context cx, Scriptable scope, boolean sealed) {
63+
LambdaConstructor constructor = createConstructor(scope);
64+
configurePrototype(constructor, scope);
65+
66+
if (sealed) {
67+
sealConstructor(constructor);
68+
}
69+
return constructor;
70+
}
71+
72+
private static LambdaConstructor createConstructor(Scriptable scope) {
73+
LambdaConstructor constructor =
74+
new LambdaConstructor(
75+
scope,
76+
CLASS_NAME,
77+
1,
78+
LambdaConstructor.CONSTRUCTOR_NEW,
79+
NativeFinalizationRegistry::constructor);
80+
constructor.setPrototypePropertyAttributes(DONTENUM | READONLY | PERMANENT);
81+
return constructor;
82+
}
83+
84+
private static void configurePrototype(LambdaConstructor constructor, Scriptable scope) {
85+
// Define prototype methods
86+
constructor.definePrototypeMethod(
87+
scope,
88+
"register",
89+
2,
90+
NativeFinalizationRegistry::register,
91+
DONTENUM,
92+
DONTENUM | READONLY);
93+
94+
constructor.definePrototypeMethod(
95+
scope,
96+
"unregister",
97+
1,
98+
NativeFinalizationRegistry::unregister,
99+
DONTENUM,
100+
DONTENUM | READONLY);
101+
102+
// Define Symbol.toStringTag
103+
constructor.definePrototypeProperty(
104+
SymbolKey.TO_STRING_TAG, CLASS_NAME, DONTENUM | READONLY);
105+
}
106+
107+
private static void sealConstructor(LambdaConstructor constructor) {
108+
constructor.sealObject();
109+
ScriptableObject prototype = (ScriptableObject) constructor.getPrototypeProperty();
110+
if (prototype != null) {
111+
prototype.sealObject();
112+
}
113+
}
114+
115+
/**
116+
* FinalizationRegistry constructor implementation. Creates a new FinalizationRegistry instance
117+
* with the given cleanup callback.
118+
*
119+
* @param cx the current context
120+
* @param scope the scope
121+
* @param args constructor arguments, expects exactly one function argument
122+
* @return the new FinalizationRegistry instance
123+
* @throws TypeError if no argument provided or argument is not a function
124+
*/
125+
private static Scriptable constructor(Context cx, Scriptable scope, Object[] args) {
126+
validateConstructorArgs(args);
127+
return createFinalizationRegistry((Function) args[0]);
128+
}
129+
130+
private static NativeFinalizationRegistry createFinalizationRegistry(Function cleanupCallback) {
131+
return new NativeFinalizationRegistry(cleanupCallback);
132+
}
133+
134+
/**
135+
* Validates constructor arguments according to ES2021 spec.
136+
*
137+
* @param args the constructor arguments
138+
* @throws TypeError if validation fails
139+
*/
140+
private static void validateConstructorArgs(Object[] args) {
141+
if (args.length < 1 || !(args[0] instanceof Function)) {
142+
throw ScriptRuntime.typeErrorById("msg.finalization.registry.no.callback");
143+
}
144+
}
145+
146+
/**
147+
* Private constructor that initializes the registry with a cleanup callback.
148+
*
149+
* @param cleanupCallback the function to call when objects are collected
150+
*/
151+
private NativeFinalizationRegistry(Function cleanupCallback) {
152+
this.cleanupCallback = cleanupCallback;
153+
this.referenceQueue = new ReferenceQueue<>();
154+
this.registrations = new ConcurrentHashMap<>();
155+
this.tokenMap = new ConcurrentHashMap<>();
156+
}
157+
158+
/**
159+
* FinalizationRegistry.prototype.register() implementation. Registers an object for cleanup
160+
* when it's garbage collected.
161+
*
162+
* @param cx the current context
163+
* @param scope the scope
164+
* @param thisObj the 'this' object
165+
* @param args method arguments: target, heldValue, [unregisterToken]
166+
* @return undefined
167+
*/
168+
private static Object register(
169+
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
170+
NativeFinalizationRegistry registry = ensureFinalizationRegistry(thisObj);
171+
return registry.registerTarget(args);
172+
}
173+
174+
/**
175+
* FinalizationRegistry.prototype.unregister() implementation. Unregisters all registrations
176+
* associated with the given token.
177+
*
178+
* @param cx the current context
179+
* @param scope the scope
180+
* @param thisObj the 'this' object
181+
* @param args method arguments: unregisterToken
182+
* @return boolean indicating if any registrations were removed
183+
*/
184+
private static Object unregister(
185+
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
186+
NativeFinalizationRegistry registry = ensureFinalizationRegistry(thisObj);
187+
return registry.unregisterToken(args);
188+
}
189+
190+
private static NativeFinalizationRegistry ensureFinalizationRegistry(Scriptable thisObj) {
191+
return LambdaConstructor.convertThisObject(thisObj, NativeFinalizationRegistry.class);
192+
}
193+
194+
/**
195+
* Registers a target object for cleanup.
196+
*
197+
* @param args the method arguments
198+
* @return Undefined.instance
199+
*/
200+
private Object registerTarget(Object[] args) {
201+
if (args.length < 2) {
202+
throw ScriptRuntime.typeErrorById("msg.finalization.registry.register.not.object");
203+
}
204+
205+
Object target = args[0];
206+
Object heldValue = args[1];
207+
Object unregisterToken = args.length > 2 ? args[2] : null;
208+
209+
if (!isValidTarget(target)) {
210+
throw ScriptRuntime.typeErrorById("msg.finalization.registry.register.not.object");
211+
}
212+
213+
if (target == heldValue) {
214+
throw ScriptRuntime.typeErrorById("msg.finalization.registry.register.same.target");
215+
}
216+
217+
// Process any pending cleanups before registering new ones
218+
processPendingCleanups();
219+
220+
Scriptable scriptableTarget = (Scriptable) target;
221+
FinalizationWeakReference weakRef = new FinalizationWeakReference(scriptableTarget);
222+
RegistrationRecord record = new RegistrationRecord(heldValue, unregisterToken);
223+
224+
registrations.put(weakRef, record);
225+
226+
if (unregisterToken != null) {
227+
tokenMap.computeIfAbsent(unregisterToken, k -> ConcurrentHashMap.newKeySet())
228+
.add(weakRef);
229+
}
230+
231+
return Undefined.instance;
232+
}
233+
234+
/**
235+
* Unregisters all registrations associated with the given token.
236+
*
237+
* @param args the method arguments
238+
* @return Boolean indicating if any registrations were removed
239+
*/
240+
private Object unregisterToken(Object[] args) {
241+
if (args.length < 1) {
242+
return Boolean.FALSE;
243+
}
244+
245+
Object token = args[0];
246+
Set<FinalizationWeakReference> refs = tokenMap.remove(token);
247+
248+
if (refs == null || refs.isEmpty()) {
249+
return Boolean.FALSE;
250+
}
251+
252+
boolean unregistered = false;
253+
for (FinalizationWeakReference ref : refs) {
254+
if (registrations.remove(ref) != null) {
255+
unregistered = true;
256+
}
257+
}
258+
259+
return unregistered ? Boolean.TRUE : Boolean.FALSE;
260+
}
261+
262+
/**
263+
* Processes pending cleanup callbacks from the reference queue. This method should be called
264+
* periodically to ensure timely cleanup callback execution.
265+
*/
266+
private void processPendingCleanups() {
267+
@SuppressWarnings("unchecked")
268+
FinalizationWeakReference ref;
269+
while ((ref = (FinalizationWeakReference) referenceQueue.poll()) != null) {
270+
processCleanup(ref);
271+
}
272+
}
273+
274+
/**
275+
* Processes cleanup for a specific weak reference.
276+
*
277+
* @param ref the weak reference to process
278+
*/
279+
private void processCleanup(FinalizationWeakReference ref) {
280+
// Remove registration record and get cleanup data atomically
281+
RegistrationRecord record = registrations.remove(ref);
282+
if (record == null) {
283+
return; // Already processed or unregistered
284+
}
285+
286+
// Remove from token map atomically to prevent race conditions
287+
if (record.unregisterToken != null) {
288+
tokenMap.computeIfPresent(
289+
record.unregisterToken,
290+
(token, refs) -> {
291+
refs.remove(ref);
292+
return refs.isEmpty() ? null : refs;
293+
});
294+
}
295+
296+
// Call the cleanup callback with proper error handling
297+
executeCleanupCallback(record.heldValue);
298+
}
299+
300+
/**
301+
* Executes the cleanup callback with proper Context management and error handling.
302+
*
303+
* @param heldValue the held value to pass to the callback
304+
*/
305+
private void executeCleanupCallback(Object heldValue) {
306+
if (cleanupCallback == null) {
307+
return;
308+
}
309+
310+
Context cx = Context.getCurrentContext();
311+
if (cx == null) {
312+
try (Context enteredCx = Context.enter()) {
313+
callCleanupCallback(enteredCx, heldValue);
314+
}
315+
} else {
316+
callCleanupCallback(cx, heldValue);
317+
}
318+
}
319+
320+
/**
321+
* Calls the cleanup callback with the held value.
322+
*
323+
* @param cx the context to use
324+
* @param heldValue the held value to pass to the callback
325+
*/
326+
private void callCleanupCallback(Context cx, Object heldValue) {
327+
try {
328+
Scriptable scope = cleanupCallback.getParentScope();
329+
cleanupCallback.call(cx, scope, scope, new Object[] {heldValue});
330+
} catch (RhinoException e) {
331+
// Report cleanup errors through Rhino's standard error reporting
332+
// while not breaking the cleanup process
333+
Context.reportWarning("FinalizationRegistry cleanup callback error: " + e.getMessage());
334+
}
335+
}
336+
337+
/**
338+
* Checks if a value is a valid FinalizationRegistry target. According to the spec, only objects
339+
* (excluding null, undefined, and symbols) can be registered.
340+
*
341+
* @param target the value to check
342+
* @return true if the target is a valid object reference
343+
*/
344+
private static boolean isValidTarget(Object target) {
345+
return target instanceof Scriptable
346+
&& target != Undefined.instance
347+
&& target != null
348+
&& !(target instanceof Symbol);
349+
}
350+
351+
@Override
352+
public String getClassName() {
353+
return CLASS_NAME;
354+
}
355+
}

0 commit comments

Comments
 (0)