@@ -114,6 +114,11 @@ struct PlotContext {
114114/// R's callback registration layer. A future refactor could wrap the C-to-Rust
115115/// bridge so that the Rust-facing hook methods receive `&mut self` explicitly,
116116/// containing the `Console::get()` unsoundness in one place.
117+ ///
118+ /// NOTE: Never hold a `RefCell` borrow while calling into R (`RObject::from`,
119+ /// `RFunction::call`, `libr::Rf_*`, etc.). Any R call can in principle re-enter
120+ /// Rust (e.g. via finalizers during GC), so keeping borrows short avoids
121+ /// `RefCell` panics.
117122pub ( crate ) struct DeviceContext {
118123 /// Whether we are running in Console, Notebook, or Background mode.
119124 session_mode : SessionMode ,
@@ -547,7 +552,7 @@ impl DeviceContext {
547552
548553 // If the currently active plot is closed, advance to a new Positron page
549554 // See https://github.com/posit-dev/positron/issues/6702.
550- if * self . id . borrow ( ) == * id {
555+ if self . id ( ) == * id {
551556 self . new_positron_page ( ) ;
552557 }
553558 }
@@ -1267,37 +1272,41 @@ unsafe extern "C-unwind" fn ps_graphics_get_metadata(id: SEXP) -> anyhow::Result
12671272 let id_str: String = RObject :: view ( id) . try_into ( ) ?;
12681273 let plot_id = PlotId ( id_str) ;
12691274
1270- let contexts = Console :: get ( ) . device_context ( ) . plot_contexts . borrow ( ) ;
1271- match contexts. get ( & plot_id) {
1272- Some ( ctx) => {
1273- let info = & ctx. metadata ;
1274- let origin_uri = info. origin . as_ref ( ) . map ( |o| o. uri . as_str ( ) ) . unwrap_or ( "" ) ;
1275-
1276- // Create a list with the metadata values
1277- let values: Vec < RObject > = vec ! [
1278- RObject :: from( info. name. as_str( ) ) ,
1279- RObject :: from( info. kind. as_str( ) ) ,
1280- RObject :: from( info. execution_id. as_str( ) ) ,
1281- RObject :: from( info. code. as_str( ) ) ,
1282- RObject :: from( origin_uri) ,
1283- ] ;
1284- let list = RObject :: try_from ( values) ?;
1285-
1286- // Set the names attribute
1287- let names: Vec < String > = vec ! [
1288- "name" . to_string( ) ,
1289- "kind" . to_string( ) ,
1290- "execution_id" . to_string( ) ,
1291- "code" . to_string( ) ,
1292- "origin_uri" . to_string( ) ,
1293- ] ;
1294- let names = RObject :: from ( names) ;
1295- libr:: Rf_setAttrib ( list. sexp , libr:: R_NamesSymbol , names. sexp ) ;
1296-
1297- Ok ( list. sexp )
1298- } ,
1299- None => Ok ( harp:: r_null ( ) ) ,
1300- }
1275+ // Clone metadata out of the borrow before calling into R. R allocations
1276+ // (`RObject::from()`, `Rf_setAttrib()`, etc.) can trigger finalizers or
1277+ // error handlers that re-enter `plot_contexts.borrow_mut()`, which would
1278+ // panic if the shared borrow were still held.
1279+ let metadata = {
1280+ let contexts = Console :: get ( ) . device_context ( ) . plot_contexts . borrow ( ) ;
1281+ contexts. get ( & plot_id) . map ( |ctx| ctx. metadata . clone ( ) )
1282+ } ;
1283+
1284+ let Some ( info) = metadata else {
1285+ return Ok ( harp:: r_null ( ) ) ;
1286+ } ;
1287+
1288+ let origin_uri = info. origin . as_ref ( ) . map ( |o| o. uri . as_str ( ) ) . unwrap_or ( "" ) ;
1289+
1290+ let values: Vec < RObject > = vec ! [
1291+ RObject :: from( info. name. as_str( ) ) ,
1292+ RObject :: from( info. kind. as_str( ) ) ,
1293+ RObject :: from( info. execution_id. as_str( ) ) ,
1294+ RObject :: from( info. code. as_str( ) ) ,
1295+ RObject :: from( origin_uri) ,
1296+ ] ;
1297+ let list = RObject :: try_from ( values) ?;
1298+
1299+ let names: Vec < String > = vec ! [
1300+ "name" . to_string( ) ,
1301+ "kind" . to_string( ) ,
1302+ "execution_id" . to_string( ) ,
1303+ "code" . to_string( ) ,
1304+ "origin_uri" . to_string( ) ,
1305+ ] ;
1306+ let names = RObject :: from ( names) ;
1307+ libr:: Rf_setAttrib ( list. sexp , libr:: R_NamesSymbol , names. sexp ) ;
1308+
1309+ Ok ( list. sexp )
13011310}
13021311
13031312/// Return the current plot ID. Used by tests to verify that layout panels
0 commit comments