miniextendr_api/unwind_protect.rs
1//! Safe API for R's `R_UnwindProtect`
2//!
3//! This module provides [`with_r_unwind_protect`] for handling R errors with Rust cleanup.
4//! It automatically runs Rust destructors when R errors occur.
5//!
6//! **Important**: R uses `longjmp` for error handling, which normally bypasses Rust destructors.
7//! Use this API to ensure cleanup happens even when R errors occur.
8//!
9//! ## When to reach for this
10//!
11//! - **Calling R APIs that can error from a body you wrote yourself**
12//! (custom ALTREP, custom connection trampoline, hand-rolled FFI shim).
13//! Wrap the R-calling section in [`with_r_unwind_protect`] so Rust
14//! destructors run if R longjmps.
15//! - **Inside a [`with_r_unwind_protect`] body it is safe to use `*_unchecked`
16//! variants of the R FFI** — see the [`crate::sys`] module doc. The lint
17//! **MXL301** recognises this as one of the three contexts where bypassing
18//! the main-thread assertion is valid (the other two being ALTREP callbacks
19//! and [`crate::worker::with_r_thread`] bodies).
20//!
21//! ## You probably don't need this from a `#[miniextendr]` body
22//!
23//! The proc-macro already wraps every function and method in a guard that
24//! converts panics into the tagged-condition transport ([`crate::error_value`]).
25//! Returning `Result::Err`, `Option::None`, or calling `panic!()` /
26//! [`crate::error!`] / [`crate::warning!`] / [`crate::message!`] is the
27//! idiomatic path. Direct [`with_r_unwind_protect`] use inside that body is
28//! almost always wrong — you'd be nesting an `R_UnwindProtect` inside another
29//! `R_UnwindProtect`, paying the longjmp-leak cost twice (see "Leaks" below).
30//!
31//! ## Don't use `Rf_error`
32//!
33//! `Rf_error` and `Rf_errorcall` longjmp directly, skipping every Rust
34//! destructor on the stack. The lint **MXL300** forbids them in user code.
35//! Panic instead (or call [`crate::error!`]) and the framework raises the
36//! corresponding R condition for you.
37//!
38//! ## Leaks
39//!
40//! On the R longjmp path (when R unwinds out of the protected body),
41//! `with_r_unwind_protect` leaks ~8 bytes (an `RErrorMarker` + `Box` header)
42//! because the cleanup handler can't reclaim them via
43//! `Box::from_raw`. Regular Rust panics from inside the body don't leak.
44//! This is the cost MXL300 is buying off: every direct `Rf_error()` would
45//! incur the same leak with no observability.
46//!
47//! ## Log drain
48//!
49//! Every call to `with_r_unwind_protect` (and its variants) drains the
50//! cross-thread log queue via the crate-private `drain_log_queue_if_available`
51//! helper before returning or re-raising an R error. This ensures that records
52//! buffered by worker threads are flushed to R's console on every FFI exit —
53//! including error paths.
54//!
55//! ## Cross references
56//!
57//! - [`crate::worker::with_r_thread`] — routes a closure to R's main thread.
58//! - [`crate::ffi_guard`] — unified panic-catching trampoline that consumes
59//! `with_r_unwind_protect_sourced` for ALTREP `RUnwind` mode.
60//! - [`crate::error_value`] / [`mod@crate::condition`] — panic → R condition
61//! transport.
62use std::{
63 any::Any,
64 borrow::Cow,
65 cell::Cell,
66 ffi::c_void,
67 panic::{AssertUnwindSafe, catch_unwind},
68 sync::OnceLock,
69};
70
71// region: raise_rust_condition_via_stop — Approach 3 for ALTREP RUnwind path
72
73/// Raise the R condition for a tagged condition value by evaluating the
74/// shared `.miniextendr_raise_condition` helper on it, the helper every
75/// generated R wrapper calls on the value its `.Call()` returns.
76///
77/// This is **Approach 3** from the issue-345 plan: an `Rf_eval` that ends in
78/// `stop(structure(...))`, for contexts with no outer R wrapper to inspect a
79/// tagged SEXP. It is the only viable option for ALTREP callbacks, which are
80/// invoked directly by R's runtime (no `.Call` frame, no R wrapper).
81///
82/// `tagged_value` builds the value (with [`crate::error_value`]'s
83/// constructors). Because the helper raises it, the condition is the one the
84/// wrapper raises for the same value, field for field (#1768): `message`,
85/// `call`, `kind`, then the `data =` fields spliced with `utils::modifyList`,
86/// so a data field named `message`, `call` or `kind` replaces the base field
87/// on both transports (#1315); the classes ahead of the `rust_*` layering.
88/// The copy evaluated here lives in the base namespace, so the caller
89/// resolves `arg_error!`'s class marker in Rust first.
90///
91/// The value must be of an error kind (`stop()` longjmps, so this function
92/// never returns); the helper's warning / message / condition arms return.
93///
94/// ## Nothing Rust-owned survives the longjmp
95///
96/// The longjmp skips every Rust frame above R's, so no destructor runs past
97/// this point. `tagged_value` consumes what it captures (message, classes,
98/// data) and is itself dropped before `Rf_eval`; the SEXPs are rooted with
99/// plain `Rf_protect`, whose stack R resets on the jump, rather than a
100/// drop guard that would never run.
101///
102/// ## MXL300 compliance
103///
104/// This function raises an R error via `Rf_eval(stop(...))`, not via direct
105/// `Rf_error`/`Rf_errorcall`. MXL300 does not flag `Rf_eval`.
106///
107/// # Safety
108///
109/// Must be called from R's main thread in a context where R longjmps are
110/// safe, with no Rust value that needs dropping left on the caller's frames.
111/// In practice, always called from `with_r_unwind_protect_sourced` (ALTREP
112/// `RUnwind` guards, [`with_r_unwind_protect_or_raise`]) after its guarded
113/// closure has returned.
114pub(crate) unsafe fn raise_rust_condition_via_stop(
115 tagged_value: impl FnOnce() -> SEXP,
116 call: Option<SEXP>,
117) -> ! {
118 use crate::sys::{R_BaseEnv, Rf_eval, Rf_lang3, Rf_protect};
119
120 unsafe {
121 let helper = crate::deferred_condition::raise_condition_helper();
122 let tagged = Rf_protect(tagged_value());
123 let expr = Rf_protect(Rf_lang3(helper, tagged, call.unwrap_or(SEXP::nil())));
124 // Longjmps through `stop()`; R pops the protect stack on the way out.
125 Rf_eval(expr, R_BaseEnv);
126 }
127 // Only a non-error tagged value (a bug in the caller) gets here, and
128 // unwinding out of an ALTREP callback would cross R's C frames.
129 std::process::abort()
130}
131
132/// The tagged value of a generic panic, `kind = "panic"`: what
133/// [`with_r_unwind_protect`] returns for one.
134fn panic_condition_value(message: &str, call: Option<SEXP>) -> SEXP {
135 // SAFETY: only called by `raise_rust_condition_via_stop`, on the main thread.
136 unsafe {
137 crate::error_value::make_rust_condition_value(
138 message,
139 crate::error_value::kind::PANIC,
140 None,
141 call,
142 )
143 }
144}
145
146// endregion
147
148use crate::sys::{self, R_ContinueUnwind, R_UnwindProtect_C_unwind};
149use crate::{Rboolean, SEXP};
150
151/// Global continuation token for R_UnwindProtect.
152///
153/// Using a single global token instead of thread-local tokens avoids leaking
154/// one token per thread that uses `with_r_unwind_protect`.
155///
156/// # Safety
157///
158/// The token is created and preserved once during first use. It remains valid
159/// for the entire R session.
160static R_CONTINUATION_TOKEN: OnceLock<SEXP> = OnceLock::new();
161
162/// Get or create the global continuation token.
163///
164/// This is public for use by the worker module.
165pub(crate) fn get_continuation_token() -> SEXP {
166 *R_CONTINUATION_TOKEN.get_or_init(|| {
167 // The continuation token must be created on R's main thread
168 // (R_MakeUnwindCont is an R API call). OnceLock ensures it is
169 // only created once and safely shared.
170 unsafe {
171 let token = sys::R_MakeUnwindCont();
172 sys::R_PreserveObject(token);
173 token
174 }
175 })
176}
177
178/// Panic payload whose message is already final (location folded, or
179/// deliberately location-free): downstream folds must use it verbatim and
180/// must NOT append the current thread's recorded panic location (#1245).
181///
182/// Produced by `worker::route_to_main_thread`'s re-panic when a `with_r_thread`
183/// closure panics on the main thread: the main-thread stringify point already
184/// folded the *true* origin location into the message before it crossed back
185/// to the worker, so the worker's own re-panic (needed to unwind out of
186/// `run_on_worker`) must carry that message forward untouched rather than
187/// re-fold its own relay call site on top.
188pub(crate) struct PreLocatedPanic(pub(crate) String);
189
190/// Extract a message from a panic payload.
191///
192/// Handles `&str`, `String`, `&String`, `PreLocatedPanic` and
193/// [`RCondition`](enum@crate::condition::RCondition) payloads (the condition's
194/// message) consistently. The borrowed variants are returned as
195/// `Cow::Borrowed`, so the common `panic!("literal")` case avoids the heap
196/// allocation that a `String` return would force. Unrecognised payload types
197/// fall back to a `Cow::Borrowed` static string.
198///
199/// Call `.into_owned()` (or `.to_string()`) at sites that need an owned
200/// `String`.
201pub fn panic_payload_to_string(payload: &(dyn Any + Send)) -> Cow<'_, str> {
202 if let Some(&s) = payload.downcast_ref::<&str>() {
203 Cow::Borrowed(s)
204 } else if let Some(s) = payload.downcast_ref::<String>() {
205 Cow::Borrowed(s.as_str())
206 } else if let Some(s) = payload.downcast_ref::<&String>() {
207 Cow::Borrowed(s.as_str())
208 } else if let Some(pre) = payload.downcast_ref::<PreLocatedPanic>() {
209 Cow::Borrowed(pre.0.as_str())
210 } else if let Some(condition) = payload.downcast_ref::<crate::condition::RCondition>() {
211 Cow::Borrowed(condition.message())
212 } else {
213 Cow::Borrowed("unknown panic")
214 }
215}
216
217/// Stringify a panic payload and fold in the Rust source location the panic
218/// hook recorded on the *current* thread, producing the final R-facing message.
219///
220/// Returns `panic_payload_to_string(payload)` with a `\n(at file:line)` suffix
221/// when [`crate::backtrace::take_last_panic_location`] has a location for this
222/// thread, otherwise the bare message. The location is captured in the process
223/// panic hook (`backtrace.rs`), which fires on the panicking thread — so this
224/// **must be called on the same thread that ran the panicking closure** (main
225/// for main-thread `#[miniextendr]` fns; the worker for worker-dispatched fns).
226///
227/// Only the *generic panic* path uses this. `error!`/`warning!`/`message!`/
228/// `condition!` (and `Result::Err` / `Option::None`) travel the typed
229/// `RCondition` / tagged-value branches and are deliberately left byte-for-byte
230/// unchanged — they carry no location suffix.
231pub(crate) fn panic_message_with_location(payload: &(dyn Any + Send)) -> String {
232 let msg = panic_payload_to_string(payload);
233 match crate::backtrace::take_last_panic_location() {
234 Some((file, line)) => format!("{msg}\n(at {file}:{line})"),
235 None => msg.into_owned(),
236 }
237}
238
239// region: R exits carried as a Rust unwind (#1835)
240
241thread_local! {
242 /// The innermost [`Boundary`] on this thread; `0` outside any.
243 static BOUNDARY: Cell<u64> = const { Cell::new(0) };
244 /// The last boundary id handed out on this thread.
245 static LAST_BOUNDARY: Cell<u64> = const { Cell::new(0) };
246}
247
248/// A miniextendr boundary that resumes R exits ([`RUnwind`]): the innermost on
249/// its thread from [`enter`](Self::enter) until it is dropped or
250/// [`leave`](Self::leave)s.
251///
252/// An exit belongs to the boundary that was innermost when its evaluation
253/// started, and only that boundary may resume it. Its jump target is an R
254/// context on the C stack, alive only while that boundary's call is: an exit
255/// caught with `catch_unwind`, kept, and resumed after the call returned would
256/// jump into a frame that no longer exists. [`resume_if_r_unwind`] refuses it.
257pub(crate) struct Boundary {
258 id: u64,
259 outer: u64,
260}
261
262impl Boundary {
263 /// Become the innermost boundary on this thread.
264 #[inline]
265 pub(crate) fn enter() -> Self {
266 let id = LAST_BOUNDARY.with(|last| {
267 let id = last.get() + 1;
268 last.set(id);
269 id
270 });
271 let outer = BOUNDARY.with(|current| current.replace(id));
272 Boundary { id, outer }
273 }
274
275 /// Make the enclosing boundary the innermost again. Call it before leaving
276 /// by an R jump, which skips `Drop`.
277 #[inline]
278 pub(crate) fn leave(&self) {
279 BOUNDARY.with(|current| current.set(self.outer));
280 }
281}
282
283impl Drop for Boundary {
284 #[inline]
285 fn drop(&mut self) {
286 self.leave();
287 }
288}
289
290/// An R non-local exit that [`call_unwinding`] carries out of its callback
291/// as a Rust unwind: an error, a condition caught by an exiting handler set up
292/// outside the call (`tryCatch(warning = )`), a restart, an interrupt.
293///
294/// The unwind runs the destructors of the Rust frames between the evaluation
295/// and the boundary that catches it. The boundary ([`run_r_unwind_protect`],
296/// the worker's main-thread loop, the FFI guards) then resumes the exit with
297/// `R_ContinueUnwind`, and R carries on to the context it was jumping to, with
298/// the condition object and call it was jumping with.
299///
300/// The continuation token is private to one evaluation and preserved until
301/// the exit resumes, so R code that a destructor runs during the unwind (a
302/// protected call of its own, which reuses the shared token) cannot overwrite
303/// the pending jump or let the collector reclaim it (#1507).
304pub(crate) struct RUnwind {
305 /// `R_MakeUnwindCont()`, preserved: the jump target and the value R was
306 /// jumping with.
307 token: SEXP,
308 /// The [`Boundary`] that was innermost when the evaluation started.
309 boundary: u64,
310}
311
312impl RUnwind {
313 /// Whether `boundary` may resume this exit: the boundary its evaluation
314 /// ran in, still on the stack.
315 #[inline]
316 pub(crate) fn belongs_to(&self, boundary: &Boundary) -> bool {
317 self.boundary == boundary.id
318 }
319
320 /// Continue the R exit where `R_UnwindProtect` stopped it. Never returns.
321 ///
322 /// # Safety
323 ///
324 /// R's main thread, at the boundary the exit [`belongs_to`](Self::belongs_to),
325 /// after its [`Boundary::leave`], with no Rust value left to drop between
326 /// this call and the R context that owns the frame (the C entry point R
327 /// called).
328 pub(crate) unsafe fn resume(self) -> ! {
329 let token = self.token;
330 std::mem::forget(self);
331 unsafe {
332 // `R_jumpctxt` roots the value it jumps with only after the cleanups
333 // of the contexts it passes have run (it sets `R_ReturnedValue`
334 // after `R_run_onexits`), and those may allocate. The protection
335 // keeps the token, and with it the value, alive until then; the
336 // jump resets the protect stack (`R_restore_globals`), which ends it.
337 sys::Rf_protect(token);
338 sys::R_ReleaseObject(token);
339 R_ContinueUnwind(token)
340 }
341 }
342
343 /// The payload a boundary raises in place of an exit it may not resume
344 /// (see [`Boundary`]). The exit is dropped.
345 pub(crate) fn stray(self: Box<Self>) -> Box<dyn Any + Send> {
346 drop(self);
347 Box::new(
348 "an R exit (an error, a condition handler's exit or a restart) was caught with \
349 `catch_unwind` and resumed outside the call that raised it; it cannot continue",
350 )
351 }
352}
353
354impl Drop for RUnwind {
355 /// Reached when something other than the exit's boundary caught the unwind
356 /// and dropped it, or the boundary refused it ([`RUnwind::stray`]). The R
357 /// exit is abandoned: R carries on from where the evaluation stopped, as
358 /// after `R_tryEval`. Release the token, on R's main thread: dropped on
359 /// another thread (code that moved the caught payload there), it waits in
360 /// [`ABANDONED_OFF_MAIN`] for the next evaluation.
361 fn drop(&mut self) {
362 if crate::worker::is_r_main_thread() {
363 unsafe { sys::R_ReleaseObject(self.token) }
364 } else {
365 ABANDONED_OFF_MAIN
366 .lock()
367 .unwrap_or_else(std::sync::PoisonError::into_inner)
368 .push(self.token);
369 HAS_ABANDONED_OFF_MAIN.store(true, std::sync::atomic::Ordering::Release);
370 }
371 }
372}
373
374/// Tokens of R exits dropped off R's main thread, where `R_ReleaseObject`
375/// would race R. [`call_unwinding`] releases them on the main thread.
376static ABANDONED_OFF_MAIN: std::sync::Mutex<Vec<SEXP>> = std::sync::Mutex::new(Vec::new());
377
378/// Whether [`ABANDONED_OFF_MAIN`] may hold a token: the evaluation's fast path.
379static HAS_ABANDONED_OFF_MAIN: std::sync::atomic::AtomicBool =
380 std::sync::atomic::AtomicBool::new(false);
381
382/// Release the tokens of exits dropped off the main thread.
383///
384/// # Safety
385///
386/// R's main thread.
387unsafe fn release_abandoned_off_main() {
388 if HAS_ABANDONED_OFF_MAIN.swap(false, std::sync::atomic::Ordering::Acquire) {
389 let tokens = std::mem::take(
390 &mut *ABANDONED_OFF_MAIN
391 .lock()
392 .unwrap_or_else(std::sync::PoisonError::into_inner),
393 );
394 for token in tokens {
395 unsafe { sys::R_ReleaseObject(token) };
396 }
397 }
398}
399
400/// Resume `payload` if it carries an R exit ([`RUnwind`]) that belongs to
401/// `boundary`; return it otherwise, or [`RUnwind::stray`] for an exit from
402/// elsewhere. Conditions deferred since `deferred_mark` belong to the call the
403/// exit abandons and are discarded first, as on the R-error path.
404///
405/// # Safety
406///
407/// As [`RUnwind::resume`]: R's main thread, at `boundary`, with nothing left
408/// to drop above the C entry point.
409pub(crate) unsafe fn resume_if_r_unwind(
410 payload: Box<dyn Any + Send>,
411 deferred_mark: usize,
412 boundary: &Boundary,
413) -> Box<dyn Any + Send> {
414 match payload.downcast::<RUnwind>() {
415 Ok(exit) if exit.belongs_to(boundary) => {
416 crate::deferred_condition::discard(deferred_mark);
417 boundary.leave();
418 unsafe { exit.resume() }
419 }
420 Ok(exit) => exit.stray(),
421 Err(payload) => payload,
422 }
423}
424
425/// Evaluate `expr` in `env` with `Rf_eval`, in the caller's R context: the
426/// condition handlers and restarts established around the `.Call()` stay in
427/// place, unlike under `R_tryEval`, whose `R_ToplevelExec` empties the handler
428/// and restart stacks for the duration. An R exit leaves as [`RUnwind`]
429/// ([`call_unwinding`]).
430///
431/// # Safety
432///
433/// As [`call_unwinding`]; `expr` and `env` are rooted for the call, `env` an
434/// environment. The result is unprotected.
435pub(crate) unsafe fn eval_unwinding(expr: SEXP, env: SEXP) -> SEXP {
436 struct EvalData {
437 expr: SEXP,
438 env: SEXP,
439 }
440
441 unsafe extern "C-unwind" fn eval(data: *mut c_void) -> SEXP {
442 let data = unsafe { &*data.cast::<EvalData>() };
443 unsafe { sys::Rf_eval(data.expr, data.env) }
444 }
445
446 let mut data = EvalData { expr, env };
447 unsafe { call_unwinding(eval, (&raw mut data).cast()) }
448}
449
450/// Call `fun(data)` in its own `R_UnwindProtect`, in the caller's R context,
451/// and return its value.
452///
453/// An R exit from `fun` (an error, an exiting handler of the caller's
454/// `tryCatch()`, a restart, an interrupt) stops at that frame, never jumping
455/// over a Rust frame. Its cleanup turns the exit into a Rust unwind
456/// ([`RUnwind`]); the boundary resumes it.
457///
458/// # Safety
459///
460/// R's main thread, inside a miniextendr boundary (a `#[miniextendr]` body, a
461/// `with_r_unwind_protect*` closure, a guarded callback). `fun` holds nothing
462/// to drop across an R jump (it is called from C), and what `data` points to
463/// outlives the call. The result is unprotected.
464pub(crate) unsafe fn call_unwinding(
465 fun: unsafe extern "C-unwind" fn(*mut c_void) -> SEXP,
466 data: *mut c_void,
467) -> SEXP {
468 struct CallData {
469 fun: unsafe extern "C-unwind" fn(*mut c_void) -> SEXP,
470 data: *mut c_void,
471 token: SEXP,
472 boundary: u64,
473 }
474
475 unsafe extern "C-unwind" fn trampoline(data: *mut c_void) -> SEXP {
476 let data = unsafe { &*data.cast::<CallData>() };
477 // An R exit from here jumps to `R_UnwindProtect`, over no Rust frame
478 // that holds anything to drop.
479 unsafe { (data.fun)(data.data) }
480 }
481
482 unsafe extern "C-unwind" fn cleanup(data: *mut c_void, jump: Rboolean) {
483 if jump != Rboolean::FALSE {
484 let data = unsafe { &*data.cast::<CallData>() };
485 // A boundary the R code entered and an R jump left skipped its
486 // `leave`; this evaluation's boundary is the innermost again.
487 BOUNDARY.with(|current| current.set(data.boundary));
488 // R stopped its jump at our frame and would continue it after this
489 // returns. Carry it out as a Rust unwind instead. `resume_unwind`
490 // skips the panic hook: this is no panic.
491 std::panic::resume_unwind(Box::new(RUnwind {
492 token: data.token,
493 boundary: data.boundary,
494 }));
495 }
496 }
497
498 unsafe {
499 release_abandoned_off_main();
500 let token = sys::R_MakeUnwindCont();
501 sys::R_PreserveObject(token);
502 let boundary = BOUNDARY.with(Cell::get);
503 let mut data = CallData {
504 fun,
505 data,
506 token,
507 boundary,
508 };
509 let value = R_UnwindProtect_C_unwind(
510 Some(trampoline),
511 (&raw mut data).cast(),
512 Some(cleanup),
513 (&raw mut data).cast(),
514 token,
515 );
516 BOUNDARY.with(|current| current.set(boundary));
517 sys::R_ReleaseObject(token);
518 value
519 }
520}
521
522// endregion
523
524// region: Log drain integration
525
526/// Drain the cross-thread log queue if the `log` feature is enabled.
527///
528/// This is called at every exit point of `run_r_unwind_protect` (normal
529/// return, Rust panic, and immediately before `R_ContinueUnwind`) so that
530/// worker-thread log records always reach R's console before the FFI call
531/// returns or re-raises an R error.
532///
533/// When the `log` feature is disabled this compiles to a no-op; there is
534/// no runtime overhead.
535#[inline]
536fn drain_log_queue_if_available() {
537 #[cfg(feature = "log")]
538 crate::optionals::log_impl::drain_log_queue();
539}
540
541// endregion
542
543/// Core R_UnwindProtect wrapper. Returns `Ok(result)` on success,
544/// `Err(payload)` on Rust panic, or diverges via `R_ContinueUnwind` on R longjmp.
545///
546/// Handles: CallData boxing, trampoline, cleanup handler, continuation token,
547/// `Box::from_raw` reclamation on all non-diverging paths.
548///
549/// Drains the cross-thread log queue (when the `log` feature is enabled) at
550/// each exit point so worker-thread records reach R's console before the FFI
551/// boundary is crossed.
552fn run_r_unwind_protect<F, R>(f: F) -> Result<R, Box<dyn Any + Send>>
553where
554 F: FnOnce() -> R,
555{
556 /// Marker type for R errors caught by R_UnwindProtect's cleanup handler.
557 struct RErrorMarker;
558
559 struct CallData<F, R> {
560 f: Option<F>,
561 result: Option<R>,
562 panic_payload: Option<Box<dyn Any + Send>>,
563 }
564
565 unsafe extern "C-unwind" fn trampoline<F, R>(data: *mut c_void) -> SEXP
566 where
567 F: FnOnce() -> R,
568 {
569 assert!(!data.is_null(), "trampoline: data pointer is null");
570 let data = unsafe { &mut *data.cast::<CallData<F, R>>() };
571 let f = data.f.take().expect("trampoline: closure already consumed");
572
573 match catch_unwind(AssertUnwindSafe(f)) {
574 Ok(result) => {
575 data.result = Some(result);
576 crate::SEXP::nil()
577 }
578 Err(payload) => {
579 data.panic_payload = Some(payload);
580 crate::SEXP::nil()
581 }
582 }
583 }
584
585 unsafe extern "C-unwind" fn cleanup_handler(_data: *mut c_void, jump: Rboolean) {
586 if jump != Rboolean::FALSE {
587 // R is about to longjmp - trigger a Rust panic so we can unwind properly
588 std::panic::panic_any(RErrorMarker);
589 }
590 }
591
592 let deferred_mark = crate::deferred_condition::mark();
593 let boundary = Boundary::enter();
594 unsafe {
595 let token = get_continuation_token();
596
597 let data = Box::into_raw(Box::new(CallData::<F, R> {
598 f: Some(f),
599 result: None,
600 panic_payload: None,
601 }));
602
603 let panic_result = catch_unwind(AssertUnwindSafe(|| {
604 R_UnwindProtect_C_unwind(
605 Some(trampoline::<F, R>),
606 data.cast(),
607 Some(cleanup_handler),
608 std::ptr::null_mut(),
609 token,
610 )
611 }));
612
613 let mut data = Box::from_raw(data);
614
615 match panic_result {
616 Ok(_) => {
617 // Check if trampoline caught a panic
618 if let Some(payload) = data.panic_payload.take() {
619 drop(data);
620 // Drain worker-thread log records before returning the panic
621 // payload to the caller (which will convert it to an R error).
622 drain_log_queue_if_available();
623 // An R exit carried out of `call_unwinding` (#1835): the Rust
624 // frames it crossed have dropped their values; continue it.
625 let payload = resume_if_r_unwind(payload, deferred_mark, &boundary);
626 Err(payload)
627 } else {
628 // Normal completion - return the result
629 let result = data
630 .result
631 .take()
632 .expect("result not set after successful completion");
633 drop(data);
634 // Drain worker-thread log records on the normal success path.
635 drain_log_queue_if_available();
636 Ok(result)
637 }
638 }
639 Err(payload) => {
640 // Drop data first to run destructors
641 drop(data);
642 // Check if this was an R error or a Rust panic
643 if payload.downcast_ref::<RErrorMarker>().is_some() {
644 // R error - drain log records before re-raising so worker
645 // thread output is not lost even on error exits.
646 drain_log_queue_if_available();
647 // No signalling while resuming an R error or exiting handler.
648 // Conditions from the abandoned call must not survive it.
649 crate::deferred_condition::discard(deferred_mark);
650 boundary.leave();
651 // Continue R's unwind (diverges, never returns)
652 R_ContinueUnwind(token);
653 } else {
654 // Rust panic — drain before returning the payload.
655 drain_log_queue_if_available();
656 Err(payload)
657 }
658 }
659 }
660 }
661}
662
663/// Execute a closure with R unwind protection, raising any Rust panic as the R
664/// error a generated wrapper would raise for it.
665///
666/// If the closure panics, the panic is caught and converted to an R error
667/// (longjmp) with `rust_*` class layering. If R raises an error (longjmp), all
668/// Rust RAII resources are properly dropped before R continues unwinding.
669///
670/// **This is NOT the user-facing path for `#[miniextendr]` functions.** That
671/// path is [`with_r_unwind_protect`], which returns a tagged SEXP instead of
672/// longjmping (the macro-generated R wrapper raises the structured condition).
673///
674/// This raising-variant exists for guard sites that have no R wrapper between
675/// them and R's runtime:
676/// - ALTREP `RUnwind` guard callbacks (via the crate-private
677/// `with_r_unwind_protect_sourced`)
678/// - FFI guard tests / benchmarks exercising the raw `R_UnwindProtect` mechanism
679///
680/// In those contexts there is no consumer-side R wrapper to inspect a tagged
681/// SEXP. Panics are routed through `raise_rust_condition_via_stop` so they
682/// still receive `rust_*` class layering (issue #345), the same `kind` and
683/// the same fields as the wrapper's condition (#1768). Trait-ABI shims use a
684/// separate SEXP-returning variant ([`with_r_unwind_protect_shim`]) that
685/// re-panics at the View boundary.
686///
687/// # Arguments
688///
689/// * `f` - The closure to execute
690/// * `call` - Optional R call SEXP for better error messages
691///
692/// Deferred conditions are signalled before the outcome is returned or raised.
693/// Any R objects held by the generic return value must remain rooted during
694/// signalling. Return an [`crate::OwnedProtect`] for a freshly allocated SEXP,
695/// then call its `get()` after this guard returns. ALTREP's SEXP trampolines do
696/// this automatically. An R-origin error discards this call's queued conditions.
697pub fn with_r_unwind_protect_or_raise<F, R>(f: F, call: Option<SEXP>) -> R
698where
699 F: FnOnce() -> R,
700{
701 with_r_unwind_protect_sourced(f, call, crate::panic_telemetry::PanicSource::UnwindProtect)
702}
703
704/// Like [`with_r_unwind_protect_or_raise`], but reports panics with a custom
705/// `PanicSource`.
706///
707/// Used by `guarded_altrep_call` so that panics inside ALTREP callbacks with
708/// `AltrepGuard::RUnwind` are still attributed to `PanicSource::Altrep`.
709///
710/// Every outcome is raised through [`raise_rust_condition_via_stop`], which
711/// hands the tagged value [`with_r_unwind_protect`] would return to the R
712/// helper the generated wrappers call, so the condition is the wrapper's, with
713/// its `kind` (#1768). This works even in ALTREP callback context, where there
714/// is no R wrapper to inspect a tagged SEXP (Approach 3 from the issue-345
715/// plan):
716///
717/// - `RCondition::Error` (`error!()`) — `kind = "error"`, its classes ahead of
718/// the `rust_*` layering, its `data =` fields.
719///
720/// - `RCondition::Conversion` (`arg_error!`) — `kind = "conversion"`, with the
721/// crate-class marker replaced in Rust by the classes `miniextendr_init!`
722/// registered: no generated R is there to resolve it.
723///
724/// - A generic panic — `kind = "panic"`, with the `(at file:line)` suffix.
725///
726/// - `Warning`, `Message`, `Condition` — a plain R error with a diagnostic
727/// message and `kind = "panic"`. `warning!()`/`message!()` from ALTREP
728/// context cannot suspend execution for non-fatal signals; documented
729/// limitation.
730pub(crate) fn with_r_unwind_protect_sourced<F, R>(
731 f: F,
732 call: Option<SEXP>,
733 source: crate::panic_telemetry::PanicSource,
734) -> R
735where
736 F: FnOnce() -> R,
737{
738 let deferred_mark = crate::deferred_condition::mark();
739 let outcome = run_r_unwind_protect(f);
740 // Preserve the original panic site before condition handlers can re-enter
741 // Rust and overwrite the panic hook's thread-local location.
742 let panic_message = outcome.as_ref().err().and_then(|payload| {
743 (!payload.is::<crate::condition::RCondition>())
744 .then(|| panic_message_with_location(payload.as_ref()))
745 });
746 let queued = crate::deferred_condition::take_pending(deferred_mark);
747 let (outcome, panic_message) = if queued.is_empty() {
748 (outcome, panic_message)
749 } else {
750 // Move both the outcome and saved panic text INTO this guard: an
751 // exiting R handler must drop all their Rust resources too.
752 // SEXP-bearing outcomes must own their roots.
753 match run_r_unwind_protect(move || {
754 unsafe { crate::deferred_condition::signal_now(queued, call) };
755 (outcome, panic_message)
756 }) {
757 Ok(outcome) => outcome,
758 Err(payload) => (Err(payload), None),
759 }
760 };
761 match outcome {
762 Ok(result) => result,
763 Err(payload) => {
764 // region: RCondition recognition for the raising-variant path
765 if payload.is::<crate::condition::RCondition>() {
766 // Take ownership so the payload's parts move into the tagged
767 // value without cloning — same idiom as `with_r_unwind_protect_shim`.
768 let cond = *payload
769 .downcast::<crate::condition::RCondition>()
770 .expect("checked is::<RCondition> above");
771 let error_kind = matches!(
772 cond,
773 crate::condition::RCondition::Error { .. }
774 | crate::condition::RCondition::Conversion { .. }
775 );
776 // The kind is the one the tagged path writes for this payload.
777 let (kind, mut parts) = cond.into_parts();
778 if error_kind {
779 // Approach 3 (issue-345): the helper the wrappers use raises
780 // the value, so `tryCatch(rust_error = h, ...)`,
781 // `tryCatch(my_class = h, ...)`, `e$kind` and the `data`
782 // fields (issue #996 path 2) all match the wrapper's
783 // condition. That copy of the helper cannot resolve an
784 // `arg_error!`'s class marker (#1740): Rust puts the
785 // package's registered classes in its place.
786 parts.class = crate::condition::resolve_conversion_error_class_marker(
787 parts.class,
788 crate::condition::crate_conversion_error_class(),
789 );
790 crate::panic_telemetry::fire(&parts.message, source);
791 unsafe {
792 raise_rust_condition_via_stop(
793 move || crate::error_value::condition_parts_value(kind, parts, call),
794 call,
795 )
796 }
797 } else {
798 // warning!/message!/condition! cannot be cleanly raised from an ALTREP
799 // or connection callback (no mechanism to suspend execution for
800 // non-fatal signals).
801 // Documented degradation: a plain R error with a fixed diagnostic,
802 // raised like the generic-panic branch below (`kind = "panic"`,
803 // `rust_error` class layering, issue #366): an error reporting the
804 // original kind would claim to be a warning. The classes and data
805 // fields are dropped along with the message; the call choice is
806 // kept (`call = none` drops the guard's call).
807 let call = parts.call.apply(call);
808 drop(parts);
809 let msg = "warning!/message!/condition! from an ALTREP or connection \
810 callback context cannot be raised as non-fatal signals; use \
811 error!() instead. This context has no R wrapper to handle \
812 signal restart.";
813 crate::panic_telemetry::fire(msg, source);
814 unsafe {
815 raise_rust_condition_via_stop(|| panic_condition_value(msg, call), call)
816 }
817 }
818 } else {
819 // Generic panic — no class layering, plain error string plus the
820 // `(at file:line)` suffix folded from the panic hook (this branch
821 // runs on the panicking thread for the ALTREP/FFI-guard path).
822 // Fire telemetry and raise via Approach 3 with rust_error class so
823 // tryCatch(rust_error = h, ...) matches even for plain panics.
824 let msg =
825 panic_message.unwrap_or_else(|| panic_message_with_location(payload.as_ref()));
826 // The raise longjmps over this frame: free the payload first.
827 drop(payload);
828 crate::panic_telemetry::fire(&msg, source);
829 unsafe {
830 raise_rust_condition_via_stop(move || panic_condition_value(&msg, call), call)
831 }
832 }
833 // endregion
834 }
835 }
836}
837
838/// Like [`with_r_unwind_protect`], but tailored for trait-ABI vtable shims.
839///
840/// Same tagged-SEXP behaviour as [`with_r_unwind_protect`], but intended for
841/// shim functions that have no R wrapper of their own. The tagged SEXP is
842/// returned to the View method wrapper, which calls
843/// [`crate::condition::repanic_if_rust_error`] to re-panic with the
844/// reconstructed [`enum@crate::condition::RCondition`]. The outer
845/// `with_r_unwind_protect` in the consumer's C entry point then catches the
846/// re-panic and builds the final tagged SEXP for the consumer's R wrapper.
847///
848/// R-origin errors (longjmp) still pass through via `R_ContinueUnwind` — the
849/// outer guard will catch them.
850///
851/// # PROTECT note
852///
853/// The returned SEXP is unprotected. The View method wrapper must not call any
854/// R API functions between receiving it and passing it to
855/// `repanic_if_rust_error`. `repanic_if_rust_error` reads the message/kind/class
856/// strings immediately and then panics (or returns), so the SEXP does not need
857/// protection beyond that window.
858pub fn with_r_unwind_protect_shim<F>(f: F) -> SEXP
859where
860 F: FnOnce() -> SEXP,
861{
862 // Conditions queued with `defer_warning` & co. inside the shim are signalled
863 // at this boundary: the consumer's queue is a different static (every
864 // package links its own copy of this crate), so nothing may be left behind.
865 let deferred_mark = crate::deferred_condition::mark();
866 let result = match run_r_unwind_protect(f) {
867 Ok(result) => result,
868 Err(payload) => {
869 // region: RCondition recognition — same as the tagged-SEXP path
870 if payload.is::<crate::condition::RCondition>() {
871 // Take ownership of the payload so the `data` Vec can be moved
872 // into `make_rust_condition_value` (consumed when materialised).
873 // A call-less payload writes the "no call" marker, which
874 // `from_tagged_sexp` reads back on the consumer's side.
875 let cond = *payload
876 .downcast::<crate::condition::RCondition>()
877 .expect("checked is::<RCondition> above");
878 // SAFETY: on the R main thread inside R_UnwindProtect.
879 unsafe { crate::error_value::rust_condition_value(cond, None) }
880 } else {
881 // Generic panic path — fold the hook-captured `(at file:line)` into
882 // the message (this shim runs on the panicking thread).
883 let msg = panic_message_with_location(payload.as_ref());
884 crate::panic_telemetry::fire(
885 &msg,
886 crate::panic_telemetry::PanicSource::UnwindProtect,
887 );
888 // SAFETY: on the R main thread inside R_UnwindProtect.
889 unsafe {
890 crate::error_value::make_rust_condition_value(
891 &msg,
892 crate::error_value::kind::PANIC,
893 None,
894 None,
895 )
896 }
897 }
898 // endregion
899 }
900 };
901 // SAFETY: vtable shims run on R's main thread, inside the consumer's `.Call`.
902 unsafe { crate::deferred_condition::finish(deferred_mark, result, None) }
903}
904
905/// Run a closure under `R_UnwindProtect`, returning a tagged condition SEXP on
906/// Rust panics instead of raising an R error.
907///
908/// This is **the** transport for all `#[miniextendr]` functions and methods.
909/// The returned error/condition SEXP is inspected by the generated R wrapper
910/// which raises a proper R condition past the Rust boundary, with `rust_*`
911/// class layering.
912///
913/// Recognises [`enum@crate::condition::RCondition`] payloads (from `error!()`,
914/// `warning!()`, `message!()`, `condition!()`) before falling through to the
915/// generic panic→string path.
916///
917/// R-origin errors (longjmp) still pass through via `R_ContinueUnwind`.
918///
919/// For guard sites that have no R wrapper to inspect a tagged SEXP (ALTREP
920/// `RUnwind` callbacks, FFI guard tests) see [`with_r_unwind_protect_or_raise`];
921/// for trait-ABI vtable shims see [`with_r_unwind_protect_shim`].
922pub fn with_r_unwind_protect<F>(f: F, call: Option<SEXP>) -> SEXP
923where
924 F: FnOnce() -> SEXP,
925{
926 match run_r_unwind_protect(f) {
927 Ok(result) => result,
928 Err(payload) => {
929 // region: RCondition recognition — must come before generic panic path
930 if payload.is::<crate::condition::RCondition>() {
931 // Take ownership so the `data` payload can be moved into
932 // `make_rust_condition_value` (consumed during materialisation).
933 let cond = *payload
934 .downcast::<crate::condition::RCondition>()
935 .expect("checked is::<RCondition> above");
936 // No panic telemetry for user-raised conditions — they are intentional.
937 // SAFETY: on the R main thread inside R_UnwindProtect.
938 return unsafe { crate::error_value::rust_condition_value(cond, call) };
939 }
940 // endregion
941
942 // Generic panic path — the primary user-facing route for
943 // `#[miniextendr]` fns running on the main thread. Fold the
944 // hook-captured `(at file:line)` into the message. (The RCondition
945 // branch above is deliberately untouched: error!/warning!/message!/
946 // condition! and Err/None carry no location suffix.)
947 let msg = panic_message_with_location(payload.as_ref());
948 crate::panic_telemetry::fire(&msg, crate::panic_telemetry::PanicSource::UnwindProtect);
949 // SAFETY: on the R main thread inside R_UnwindProtect.
950 unsafe {
951 crate::error_value::make_rust_condition_value(
952 &msg,
953 crate::error_value::kind::PANIC,
954 None,
955 call,
956 )
957 }
958 }
959 }
960}