Add v8::Locker, v8::Unlocker, and UnenteredIsolate bindings#1896
Add v8::Locker, v8::Unlocker, and UnenteredIsolate bindings#1896max-lt wants to merge 4 commits into
Conversation
|
To make this actually safe, Also, v8::Locker internals don't belong in Also, these ai generated comments and pr descriptions are really fucking annoying. You need to take responsibility for what you are submitting, rather than just assuming claude will be correct. |
|
Will rework, thanks for the review |
|
Applied requested changes. Let me know if I missed anything. |
|
I would also like to help with MR. Do you need any help, or can I contribute to speed up the merge with the main code? |
|
Thanks @RainyPixel for offering help! I'd really appreciate it. Honestly, what I need most right now is a thorough code review. I believe the current implementation addresses @devsnek's safety concerns, but having fresh eyes on it would really help move this forward. In the meantime, we maintain a working fork at https://github.com/openworkers/rusty-v8 (currently at v145 with ptrcomp & sandbox feature flags) that we use in production for https://github.com/openworkers/openworkers-runner. It's also published on crates.io as https://crates.io/crates/openworkers-v8. The isolate pooling works well for our use case. Feel free to check it out if you want to see the implementation in action. |
|
@RainyPixel's follow-up commit adds panic safety for the Locker::new() initialization path and some compile-time safety tests. Ready for review when you have time. |
|
Commit de789a7 makes Locker a pure mutex, add IsolateScope for Enter/Exit Previous changes introduced Locker + unentered isolates to avoid OwnedIsolate's LIFO drop constraint. But the Locker itself still called Enter()/Exit() internally, which reintroduced the same LIFO constraint at the Locker level, when multiple Lockers for different isolates coexist on the same thread (async pool), dropping them out of order still corrupts the entry stack. Last fix completes the decoupling:
Tests: 10 safety tests (non-LIFO drop, concurrent lockers, stress) + 6 IsolateScope tests (interleaved isolates, round-robin). |
|
de789a7 called Enter()/Exit() during Locker construction, attempting to influence top_level_ in the C++ Locker. This was unnecessary, Locker::Initialize() determines top_level_ via RestoreThread() (archived thread state from Unlocker pairs), not via the entry stack. Since we never use Unlocker, top_level_ is always true, which correctly calls FreeThreadResources() on drop. 5b42bdd Removed the dead Enter/Exit calls and the IsolateExitGuard. No behavioral change. |
- Add IsolateExitGuard to ensure v8::Isolate::Exit is called if panic occurs during Locker::new() after Enter has been called - Add comprehensive documentation for Locker and UnenteredIsolate including thread safety guarantees and usage examples - Add debug_assert to verify no Locker is held when dropping UnenteredIsolate - Add UnenteredIsolate::as_raw() method for low-level access - Expand test coverage: - locker_state_preserved_across_locks - locker_drop_releases_lock - unentered_isolate_as_raw - locker_send_isolate_between_threads (verifies Send trait works) This improves safety guarantees for multi-threaded isolate usage.
Add compile_fail tests to ensure Rust's type system prevents misuse: - locker_double_borrow: prevents creating two Lockers simultaneously - locker_not_send: Locker cannot be sent to another thread - locker_scope_outlives: HandleScope cannot outlive its Locker Add comprehensive unsafe documentation to raw::Locker: - Document memory layout and size requirements - Document all safety invariants for initialization - Document thread affinity requirements Improve runtime tests: - Fix lifetime issues in locker_send_isolate_between_threads - Simplify locker_state_preserved_across_locks test All 9 runtime tests and 15 compile_fail tests pass.
Enter() must be called after acquiring the lock, and Exit() before releasing it, because V8's entry_stack_ is not thread-safe. - new: Lock -> Enter (hold lock before touching entry_stack_) - drop: Exit -> Unlock (exit while still holding the lock)
|
Rebased on upstream main (v146.4.0). The pure-mutex decoupling (de789a7, 5b42bdd) was actually a workaround for a bug in the original Locker: Enter() was called before acquiring the lock. Since V8's entry_stack_ is not thread-safe, this caused a race condition (vector[] index out of bounds crash under parallel usage). The real fix is the ordering:
This matches the supabase implementation. IsolateScope removed, Locker handles Enter/Exit with correct ordering, no footgun from DerefMut on a non-entered isolate. |
Adds bindings for multi-threaded isolate pooling, enabling architectures like Cloudflare Workers.
Changes
UnenteredIsolate: New isolate type without auto-enter, removes LIFO drop constraintv8::Locker: Acquires exclusive thread access to an isolatev8::Unlocker: Temporarily releases a lockunsafe impl SendforUnenteredIsolate(safe with Locker)Key Features
Arc<Mutex<UnenteredIsolate>>Tests
Note: Each thread must call
unsafe { isolate.enter() }before use to setup V8's thread-local state.No breaking changes - all new APIs