@@ -374,6 +374,89 @@ pub fn pump_messages() -> bool {
374374 dispatched
375375}
376376
377+ /// Runs `f` on the JS thread whose isolate is `owner` (the UI thread's or a worker's) and waits for
378+ /// its result: inline when that is the current thread, otherwise handed over the way a callback
379+ /// invoked on a background thread is. `None` when that thread can't run it (gone, or unknown).
380+ #[ cfg( feature = "classic" ) ]
381+ pub ( crate ) fn run_on_js_thread_sync < R : Send > ( owner : * mut v8:: Isolate , f : impl FnOnce ( ) -> R + Send ) -> Option < R > {
382+ if !is_live_isolate ( owner) {
383+ return None ;
384+ }
385+ if DELEGATE_ISOLATE_PTR . with ( |c| c. get ( ) ) == owner {
386+ return Some ( f ( ) ) ;
387+ }
388+ if worker_threads:: is_worker_isolate ( owner as usize ) {
389+ return worker_threads:: run_on_worker_sync ( owner as usize , f) ;
390+ }
391+ if ui_dispatcher:: is_ui_thread ( ) {
392+ return None ;
393+ }
394+ ui_dispatcher:: run_on_ui_thread_sync ( f)
395+ }
396+
397+ /// Queues `f` on the JS thread whose isolate is `owner` without waiting; runs it inline when that
398+ /// is the current thread. `false` when there is no such thread to run it on.
399+ #[ cfg( feature = "classic" ) ]
400+ pub ( crate ) fn post_to_js_thread ( owner : * mut v8:: Isolate , f : impl FnOnce ( ) + Send + ' static ) -> bool {
401+ if !is_live_isolate ( owner) {
402+ return false ;
403+ }
404+ if DELEGATE_ISOLATE_PTR . with ( |c| c. get ( ) ) == owner {
405+ f ( ) ;
406+ return true ;
407+ }
408+ if worker_threads:: is_worker_isolate ( owner as usize ) {
409+ return worker_threads:: post_to_worker ( owner as usize , f) ;
410+ }
411+ if !ui_dispatcher:: is_initialized ( ) {
412+ return false ;
413+ }
414+ ui_dispatcher:: post_to_ui_thread ( f) ;
415+ true
416+ }
417+
418+ /// The isolates of the runtimes alive in this process (the UI thread's and each worker's). A
419+ /// callback that outlives its worker can't run anywhere: work for a disposed isolate is refused.
420+ #[ cfg( feature = "classic" ) ]
421+ static LIVE_ISOLATES : std:: sync:: LazyLock < std:: sync:: Mutex < std:: collections:: HashSet < usize > > > =
422+ std:: sync:: LazyLock :: new ( || std:: sync:: Mutex :: new ( std:: collections:: HashSet :: new ( ) ) ) ;
423+
424+ #[ cfg( feature = "classic" ) ]
425+ fn is_live_isolate ( isolate : * mut v8:: Isolate ) -> bool {
426+ !isolate. is_null ( ) && LIVE_ISOLATES . lock ( ) . is_ok_and ( |live| live. contains ( & ( isolate as usize ) ) )
427+ }
428+
429+ /// The isolate each .NET callback's JS function belongs to (callback ids are process-wide, the
430+ /// functions live in their JS thread's `DOTNET_JS_CALLBACKS`), so a callback the bridge invokes on
431+ /// another thread is run on the right one.
432+ #[ cfg( feature = "classic" ) ]
433+ static DOTNET_CALLBACK_OWNERS : std:: sync:: LazyLock < std:: sync:: Mutex < HashMap < i32 , usize > > > =
434+ std:: sync:: LazyLock :: new ( || std:: sync:: Mutex :: new ( HashMap :: new ( ) ) ) ;
435+
436+ #[ cfg( feature = "classic" ) ]
437+ pub ( crate ) fn record_dotnet_callback_owner ( callback_id : i32 ) {
438+ let owner = DELEGATE_ISOLATE_PTR . with ( |c| c. get ( ) ) as usize ;
439+ if let Ok ( mut owners) = DOTNET_CALLBACK_OWNERS . lock ( ) {
440+ owners. insert ( callback_id, owner) ;
441+ }
442+ }
443+
444+ #[ cfg( feature = "classic" ) ]
445+ pub ( crate ) fn dotnet_callback_owner ( callback_id : i32 ) -> Option < * mut v8:: Isolate > {
446+ DOTNET_CALLBACK_OWNERS
447+ . lock ( )
448+ . ok ( )
449+ . and_then ( |owners| owners. get ( & callback_id) . copied ( ) )
450+ . map ( |o| o as * mut v8:: Isolate )
451+ }
452+
453+ #[ cfg( feature = "classic" ) ]
454+ pub ( crate ) fn forget_dotnet_callback_owner ( callback_id : i32 ) {
455+ if let Ok ( mut owners) = DOTNET_CALLBACK_OWNERS . lock ( ) {
456+ owners. remove ( & callback_id) ;
457+ }
458+ }
459+
377460/// Retrieve (and clear) the last stored JS error.
378461pub fn get_last_js_error ( ) -> Option < String > {
379462 LAST_JS_ERROR . with ( |e| e. borrow_mut ( ) . take ( ) )
@@ -8009,8 +8092,8 @@ pub(crate) struct JsDelegateData {
80098092 /// For a delegate whose Invoke the shared vtable can't implement (see [`delegate_invoke`]):
80108093 /// the typed Invoke and the delegate's own vtable, which points at it.
80118094 typed : Option < ( delegate_invoke:: TypedInvoke , Box < JsDelegateVtbl > ) > ,
8012- /// The isolate `js_func` belongs to: the UI thread's, or null for a delegate created where no
8013- /// isolate is registered (a worker), which can't be handed to the UI thread.
8095+ /// The isolate `js_func` belongs to ( the UI thread's or a worker's): Invoke and the final
8096+ /// Release run on that isolate's thread.
80148097 owner_isolate : * mut v8:: Isolate ,
80158098}
80168099
@@ -8090,15 +8173,21 @@ unsafe extern "system" fn js_delegate_release(this: *mut JsDelegate) -> u32 {
80908173 std:: sync:: atomic:: fence ( AtomicOrdering :: Acquire ) ;
80918174 let b = Box :: from_raw ( this) ;
80928175 let data = Box :: from_raw ( b. data ) ;
8093- // The function handle belongs to the UI thread's isolate: a final release on a background
8094- // thread (the component that invoked the delegate there) frees it on the UI thread.
8095- if !data. owner_isolate . is_null ( ) && !ui_dispatcher:: is_ui_thread ( ) && ui_dispatcher:: is_initialized ( ) {
8096- struct SendData ( Box < JsDelegateData > ) ;
8097- unsafe impl Send for SendData { }
8098- let data = SendData ( data) ;
8099- ui_dispatcher:: post_to_ui_thread ( move || drop ( data) ) ;
8100- } else {
8176+ // The function handle belongs to its JS thread's isolate: a final release on another thread
8177+ // (the component that invoked the delegate there) frees it on that thread.
8178+ let owner = data. owner_isolate ;
8179+ if DELEGATE_ISOLATE_PTR . with ( |c| c. get ( ) ) == owner || owner. is_null ( ) {
81018180 drop ( data) ;
8181+ } else {
8182+ // Never dropped elsewhere: if that thread is gone (a terminated worker), the handle
8183+ // leaks rather than being freed into a disposed isolate.
8184+ struct SendData ( std:: mem:: ManuallyDrop < Box < JsDelegateData > > ) ;
8185+ unsafe impl Send for SendData { }
8186+ let data = SendData ( std:: mem:: ManuallyDrop :: new ( data) ) ;
8187+ post_to_js_thread ( owner, move || {
8188+ let data = data;
8189+ drop ( std:: mem:: ManuallyDrop :: into_inner ( data. 0 ) ) ;
8190+ } ) ;
81028191 }
81038192 // b (JsDelegate) dropped here
81048193 }
@@ -8183,23 +8272,16 @@ fn js_delegate_invoke_with_args(
81838272 result_slot : * mut c_void ,
81848273) -> HRESULT {
81858274 let isolate_ptr = DELEGATE_ISOLATE_PTR . with ( |c| c. get ( ) ) ;
8186- if !isolate_ptr. is_null ( ) && isolate_ptr != data. owner_isolate {
8187- // Never run a function against an isolate it doesn't belong to.
8188- return HRESULT ( 0x8001010Eu32 as i32 ) ; // RPC_E_WRONG_THREAD
8189- }
8190- if isolate_ptr. is_null ( ) {
8191- // Invoked directly on a background thread (a component that calls its handlers without
8192- // marshaling them back to the registering apartment): run the JS on the JS (UI) thread
8193- // and wait, so the call is delivered and its result written before Invoke returns. The
8194- // arguments are borrowed for the duration of Invoke, which is exactly how long they're used.
8195- // Only the UI thread's functions can go there; a worker's stay undeliverable.
8196- if ui_dispatcher:: is_ui_thread ( ) || data. owner_isolate . is_null ( ) {
8197- return HRESULT ( 0x80004005u32 as i32 ) ;
8198- }
8275+ if isolate_ptr != data. owner_isolate {
8276+ // Invoked on another thread (a component that calls its handlers without marshaling them
8277+ // back to the registering apartment): run the JS on the thread that owns the function —
8278+ // the UI thread or a worker — and wait, so the call is delivered and its result written
8279+ // before Invoke returns. The arguments are borrowed for the duration of Invoke, which is
8280+ // exactly how long they're used.
81998281 struct SendPtr ( * mut JsDelegate , * mut c_void ) ;
82008282 unsafe impl Send for SendPtr { }
82018283 let ptrs = SendPtr ( this, result_slot) ;
8202- return ui_dispatcher :: run_on_ui_thread_sync ( move || {
8284+ return run_on_js_thread_sync ( data . owner_isolate , move || {
82038285 let ptrs = ptrs;
82048286 // SAFETY: the caller holds a reference on the delegate for the duration of Invoke.
82058287 let data = unsafe { & * ( * ptrs. 0 ) . data } ;
@@ -9036,6 +9118,9 @@ impl Runtime {
90369118 pub fn register_delegate_isolate_ptr ( & mut self ) {
90379119 let raw_isolate: * mut v8:: Isolate = & mut * self . isolate as * mut v8:: Isolate ;
90389120 DELEGATE_ISOLATE_PTR . with ( |cell| cell. set ( raw_isolate) ) ;
9121+ if let Ok ( mut live) = LIVE_ISOLATES . lock ( ) {
9122+ live. insert ( raw_isolate as usize ) ;
9123+ }
90399124 }
90409125
90419126 /// Provides mutable access to the underlying V8 isolate.
@@ -9352,6 +9437,9 @@ impl Drop for Runtime {
93529437 // raw isolate pointers) must be cleared here, while `self.isolate` is
93539438 // still alive. Anything left behind dangles into freed isolate memory
93549439 // and crashes the next Runtime created on this thread.
9440+ if let Ok ( mut live) = LIVE_ISOLATES . lock ( ) {
9441+ live. remove ( & ( & mut * self . isolate as * mut v8:: Isolate as usize ) ) ;
9442+ }
93559443 crate :: wrapper_cache:: clear ( ) ;
93569444 EVENT_REGISTRY . with ( |m| m. borrow_mut ( ) . clear ( ) ) ;
93579445 ESM_MODULE_REGISTRY . with ( |m| m. borrow_mut ( ) . clear ( ) ) ;
@@ -9360,6 +9448,8 @@ impl Drop for Runtime {
93609448 ESM_HTTP_URLS . with ( |m| m. borrow_mut ( ) . clear ( ) ) ;
93619449 ESM_LOAD_ERRORS . with ( |m| m. borrow_mut ( ) . clear ( ) ) ;
93629450 crate :: esm_http:: clear_thread_vocabulary ( ) ;
9451+ // The ids keep their owner: a managed delegate that outlives this runtime (a terminated
9452+ // worker's) is refused rather than run elsewhere, until .NET releases it.
93639453 DOTNET_JS_CALLBACKS . with ( |m| m. borrow_mut ( ) . clear ( ) ) ;
93649454 DOTNET_ONESHOT_JS_CALLBACKS . with ( |m| m. borrow_mut ( ) . clear ( ) ) ;
93659455 crate :: timers:: clear_thread_tasks ( ) ;
0 commit comments