Skip to content

Commit a726ae6

Browse files
committed
Drop refcell borrows before calling into R
1 parent 16c7857 commit a726ae6

1 file changed

Lines changed: 41 additions & 32 deletions

File tree

crates/ark/src/plots/graphics_device.rs

Lines changed: 41 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
117122
pub(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

Comments
 (0)