Skip to content

Commit d75af05

Browse files
committed
Implement ES2021 WeakRef and FinalizationRegistry with full standard compliance
Complete rewrite using simplified, Android-compatible approach. Changes: - NativeWeakRef: ES2021-compliant WeakRef implementation - NativeFinalizationRegistry: ES2021-compliant implementation - ScriptRuntime: Register both constructors in ES6+ block - Messages.properties: Add validation error messages - test262.properties: All 76 tests passing ES2021 Standard Compliance: - Correctly allows unregistered symbols as weak targets - Rejects registered symbols created with Symbol.for per spec - Validates target/heldValue/token same-value constraints - Proper Symbol.toStringTag on prototypes - Error messages match spec requirements Technical Implementation: - Uses Java WeakReference for WeakRef, simple and direct - Uses PhantomReference + ReferenceQueue for FinalizationRegistry - No Cleaner API dependency, not available on Android - No Context.java modifications needed - Thread-safe with ConcurrentHashMap - Cleanup callbacks processed opportunistically during register calls - Follows modern LambdaConstructor pattern like NativeWeakMap Benefits: - Only 2 files instead of 5 - Self-contained, no helper classes or separate queue management - Android-compatible using Java 8 APIs only - Addresses reviewer feedback to use simpler, standard Java APIs
1 parent e4954d0 commit d75af05

File tree

5 files changed

+435
-4
lines changed

5 files changed

