Skip to content

Commit 51a739e

Browse files
committed
Implement FinalizationRegistry pt. 3
1 parent 9cd2d6c commit 51a739e

File tree

4 files changed

+75
-37
lines changed

4 files changed

+75
-37
lines changed

core/engine/src/builtins/finalization_registry/mod.rs

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@ use super::{BuiltInConstructor, BuiltInObject, IntrinsicObject, builder::BuiltIn
2525

2626
/// On GG collection, sends a message to a [`FinalizationRegistry`] indicating that it needs to
2727
/// be collected.
28-
#[derive(Trace, Clone)]
28+
#[derive(Trace)]
2929
#[boa_gc(unsafe_empty_trace)]
30-
struct CleanupSignaler(async_channel::WeakSender<()>);
30+
struct CleanupSignaler(Cell<Option<async_channel::WeakSender<()>>>);
3131

3232
impl Finalize for CleanupSignaler {
3333
fn finalize(&self) {
34-
if let Some(sender) = self.0.upgrade() {
34+
if let Some(sender) = self.0.take()
35+
&& let Some(sender) = sender.upgrade()
36+
{
3537
// We don't need to handle errors:
3638
// - If the channel is full, the `FinalizationRegistry` has already
3739
// been enqueued for cleanup.
@@ -52,7 +54,7 @@ pub(crate) struct RegistryCell {
5254

5355
/// Boa's implementation of ECMAScript's [`FinalizationRegistry`] builtin object.
5456
///
55-
/// FinalizationRegistry provides a way to request that a cleanup callback get called at some point
57+
/// `FinalizationRegistry` provides a way to request that a cleanup callback get called at some point
5658
/// when a value registered with the registry has been reclaimed (garbage-collected).
5759
///
5860
/// [`FinalizationRegistry`]: https://tc39.es/ecma262/#sec-finalization-registry-objects
@@ -178,7 +180,7 @@ impl BuiltInConstructor for FinalizationRegistry {
178180
async move |context| inner_cleanup(weak_registry, receiver, context).await,
179181
)));
180182

181-
result.map(|_| JsValue::undefined())
183+
result.map(|()| JsValue::undefined())
182184
}
183185

184186
context.enqueue_job(Job::FinalizationRegistryCleanupJob(NativeAsyncJob::new(
@@ -249,7 +251,9 @@ impl FinalizationRegistry {
249251
let cell = RegistryCell {
250252
target: Ephemeron::new(
251253
target_obj.inner(),
252-
CleanupSignaler(registry.cleanup_notifier.clone().downgrade()),
254+
CleanupSignaler(Cell::new(Some(
255+
registry.cleanup_notifier.clone().downgrade(),
256+
))),
253257
),
254258
held_value: held_value.clone(),
255259
unregister_token,
@@ -302,14 +306,18 @@ impl FinalizationRegistry {
302306
// a. If cell.[[UnregisterToken]] is not empty and SameValue(cell.[[UnregisterToken]], unregisterToken) is true, then
303307
if let Some(tok) = cell.unregister_token.as_ref()
304308
&& let Some(tok) = tok.upgrade()
305-
&& Gc::ptr_eq(&tok, &unregister_token)
309+
&& Gc::ptr_eq(&tok, unregister_token)
306310
{
307311
// i. Remove cell from finalizationRegistry.[[Cells]].
308312
let cell = registry.cells.swap_remove(i);
309-
if let Some(value) = cell.target.value() {
310-
// Remove the inner signaler to avoid notifying a registry that doesn't
311-
// have dead entries.
312-
value.0.take();
313+
let _key = cell.target.key();
314+
// TODO: it might be better to add a special ref for the value that
315+
// also preserves the original key instead.
316+
// SAFETY: the original key is alive per our previous call to `key`,
317+
// so if this returns `Some`, then the value cannot be collected
318+
// until `key` gets dropped.
319+
unsafe {
320+
cell.target.value_ref().and_then(|v| v.0.take());
313321
}
314322

315323
// ii. Set removed to true.
@@ -353,7 +361,9 @@ impl FinalizationRegistry {
353361
break Ok(());
354362
}
355363
// 3. While finalizationRegistry.[[Cells]] contains a Record cell such that cell.[[WeakRefTarget]] is empty, an implementation may perform the following steps:
356-
if !obj.borrow().data().cells[i].target.has_value() {
364+
if obj.borrow().data().cells[i].target.has_value() {
365+
i += 1;
366+
} else {
357367
// a. Choose any such cell.
358368
// b. Remove cell from finalizationRegistry.[[Cells]].
359369
let cell = obj.borrow_mut().data_mut().cells.swap_remove(i);
@@ -368,8 +378,6 @@ impl FinalizationRegistry {
368378
if let Err(err) = result {
369379
break Err(err);
370380
}
371-
} else {
372-
i += 1;
373381
}
374382
};
375383

core/engine/src/job.rs

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -535,23 +535,23 @@ pub enum Job {
535535
///
536536
/// See [`GenericJob`] for more information.
537537
GenericJob(GenericJob),
538-
/// A job that will eventually cleanup a FinalizationRegistry.
538+
/// A job that will eventually cleanup a `FinalizationRegistry`.
539539
///
540540
/// This job differs slightly from the [spec]; originally it's defined
541-
/// as being enqueued exactly when a FinalizationRegistry needs to call
541+
/// as being enqueued exactly when a `FinalizationRegistry` needs to call
542542
/// `FinalizationRegistry::cleanup`, but here it's defined as an async
543543
/// job that suspends execution until it receives a signal from the engine
544-
/// that the FinalizationRegistry needs to be cleaned up.
544+
/// that the `FinalizationRegistry` needs to be cleaned up.
545545
///
546546
/// # Execution
547547
///
548548
/// As described on the [spec's section about execution][execution],
549549
///
550550
/// > Because calling HostEnqueueFinalizationRegistryCleanupJob is optional,
551-
/// registered objects in a FinalizationRegistry do not necessarily hold
552-
/// that FinalizationRegistry live. Implementations may omit FinalizationRegistry
553-
/// callbacks for any reason, e.g., if the FinalizationRegistry itself becomes
554-
/// dead, or if the application is shutting down.
551+
/// > registered objects in a FinalizationRegistry do not necessarily hold
552+
/// > that FinalizationRegistry live. Implementations may omit FinalizationRegistry
553+
/// > callbacks for any reason, e.g., if the FinalizationRegistry itself becomes
554+
/// > dead, or if the application is shutting down.
555555
///
556556
/// For this reason, it is recommended to exclude `FinalizationRegistry` cleanup
557557
/// jobs from any condition that returns from [`JobExecutor::run_jobs`].
@@ -657,6 +657,7 @@ impl JobExecutor for IdleJobExecutor {
657657
pub struct SimpleJobExecutor {
658658
promise_jobs: RefCell<VecDeque<PromiseJob>>,
659659
async_jobs: RefCell<VecDeque<NativeAsyncJob>>,
660+
finalization_registry_jobs: RefCell<VecDeque<NativeAsyncJob>>,
660661
timeout_jobs: RefCell<BTreeMap<JsInstant, Vec<TimeoutJob>>>,
661662
generic_jobs: RefCell<VecDeque<GenericJob>>,
662663
stop: Arc<AtomicBool>,
@@ -714,6 +715,9 @@ impl JobExecutor for SimpleJobExecutor {
714715
.push(t);
715716
}
716717
Job::GenericJob(g) => self.generic_jobs.borrow_mut().push_back(g),
718+
Job::FinalizationRegistryCleanupJob(fr) => {
719+
self.finalization_registry_jobs.borrow_mut().push_back(fr);
720+
}
717721
}
718722
}
719723

@@ -726,6 +730,7 @@ impl JobExecutor for SimpleJobExecutor {
726730
Self: Sized,
727731
{
728732
let mut group = FutureGroup::new();
733+
let mut fr_group = FutureGroup::new();
729734
loop {
730735
if self.stop.load(Ordering::Relaxed) {
731736
self.stop.store(false, Ordering::Relaxed);
@@ -737,6 +742,10 @@ impl JobExecutor for SimpleJobExecutor {
737742
group.insert(job.call(context));
738743
}
739744

745+
for job in mem::take(&mut *self.finalization_registry_jobs.borrow_mut()) {
746+
fr_group.insert(job.call(context));
747+
}
748+
740749
// Dispatch all past-due timeout jobs before the termination check.
741750
{
742751
let now = context.borrow().clock().now();
@@ -763,7 +772,14 @@ impl JobExecutor for SimpleJobExecutor {
763772
}
764773

765774
if self.is_empty() && group.is_empty() {
766-
break;
775+
match future::poll_once(fr_group.next()).await.flatten() {
776+
Some(Err(err)) => {
777+
self.clear();
778+
return Err(err);
779+
}
780+
_ if !self.is_empty() => {}
781+
_ => break,
782+
}
767783
}
768784

769785
if let Some(Err(err)) = future::poll_once(group.next()).await.flatten() {

core/gc/src/pointers/ephemeron.rs

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,7 @@ pub struct Ephemeron<K: Trace + ?Sized + 'static, V: Trace + 'static> {
2222
inner_ptr: NonNull<EphemeronBox<K, V>>,
2323
}
2424

25-
26-
impl<K: Trace + ?Sized, V: Trace + Clone> Ephemeron<K, V> {
27-
/// Gets the stored value of this `Ephemeron`, or `None` if the key was already garbage collected.
28-
///
29-
/// This needs to return a clone of the value because holding a reference to it between
30-
/// garbage collection passes could drop the underlying allocation, causing an Use After Free.
31-
#[must_use]
32-
pub fn value(&self) -> Option<V> {
33-
// SAFETY: this is safe because `Ephemeron` is tracked to always point to a valid pointer
34-
// `inner_ptr`.
35-
unsafe { self.inner_ptr.as_ref().value().cloned() }
36-
}
37-
25+
impl<K: Trace + ?Sized, V: Trace> Ephemeron<K, V> {
3826
/// Gets the stored key of this `Ephemeron`, or `None` if the key was already garbage collected.
3927
#[inline]
4028
#[must_use]
@@ -52,6 +40,20 @@ impl<K: Trace + ?Sized, V: Trace + Clone> Ephemeron<K, V> {
5240
Some(unsafe { Gc::from_raw(key_ptr) })
5341
}
5442

43+
/// Gets a reference to the stored value of this `Ephemeron`, or `None` if the
44+
/// key was already garbage collected.
45+
///
46+
/// # Safety
47+
///
48+
/// The key must be kept alive while this reference is active. Otherwise,
49+
/// the reference may point to deallocated memory if the key gets collected.
50+
#[must_use]
51+
pub unsafe fn value_ref(&self) -> Option<&V> {
52+
// SAFETY: this is safe because `Ephemeron` is tracked to always point to a valid pointer
53+
// `inner_ptr`.
54+
unsafe { self.inner_ptr.as_ref().value() }
55+
}
56+
5557
/// Checks if the [`Ephemeron`] has a value.
5658
#[must_use]
5759
pub fn has_value(&self) -> bool {
@@ -61,6 +63,19 @@ impl<K: Trace + ?Sized, V: Trace + Clone> Ephemeron<K, V> {
6163
}
6264
}
6365

66+
impl<K: Trace + ?Sized, V: Trace + Clone> Ephemeron<K, V> {
67+
/// Gets the stored value of this `Ephemeron`, or `None` if the key was already garbage collected.
68+
///
69+
/// This needs to return a clone of the value because holding a reference to it between
70+
/// garbage collection passes could drop the underlying allocation, causing an Use After Free.
71+
#[must_use]
72+
pub fn value(&self) -> Option<V> {
73+
// SAFETY: this is safe because `Ephemeron` is tracked to always point to a valid pointer
74+
// `inner_ptr`.
75+
unsafe { self.inner_ptr.as_ref().value().cloned() }
76+
}
77+
}
78+
6479
impl<K: Trace + ?Sized, V: Trace> Ephemeron<K, V> {
6580
/// Creates a new `Ephemeron`.
6681
#[must_use]

test262_config.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ flags = []
77
features = [
88
### Unimplemented features:
99

10-
"FinalizationRegistry",
1110
"symbols-as-weakmap-keys",
1211
"Intl.DisplayNames",
1312
"Intl.RelativeTimeFormat",
@@ -88,5 +87,5 @@ tests = [
8887
"test/intl402/Locale/constructor-options-canonicalized.js",
8988
# TODO: Remove this once regress fixes the named groups parsing issue.
9089
"test/built-ins/RegExp/named-groups/non-unicode-property-names-valid.js",
91-
90+
9291
]

0 commit comments

Comments
 (0)