Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Weak handles and finalizers #895

Merged
merged 5 commits into from
May 9, 2022
Merged

Conversation

andreubotella
Copy link
Contributor

@andreubotella andreubotella commented Feb 9, 2022

Just a strawperson proposal to test out the waters. The Discord conversation mentioned storing the callbacks in the isolate annex (or something), but doing it with pinned boxes seems easier to implement. We can still change it to use the annex, of course.

Closes #861.

@CLAassistant
Copy link

CLAassistant commented Feb 9, 2022

CLA assistant check
All committers have signed the CLA.

@andreubotella andreubotella force-pushed the weak branch 2 times, most recently from 7d6c565 to 1b98d81 Compare February 9, 2022 09:01
@andreubotella
Copy link
Contributor Author

cc @lucacasonato @piscisaureus

@piscisaureus
Copy link
Member

I know this is WIP, but it would be nice to get some sort of test so we can see that it works (and that lets us poke at some edge cases).

src/handle.rs Outdated
}

extern "C" fn second_pass_callback(wci: *const WeakCallbackInfo) {
// FIXME!!!!: Do we know for a fact that the parameter hasn't been dropped??
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is safe? WeakData<T> is not dropped until Weak<T> itself drops.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first pass callback must be called the moment the reference is GC'd, since it's that callback's job to reset the weak handle, but the second pass callback has no such guarantees as far as I'm aware. Looking in the V8 code it seems like sometimes second passes run during GC, but other times they're scheduled on the platform task runner (which I believe would only get called when pumping the V8 message loop at the beginning of the next run in Deno's event loop).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What mechanism exists that could cause the WeakData to be dropped earlier than expected?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My concern is a scenario where the value is GC'd while Weak is still alive, but the second pass callback is scheduled for later, and Weak drops before that second pass callback runs. We could have flags in WeakData to check whether the second callback has run, and whether the corresponding Weak has dropped, and then leak the Box<WeakData> in the drop impl and deallocate it in the second pass callback if this is the case.

It seems like the various ways to trigger GC in tests (gc(), sending a low-memory notification, even running the GC in predictable mode) will cause second passes to run synchronously, so I'm not sure that this is testable.

@andreubotella
Copy link
Contributor Author

andreubotella commented Feb 15, 2022

I was getting weird results when creating weak handles from globals, and apparently dropping globals will not cause the first callback to be called.

#[test]
fn weak_from_global() {
  let _setup_guard = setup();

  let mut isolate = v8::Isolate::new(Default::default());
  let mut scope = v8::HandleScope::new(&mut isolate);
  let context = v8::Context::new(&mut scope);
  let scope = &mut v8::ContextScope::new(&mut scope, context);

  let global = {
    let object = v8::Object::new(scope);
    v8::Global::new(scope, object)
  };

  let weak = v8::Weak::new(scope, &global);
  assert!(!weak.is_empty());
  assert_eq!(weak.open(scope).unwrap(), global.open(scope));

  drop(global);
  eval(scope, "gc()").unwrap();
  assert!(weak.is_empty());  // This assertion fails.
}

@littledivy
Copy link
Member

littledivy commented Feb 15, 2022

@andreubotella Try a v8::Isolate::low_memory_notification after drop(global)
d8 uses it to force weak callback invocation. https://chromium.googlesource.com/v8/v8/+/3.27.34.7/src/d8.cc#1385

@piscisaureus
Copy link
Member

I was getting weird results when creating weak handles from globals, and apparently dropping globals will not cause the first callback to be called.

I think the problem is that the HandleScope is still keeping the Local<Object> that was initially created alive.

@piscisaureus
Copy link
Member

@andreubotella FYI I've fixed and added the weak_from_global test.

@piscisaureus
Copy link
Member

Added some comments but this looks pretty good to me overall.
@andreubotella is there anything you are still trying to figure out or is it ready for final review?

@andreubotella
Copy link
Contributor Author

Added some comments but this looks pretty good to me overall. @andreubotella is there anything you are still trying to figure out or is it ready for final review?

I was trying to figure out how to test that there's no UB if the second pass callback is called asynchronously, after Weak is dropped, but since it doesn't seem to be testable, I've just added a fix that seems like would work, so this is now ready for review.

@andreubotella andreubotella marked this pull request as ready for review February 22, 2022 01:24
@piscisaureus
Copy link
Member