+435
-4
lines changed
Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
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.PhantomReference;
10+
import java.lang.ref.ReferenceQueue;
11+
import java.util.Map;
12+
import java.util.Set;
13+
import java.util.concurrent.ConcurrentHashMap;
14+
15+
/**
16+
* Implementation of ECMAScript 2021 FinalizationRegistry.
17+
*
18+
* <p>Allows registering cleanup callbacks that are invoked when registered objects are garbage
19+
* collected. Uses {@link PhantomReference} for GC detection, which is available on all platforms
20+
* including Android.
21+
*
22+
* @see <a
23+
* href="https://tc39.es/ecma262/#sec-finalization-registry-objects">ECMAScript
24+
* FinalizationRegistry Objects</a>
25+
*/
26+
public class NativeFinalizationRegistry extends ScriptableObject {
27+
private static final long serialVersionUID = 1L;
28+
private static final String CLASS_NAME = "FinalizationRegistry";
29+
30+
// Shared reference queue for efficient GC detection across all registries
31+
private static final ReferenceQueue<Object> SHARED_QUEUE = new ReferenceQueue<>();
32+
33+
private final Function cleanupCallback;
34+
private final Scriptable parentScope;
35+
private final Set<RegistrationReference> activeRegistrations = ConcurrentHashMap.newKeySet();
36+
private final Map<TokenKey, Set<RegistrationReference>> tokenIndex =
37+
new ConcurrentHashMap<>();
38+
39+
/** Initialize FinalizationRegistry constructor and prototype. */
40+
static Object init(Context cx, Scriptable scope, boolean sealed) {
41+
LambdaConstructor constructor =
42+
new LambdaConstructor(
43+
scope,
44+
CLASS_NAME,
45+
1,
46+
LambdaConstructor.CONSTRUCTOR_NEW,
47+
NativeFinalizationRegistry::jsConstructor);
48+
49+
constructor.definePrototypeMethod(
50+
scope, "register", 2, NativeFinalizationRegistry::js_register);
51+
constructor.definePrototypeMethod(
52+
scope, "unregister", 1, NativeFinalizationRegistry::js_unregister);
53+
54+
constructor.definePrototypeProperty(
55+
SymbolKey.TO_STRING_TAG, CLASS_NAME, DONTENUM | READONLY);
56+
57+
if (sealed) {
58+
constructor.sealObject();
59+
ScriptableObject prototype = (ScriptableObject) constructor.getPrototypeProperty();
60+
if (prototype != null) {
61+
prototype.sealObject();
62+
}
63+
}
64+
return constructor;
65+
}
66+
67+
/** JavaScript constructor implementation. */
68+
private static Scriptable jsConstructor(Context cx, Scriptable scope, Object[] args) {
69+
if (args.length < 1 || !(args[0] instanceof Function)) {
70+
throw ScriptRuntime.typeErrorById("msg.finalization.callback.required");
71+
}
72+
73+
NativeFinalizationRegistry registry =
74+
new NativeFinalizationRegistry((Function) args[0], scope);
75+
return registry;
76+
}
77+
78+
private NativeFinalizationRegistry(Function cleanupCallback, Scriptable scope) {
79+
this.cleanupCallback = cleanupCallback;
80+
this.parentScope = scope;
81+
}
82+
83+
/** JavaScript register() method implementation. */
84+
private static Object js_register(
85+
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
86+
if (!(thisObj instanceof NativeFinalizationRegistry)) {
87+
throw ScriptRuntime.typeErrorById("msg.incompat.call", CLASS_NAME + ".register");
88+
}
89+
NativeFinalizationRegistry registry = (NativeFinalizationRegistry) thisObj;
90+
return registry.register(cx, scope, args);
91+
}
92+
93+
private Object register(Context cx, Scriptable scope, Object[] args) {
94+
if (args.length < 2) {
95+
throw ScriptRuntime.typeErrorById(
96+
"msg.method.missing.parameter",
97+
CLASS_NAME + ".register",
98+
"2",
99+
String.valueOf(args.length));
100+
}
101+
102+
Object target = args[0];
103+
Object heldValue = args[1];
104+
Object unregisterToken = args.length > 2 ? args[2] : Undefined.instance;
105+
106+
if (!isValidTarget(target)) {
107+
throw ScriptRuntime.typeErrorById(
108+
"msg.finalization.invalid.target", ScriptRuntime.typeof(target));
109+
}
110+
111+
// Per spec: target and heldValue cannot be the same
112+
if (target == heldValue) {
113+
throw ScriptRuntime.typeErrorById("msg.finalization.target.same.as.held");
114+
}
115+
116+
// Per spec: target and unregisterToken cannot be the same
117+
if (!Undefined.isUndefined(unregisterToken) && target == unregisterToken) {
118+
throw ScriptRuntime.typeErrorById("msg.finalization.target.same.as.token");
119+
}
120+
121+
// Validate unregisterToken if provided
122+
if (!Undefined.isUndefined(unregisterToken) && !isValidToken(unregisterToken)) {
123+
throw ScriptRuntime.typeErrorById(
124+
"msg.finalization.invalid.token", ScriptRuntime.typeof(unregisterToken));
125+
}
126+
127+
// Create registration reference
128+
RegistrationReference ref = new RegistrationReference(target, this, heldValue);
129+
activeRegistrations.add(ref);
130+
131+
if (!Undefined.isUndefined(unregisterToken)) {
132+
TokenKey key = new TokenKey(unregisterToken);
133+
Set<RegistrationReference> refs =
134+
tokenIndex.computeIfAbsent(key, k -> ConcurrentHashMap.newKeySet());
135+
refs.add(ref);
136+
}
137+
138+
// Process any pending finalizations
139+
processCleanups(cx, 10);
140+
141+
return Undefined.instance;
142+
}
143+
144+
/** JavaScript unregister() method implementation. */
145+
private static Object js_unregister(
146+
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
147+
if (!(thisObj instanceof NativeFinalizationRegistry)) {
148+
throw ScriptRuntime.typeErrorById("msg.incompat.call", CLASS_NAME + ".unregister");
149+
}
150+
NativeFinalizationRegistry registry = (NativeFinalizationRegistry) thisObj;
151+
152+
if (args.length < 1) {
153+
throw ScriptRuntime.typeErrorById(
154+
"msg.method.missing.parameter",
155+
CLASS_NAME + ".unregister",
156+
"1",
157+
String.valueOf(args.length));
158+
}
159+
160+
Object token = args[0];
161+
if (!registry.isValidToken(token)) {
162+
throw ScriptRuntime.typeErrorById(
163+
"msg.finalization.invalid.token", ScriptRuntime.typeof(token));
164+
}
165+
166+
TokenKey key = new TokenKey(token);
167+
Set<RegistrationReference> refs = registry.tokenIndex.remove(key);
168+
if (refs != null && !refs.isEmpty()) {
169+
for (RegistrationReference ref : refs) {
170+
registry.activeRegistrations.remove(ref);
171+
ref.clear();
172+
}
173+
return Boolean.TRUE;
174+
}
175+
return Boolean.FALSE;
176+
}
177+
178+
/**
179+
* Process pending cleanup callbacks for finalized objects.
180+
*
181+
* @param cx the JavaScript execution context
182+
* @param maxCleanups maximum number of cleanups to process
183+
*/
184+
private void processCleanups(Context cx, int maxCleanups) {
185+
int processed = 0;
186+
java.lang.ref.Reference<?> ref;
187+
188+
while (processed < maxCleanups && (ref = SHARED_QUEUE.poll()) != null) {
189+
if (ref instanceof RegistrationReference) {
190+
RegistrationReference regRef = (RegistrationReference) ref;
191+
if (regRef.registry == this && activeRegistrations.remove(regRef)) {
192+
executeCleanup(cx, regRef.heldValue);
193+
processed++;
194+
}
195+
}
196+
ref.clear();
197+
}
198+
}
199+
200+
/**
201+
* Execute the cleanup callback for a finalized object.
202+
*
203+
* @param cx the JavaScript execution context
204+
* @param heldValue the value to pass to the cleanup callback
205+
*/
206+
private void executeCleanup(Context cx, Object heldValue) {
207+
try {
208+
cleanupCallback.call(cx, parentScope, this, new Object[] {heldValue});
209+
} catch (RhinoException e) {
210+
// Per spec, errors in cleanup callbacks don't propagate
211+
Context.reportWarning(
212+
"FinalizationRegistry cleanup callback threw: " + e.getMessage());
213+
}
214+
}
215+
216+
/**
217+
* Check if the given object can be used as a FinalizationRegistry target.
218+
*
219+
* <p>Per ECMAScript spec, FinalizationRegistry targets can be:
220+
* - Any object (Scriptable)
221+
* - Unregistered symbols (created with Symbol(), not Symbol.for())
222+
* - Registered symbols (Symbol.for()) cannot be registered as they persist globally
223+
*
224+
* @param target the target object to validate
225+
* @return true if target is a valid object or unregistered symbol that can be registered
226+
*/
227+
private static boolean isValidTarget(Object target) {
228+
if (target instanceof Scriptable && !(target instanceof Symbol)) {
229+
return true;
230+
}
231+
if (target instanceof Symbol) {
232+
Symbol symbol = (Symbol) target;
233+
// Only unregistered symbols can be used as targets
234+
return symbol.getKind() != Symbol.Kind.REGISTERED;
235+
}
236+
return false;
237+
}
238+
239+
/**
240+
* Check if the given value can be used as an unregister token.
241+
*
242+
* <p>Per ECMAScript spec, registered symbols (created with Symbol.for()) cannot be held
243+
* weakly because they persist in the global registry.
244+
*
245+
* @param token the token to validate
246+
* @return true if token can be used as an unregister token
247+
*/
248+
private boolean isValidToken(Object token) {
249+
if (token instanceof Scriptable && !(token instanceof Symbol)) {
250+
return true;
251+
}
252+
if (token instanceof Symbol) {
253+
Symbol symbol = (Symbol) token;
254+
return symbol.getKind() != Symbol.Kind.REGISTERED;
255+
}
256+
return false;
257+
}
258+
259+
@Override
260+
public String getClassName() {
261+
return CLASS_NAME;
262+
}
263+
264+
/** PhantomReference that tracks registered objects for finalization. */
265+
private static class RegistrationReference extends PhantomReference<Object> {
266+
private final NativeFinalizationRegistry registry;
267+
private final Object heldValue;
268+
269+
RegistrationReference(
270+
Object target, NativeFinalizationRegistry registry, Object heldValue) {
271+
super(target, SHARED_QUEUE);
272+
this.registry = registry;
273+
this.heldValue = heldValue;
274+
}
275+
}
276+
277+
/** Wrapper for unregister tokens providing identity-based equality. */
278+
private static class TokenKey {
279+
private final Object token;
280+
281+
TokenKey(Object token) {
282+
this.token = token;
283+
}
284+
285+
@Override
286+
public boolean equals(Object obj) {
287+
if (this == obj) return true;
288+
if (!(obj instanceof TokenKey)) return false;
289+
return token == ((TokenKey) obj).token;
290+
}
291+
292+
@Override
293+
public int hashCode() {
294+
return System.identityHashCode(token);
295+
}
296+
}
297+
}

0 commit comments

Comments
 (0)