@@ -98,13 +98,13 @@ impl From<RoomState> for Membership {
9898
9999#[ derive( uniffi:: Object ) ]
100100pub struct Room {
101- pub ( super ) inner : SdkRoom ,
101+ pub ( super ) inner : AsyncRuntimeDropped < SdkRoom > ,
102102 utd_hook_manager : Option < Arc < UtdHookManager > > ,
103103}
104104
105105impl Room {
106106 pub ( crate ) fn new ( inner : SdkRoom , utd_hook_manager : Option < Arc < UtdHookManager > > ) -> Self {
107- Room { inner, utd_hook_manager }
107+ Room { inner : AsyncRuntimeDropped :: new ( inner ) , utd_hook_manager }
108108 }
109109}
110110
@@ -1981,3 +1981,51 @@ impl TryFrom<SdkRoomSendQueueUpdate> for RoomSendQueueUpdate {
19811981 } )
19821982 }
19831983}
1984+
1985+ #[ cfg( all( test, not( target_family = "wasm" ) ) ) ]
1986+ mod tests {
1987+ use std:: time:: Duration ;
1988+
1989+ use matrix_sdk:: { ruma:: room_id, test_utils:: mocks:: MatrixMockServer } ;
1990+ use tempfile:: tempdir;
1991+
1992+ use super :: * ;
1993+
1994+ /// Dropping an FFI [`Room`] on a non-tokio thread must not panic.
1995+ ///
1996+ /// Regression test: when `Room.inner` was `SdkRoom` (not wrapped in
1997+ /// [`AsyncRuntimeDropped`]), garbage-collection of an orphaned `Room` on
1998+ /// the Hermes JS thread caused `SyncWrapper<rusqlite::Connection>::drop`
1999+ /// to call `tokio::task::spawn_blocking` from a thread with no tokio
2000+ /// runtime context. Rust turns a panic inside `Drop` into SIGABRT.
2001+ ///
2002+ /// The fix wraps `inner` in [`AsyncRuntimeDropped`], which enters the
2003+ /// global tokio runtime context before running `SdkRoom`'s destructor.
2004+ #[ tokio:: test]
2005+ async fn room_drop_on_non_tokio_thread_does_not_panic ( ) {
2006+ let server = MatrixMockServer :: new ( ) . await ;
2007+ let dir = tempdir ( ) . unwrap ( ) ;
2008+
2009+ let client =
2010+ server. client_builder ( ) . on_builder ( |b| b. sqlite_store ( dir. path ( ) , None ) ) . build ( ) . await ;
2011+
2012+ // Allow background init tasks to complete; after this the only strong
2013+ // reference to `ClientInner` is the local `client` variable.
2014+ tokio:: time:: sleep ( Duration :: from_secs ( 1 ) ) . await ;
2015+
2016+ let room_id = room_id ! ( "!test:example.com" ) ;
2017+ let sdk_room = server. sync_joined_room ( & client, room_id) . await ;
2018+ let ffi_room = Room :: new ( sdk_room, None ) ;
2019+
2020+ // Dropping `client` makes `ffi_room` the sole Arc holder of
2021+ // `ClientInner`, mirroring the situation where the FFI client has
2022+ // already been released and GC finalises the last room JS object.
2023+ drop ( client) ;
2024+
2025+ // Simulate Hermes GC on the JS thread (a non-tokio thread).
2026+ // Without the `AsyncRuntimeDropped` wrapper this causes a SIGABRT.
2027+ std:: thread:: spawn ( move || drop ( ffi_room) )
2028+ . join ( )
2029+ . expect ( "Room::drop panicked on a non-tokio thread" ) ;
2030+ }
2031+ }
0 commit comments