I was trying to figure out how to test that there's no UB if the second pass callback is called asynchronously, after Weak is dropped, but since it doesn't seem to be testable, I've just added a fix that seems like would work, so this is now ready for review.

I think the biggest concern is now what happens when the isolate is disposed between the first and second stage.
I think in the latest iteration of this patch the WeakData would be leaked, unless V8 runs all pending second stage callbacks synchronously when disposed.

Is that correct?

@andreubotella
Copy link
Contributor Author

I think the biggest concern is now what happens when the isolate is disposed between the first and second stage. I think in the latest iteration of this patch the WeakData would be leaked, unless V8 runs all pending second stage callbacks synchronously when disposed.

Is that correct?

That's right. But I don't think there's a way to prevent leaking the WeakData.

}
}

fn get_pointer(&self) -> Option<NonNull<T>> {
Copy link
Member

@littledivy littledivy Feb 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In NAPI, we implement reference counting to these Weak instances, if the ref count is greater than 0 we need to "clear the weak" and prevent finalization (sorta like what's happening in drop() rn?) What would be the right API for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per conversation on Discord, keeping track of the reference count and clearing the weak handle can be done in Deno, but we would need a way to prevent finalization of a Weak, possibly even after the first pass callback has run. Since the second pass callback will only run the finalizer if it's Some, preventing finalization would just involve setting it to None.

@andreubotella andreubotella changed the title [WIP] Weak handles and finalizers Weak handles and finalizers Feb 24, 2022
src/handle.rs Outdated
}
}

pub fn unset_finalizer(&mut self) {
Copy link
Contributor Author

@andreubotella andreubotella Feb 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method doesn't really have to take &mut self, since the finalizer is behind a Cell, but semantically it does mutate the handle. WDYT?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is better that using &self 👍 let's keep it

@piscisaureus piscisaureus self-assigned this Feb 25, 2022
@andreubotella
Copy link
Contributor Author

andreubotella commented Mar 1, 2022

Rebasing this to main seems to introduce a V8 crash in weak_is_empty_after_dropping_isolate:

#
# Fatal error in ../../../v8/src/handles/global-handles.cc, line 311
# Debug check failed: AsChild()->IsInUse().

Edit: Nevermind, this was me forgetting that I had to update the usage of Global::new_raw in to_global after #906.

@andreubotella
Copy link
Contributor Author

andreubotella commented Mar 1, 2022

I'm not 100% sure that the semantics of when finalizers are run make sense. If a Weak<T> is dropped while the value hasn't been GC'd, the finalizer won't run. But if a Weak<T> is dropped after the value has been GC'd, the finalizer might still run after it's dropped. Is this something that makes sense? The finalizer can be canceled with unset_finalizer(), so it's not world-breaking if you don't want that behavior. Also, the alternative of unsetting the finalizer on drop by default would be a lot harder to prevent.

Also, while I was thinking of this I noticed that the Drop impl assumes that if the finalizer is unset, there's no second-pass callback still to run (because there was never a finalizer, or because the callback has already run, consuming the finalizer), and so assumes that it's safe to drop the Box<WeakData<T>>. With unset_finalizer(), this is no longer true. I'll think of how to fix this tomorrow.

@piscisaureus
Copy link
Member

If a Weak<T> is dropped while the value hasn't been GC'd, the finalizer won't run. But if a Weak<T> is dropped after the value has been GC'd, the finalizer might still run after it's dropped. Is this something that makes sense?

I don't think that makes sense - we should prevent the finalizer from running after the Weak has been dropped.

Also, while I was thinking of this I noticed that the Drop impl assumes that if the finalizer is unset, there's no second-pass callback still to run (because there was never a finalizer, or because the callback has already run, consuming the finalizer), and so assumes that it's safe to drop the Box<WeakData<T>>. With unset_finalizer(), this is no longer true. I'll think of how to fix this tomorrow.

I'm also not super happy with the potential for memory leaks when the Isolate is dropped between first and second stage callbacks. Maybe what we should do is the following:

  • Store the IsolateHandle in the WeakData rather than in the Weak.
  • Assign each finalizer a unique id.
  • Add a HashMap<WeakId, FinalizerFn> to the IsolateAnnex.
  • When a Weak is dropped, remove its entry from the hashmap.
  • In the second-stage callback, look up the finalizer in the hashmap by its id. If not found, assume that the Weak has been dropped and don't call any callbacks.
  • When the isolate is disposed, clear the hashmap.

@andreubotella
Copy link
Contributor Author

andreubotella commented Mar 6, 2022

I'm also not super happy with the potential for memory leaks when the Isolate is dropped between first and second stage callbacks. Maybe what we should do is the following:

  • Store the IsolateHandle in the WeakData rather than in the Weak.

  • Assign each finalizer a unique id.

  • Add a HashMap<WeakId, FinalizerFn> to the IsolateAnnex.

  • When a Weak is dropped, remove its entry from the hashmap.

  • In the second-stage callback, look up the finalizer in the hashmap by its id. If not found, assume that the Weak has been dropped and don't call any callbacks.

  • When the isolate is disposed, clear the hashmap.

Done. Rather than storing the IsolateHandle in WeakData, I instead used v8::WeakCallbackInfo::GetIsolate().

@andreubotella
Copy link
Contributor Author

@littledivy In the end we decided to remove the unset_finalizer method, and instead unset the finalizer automatically when dropping a Weak<T>. This covers all of the NAPI requirements, right?

@piscisaureus This should be now ready.

@littledivy
Copy link
Member

instead unset the finalizer automatically when dropping a Weak. This covers all of the NAPI requirements, right?

yup

Copy link
Member

@littledivy littledivy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using this branch crashes the Deno repl. (Something going on with typescript compiler ops).

#
# Fatal error in ../../../../rusty_v8/v8/src/objects/objects-inl.h, line 455
# Debug check failed: IsNumber().
#
#
#
#FailureMessage Object: 0x1701736c8
==== C stack trace ===============================

    0   deno                                0x000000010470d18c v8::base::debug::StackTrace::StackTrace() + 24
    1   deno                                0x0000000104714d5c v8::platform::(anonymous namespace)::PrintStackTrace() + 116
    2   deno                                0x0000000104704ce4 V8_Fatal(char const*, int, char const*, ...) + 268
    3   deno                                0x00000001047047ac std::__1::enable_if<((!(std::is_function<std::__1::remove_pointer<char>::type>::value)) && (!(std::is_enum<char>::value))) && (has_output_operator<char, v8::base::CheckMessageStream>::value), std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > >::type v8::base::PrintCheckOperand<char>(char) + 0
    4   deno                                0x00000001047e1a18 v8::Integer::Value() const + 148
    5   deno                                0x00000001046e9778 v8__Integer__Value + 24
    6   deno                                0x00000001046b31dc _ZN2v86number35_$LT$impl$u20$v8..data..Integer$GT$5value17h664286859e3a18ffE + 24
    7   deno                                0x0000000100df1d6c deno::lsp::tsc::op_respond::v8_func::h35ecb08cd383903e + 120
    8   deno                                0x0000000100e1f938 core::ops::function::Fn::call::h519fb15c654e8819 + 52
    9   deno                                0x000000010136ec84 _ZN2v88function147_$LT$impl$u20$v8..support..MapFnFrom$LT$F$GT$$u20$for$u20$extern$u20$$u22$C$u22$$u20$fn$LP$$BP$const$u20$v8..function..FunctionCallbackInfo$RP$$GT$7mapping28_$u7b$$u7b$closure$u7d$$u7d$17h75289b9897a40af5E + 332
    10  deno                                0x00000001013ea9f4 _ZN105_$LT$extern$u20$$u22$C$u22$$u20$fn$LP$A0$RP$$u20$.$GT$$u20$R$u20$as$u20$v8..support..CFnFrom$LT$F$GT$$GT$7mapping4c_fn17h0f7a4e174e2d7ed8E + 188
    11  deno                                0x00000001048444c0 v8::internal::FunctionCallbackArguments::Call(v8::internal::CallHandlerInfo) + 800
    12  deno                                0x0000000104842a8c v8::internal::MaybeHandle<v8::internal::Object> v8::internal::(anonymous namespace)::HandleApiCallHelper<false>(v8::internal::Isolate*, v8::internal::Handle<v8::internal::HeapObject>, v8::internal::Handle<v8::internal::HeapObject>, v8::internal::Handle<v8::internal::FunctionTemplateInfo>, v8::internal::Handle<v8::internal::Object>, v8::internal::BuiltinArguments) + 816
    13  deno                                0x00000001048413e8 v8::internal::Builtin_HandleApiCall(int, unsigned long*, v8::internal::Isolate*) + 524
    14  deno                                0x0000000105e2b6c8 Builtins_CEntry_Return1_DontSaveFPRegs_ArgvOnStack_BuiltinExit + 136
    15  deno                                0x0000000105bb4314 Builtins_InterpreterEntryTrampoline + 308
    16  deno                                0x0000000105bb4314 Builtins_InterpreterEntryTrampoline + 308
    17  deno                                0x0000000105bb4314 Builtins_InterpreterEntryTrampoline + 308
    18  deno                                0x0000000105bb4314 Builtins_InterpreterEntryTrampoline + 308
    19  deno                                0x0000000105bad7d8 Builtins_JSEntryTrampoline + 184
    20  deno                                0x0000000105bad468 Builtins_JSEntry + 168
    21  deno                                0x0000000104a47c9c v8::internal::(anonymous namespace)::Invoke(v8::internal::Isolate*, v8::internal::(anonymous namespace)::InvokeParams const&) + 4376
    22  deno                                0x0000000104a48640 v8::internal::Execution::CallScript(v8::internal::Isolate*, v8::internal::Handle<v8::internal::JSFunction>, v8::internal::Handle<v8::internal::Object>, v8::internal::Handle<v8::internal::Object>) + 472
    23  deno                                0x00000001047c32e4 v8::Script::Run(v8::Local<v8::Context>, v8::Local<v8::Data>) + 1216
    24  deno                                0x00000001046eb3f4 v8__Script__Run + 92
    25  deno                                0x00000001046ca600 _ZN2v86script34_$LT$impl$u20$v8..data..Script$GT$3run28_$u7b$$u7b$closure$u7d$$u7d$17hedcb8a831580682dE + 56
    26  deno                                0x00000001046d88f0 _ZN2v85scope27HandleScope$LT$$LP$$RP$$GT$10cast_local17h64b34c9057b07c6dE + 72
    27  deno                                0x00000001046b4434 _ZN2v86script34_$LT$impl$u20$v8..data..Script$GT$3run17h89116a7209c48deaE + 64
    28  deno                                0x00000001045bfc0c deno_core::runtime::JsRuntime::execute_script::h157dd1708db9f406 + 512
    29  deno                                0x0000000100d52b80 deno::lsp::tsc::request::h29a86fe263d8ab14 + 1340
    30  deno                                0x0000000100d29630 _ZN4deno3lsp3tsc8TsServer3new28_$u7b$$u7b$closure$u7d$$u7d$28_$u7b$$u7b$closure$u7d$$u7d$17h4c11f6a6e80c014fE + 900
    31  deno                                0x0000000100f2448c _ZN97_$LT$core..future..from_generator..GenFuture$LT$T$GT$$u20$as$u20$core..future..future..Future$GT$4poll17h707b9a8b1146c4e8E + 84
    32  deno                                0x0000000100a3a350 _ZN72_$LT$core..pin..Pin$LT$P$GT$$u20$as$u20$core..future..future..Future$GT$4poll17h1e2f367c7e90200aE + 88
    33  deno                                0x0000000100bfb574 _ZN5tokio7runtime15basic_scheduler9CoreGuard8block_on28_$u7b$$u7b$closure$u7d$$u7d$28_$u7b$$u7b$closure$u7d$$u7d$28_$u7b$$u7b$closure$u7d$$u7d$17h20239f57c9824759E + 72
    34  deno                                0x000000010118486c _ZN5tokio4coop11with_budget28_$u7b$$u7b$closure$u7d$$u7d$17h5b94841875dd443bE + 196
    35  deno                                0x0000000100a55708 _ZN3std6thread5local17LocalKey$LT$T$GT$8try_with17heb4e8f9baffa8d6bE + 188
    36  deno                                0x0000000100a4f9f0 _ZN3std6thread5local17LocalKey$LT$T$GT$4with17h7ee4b6176ab330f5E + 48
    37  deno                                0x0000000100bfb428 _ZN5tokio7runtime15basic_scheduler9CoreGuard8block_on28_$u7b$$u7b$closure$u7d$$u7d$28_$u7b$$u7b$closure$u7d$$u7d$17hf11bb8487b25d55dE + 172
    38  deno                                0x0000000100bf354c tokio::runtime::basic_scheduler::Context::enter::h7faa10e32a8bb836 + 308
    39  deno                                0x0000000100bf9034 _ZN5tokio7runtime15basic_scheduler9CoreGuard8block_on28_$u7b$$u7b$closure$u7d$$u7d$17h3fb3f58991aba375E + 380
    40  deno                                0x0000000100bfd6c0 _ZN5tokio7runtime15basic_scheduler9CoreGuard5enter28_$u7b$$u7b$closure$u7d$$u7d$17ha03d9cf4d5dfbc31E + 48
    41  deno                                0x00000001009bb458 _ZN5tokio6macros10scoped_tls18ScopedKey$LT$T$GT$3set17hedbcace6b9a4df13E + 128
    42  deno                                0x0000000100bfd368 tokio::runtime::basic_scheduler::CoreGuard::enter::hfbe77a5b314a5684 + 284
    43  deno                                0x0000000100bf8038 tokio::runtime::basic_scheduler::CoreGuard::block_on::hf7a1f36eeb94d7fa + 56
    44  deno                                0x0000000100bf0c70 tokio::runtime::basic_scheduler::BasicScheduler::block_on::hf19a33b65c602371 + 204
    45  deno                                0x000000010104fd40 tokio::runtime::Runtime::block_on::h40ace54bea16a12c + 152
    46  deno                                0x0000000100d29254 _ZN4deno3lsp3tsc8TsServer3new28_$u7b$$u7b$closure$u7d$$u7d$17h54152d3e4debc869E + 168
    47  deno                                0x0000000100ae5820 std::sys_common::backtrace::__rust_begin_short_backtrace::h47533819778b5f96 + 32
    48  deno                                0x0000000101167a8c _ZN3std6thread7Builder15spawn_unchecked28_$u7b$$u7b$closure$u7d$$u7d$28_$u7b$$u7b$closure$u7d$$u7d$17hda4235906f1b64d0E + 32
    49  deno                                0x00000001008b0484 _ZN115_$LT$core..panic..unwind_safe..AssertUnwindSafe$LT$F$GT$$u20$as$u20$core..ops..function..FnOnce$LT$$LP$$RP$$GT$$GT$9call_once17h99da90c9c54a2aa7E + 32
    50  deno                                0x00000001008d55cc std::panicking::try::do_call::h732e87fb06b765f5 + 68
    51  deno                                0x00000001008fb9e4 __rust_try + 32
    52  deno                                0x00000001008cd518 std::panicking::try::hb4cfc88c580ffc43 + 116
    53  deno                                0x00000001013d02bc std::panic::catch_unwind::hf674a37210def406 + 32
    54  deno                                0x0000000101166e7c _ZN3std6thread7Builder15spawn_unchecked28_$u7b$$u7b$closure$u7d$$u7d$17h456ccffcf1429df8E + 280
    55  deno                                0x0000000100e21ed4 _ZN4core3ops8function6FnOnce40call_once$u7b$$u7b$vtable.shim$u7d$$u7d$17h4e4a95ed1146f2b7E + 24
    56  deno                                0x00000001062c7298 std::sys::unix::thread::Thread::new::thread_start::ha92b558f2b29b4cc + 48
    57  libsystem_pthread.dylib             0x00000001bad41240 _pthread_start + 148
    58  libsystem_pthread.dylib             0x00000001bad3c024 thread_start + 8
fish: Job 1, 'target/debug/deno' terminated by signal SIGTRAP (Trace or breakpoint trap)

@andreubotella
Copy link
Contributor Author

Using this branch crashes the Deno repl. (Something going on with typescript compiler ops).

Not caused by this PR. It seems to be triggered by updating the V8 submodule to 10.0-lkgr-denoland, but the ultimate cause is Deno keeping an opIds object in JS-land that doesn't get synchronized with the actual ops. I submitted a PR to fix that at the root in denoland/deno#14224.

This change adds support for weak handles that don't prevent GC of the
referenced objects, through the `v8::Weak<T>` API. A weak handle can
be empty (if it was created empty or its object was GC'd) or
non-empty, and if non-empty it allows getting its object as a global
or local.

When creating a `v8::Weak` you can also set a finalizer that will be
called at some point after the object is GC'd, as long as the weak
handle is still alive at that point. This finalization corresponds to
the second-pass callback in `kParameter` mode in the C++ API, so it
will only be called after the object is GC'd. The finalizer function
is a `FnOnce` that may close over data, and which takes a
`&mut Isolate` as an argument.

The C++ finalization API doesn't guarantee _when_ or even _if_ the
finalizer will ever be called, but in order to prevent memory leaks,
the rusty_v8 wrapper ensures that it will be called at some point,
even if it's just before the isolate gets dropped.

`v8::Weak<T>` implements `Clone`, but a finalizer is tied to a single
weak handle, so its clones won't be able to keep the finalizer alive.
And in fact, cloning will create a new weak handle that isn't tied to
a finalizer at all. `v8::Weak::clone_with_finalizer` can be used to
make a clone of a weak handle which has a finalizer tied to it.

Note that `v8::Weak<T>` doesn't implement `Hash`, because the hash
would have to change once the handle's object is GC'd, which is a big
gotcha and would break some of the algorithms that rely on hashes,
such as the Rust std's `HashMap`.

Closes denoland#861.
@andreubotella
Copy link
Contributor Author

Since there's been a lot of back and forth on some topics, the resulting behavior might not be clear, so I've squashed all the commits and written a single commit message that hopefully informs the reviews.

@andreubotella
Copy link
Contributor Author

andreubotella commented May 8, 2022

Currently, this PR runs any remaining finalization callbacks as the isolate is being disposed, in particular after dropping the isolate's annex, which as @littledivy found, triggers a crash when you run v8::Global::from_raw or v8::Weak::from_raw in such a finalizer. But as I looked into this, I found something that is more concerning.

v8::Global instances are not tied to the isolate's lifetime, and so they can outlive it. This is not a problem because in order to use them you usually need a mutable reference to the corresponding isolate, and the few cases where you don't need one will panic if you use them after the corresponding isolate is dropped. However, the fact that dropping the isolate will run every registered finalizer, means that you can access a v8::Global to some object from a finalizer tied to that object. Furthermore, since the finalizer is passed a &mut Isolate, you might expect to be able to use the object. Trying that will panic, but if we fix the above crash by running the finalizers before dropping the annex, that might not be the case.

The V8 C++ API guarantees nothing about whether a finalizer will run. This PR currently guarantees that the finalizer will always run, but that causes the above mess. I think we should instead run the finalizers as long as there isn't a v8::Global keeping the object alive at the time that the isolate is dropped. To do this we could call v8::Isolate::low_memory_notification(), which triggers a full synchronous GC. Since this is expensive, we would only trigger that low-memory notification if the finalizer map is not empty.

The tricky part is how to run the GC after all scopes and contexts have been dropped, so you could have finalizers on the global object.

Edit: I think this is now ready. I checked every weak or finalizer-related test, as well as @littledivy's test case, with valgrind, and there seem to be no memory bugs anymore. I did notice a use-after-free of WeakData when you use Weak::from_raw inside its finalizer, but I fixed that by moving the finalizer call in the second pass callback to after any use of the WeakData.

Copy link
Member

@littledivy littledivy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

Copy link
Member

@bartlomieju bartlomieju left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thank you @andreubotella, this is great work!

@bartlomieju bartlomieju merged commit 1ed35fd into denoland:main May 9, 2022
@andreubotella andreubotella deleted the weak branch May 9, 2022 10:21
SpencerSharkey pushed a commit to SpencerSharkey/rusty_v8 that referenced this pull request May 9, 2022
This change adds support for weak handles that don't prevent GC of the
referenced objects, through the `v8::Weak<T>` API. A weak handle can
be empty (if it was created empty or its object was GC'd) or
non-empty, and if non-empty it allows getting its object as a global
or local.

When creating a `v8::Weak` you can also set a finalizer that will be
called at some point after the object is GC'd, as long as the weak
handle is still alive at that point. This finalization corresponds to
the second-pass callback in `kParameter` mode in the C++ API, so it
will only be called after the object is GC'd. The finalizer function
is a `FnOnce` that may close over data, and which takes a
`&mut Isolate` as an argument.

The C++ finalization API doesn't guarantee _when_ or even _if_ the
finalizer will ever be called, but in order to prevent memory leaks,
the rusty_v8 wrapper ensures that it will be called at some point,
even if it's just before the isolate gets dropped.

`v8::Weak<T>` implements `Clone`, but a finalizer is tied to a single
weak handle, so its clones won't be able to keep the finalizer alive.
And in fact, cloning will create a new weak handle that isn't tied to
a finalizer at all. `v8::Weak::clone_with_finalizer` can be used to
make a clone of a weak handle which has a finalizer tied to it.

Note that `v8::Weak<T>` doesn't implement `Hash`, because the hash
would have to change once the handle's object is GC'd, which is a big
gotcha and would break some of the algorithms that rely on hashes,
such as the Rust std's `HashMap`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support weak references or a Rust-level WeakMap
5 participants