Skip to main content

extendr_api/
thread_safety.rs

1//! Provide limited protection for multithreaded access to the R API.
2use crate::*;
3use extendr_ffi::{
4    R_MakeUnwindCont, R_UnwindProtect, Rboolean, Rf_error, Rf_protect, Rf_unprotect,
5};
6use std::cell::Cell;
7use std::sync::Mutex;
8
9/// A global lock, that should represent the global lock on the R-API.
10/// It is not tied to an actual instance of R.
11static R_API_LOCK: Mutex<()> = Mutex::new(());
12
13thread_local! {
14    static THREAD_HAS_LOCK: Cell<bool> = const { Cell::new(false) };
15}
16
17/// Run `f` while ensuring that `f` runs in a single-threaded manner.
18///
19/// This is intended for single-threaded access of the R's C-API.
20/// It is possible to have nested calls of `single_threaded` without deadlocking.
21///
22/// Note: This will fail badly if the called function `f` panics or calls `Rf_error`.
23pub fn single_threaded<F, R>(f: F) -> R
24where
25    F: FnOnce() -> R,
26{
27    let has_lock = THREAD_HAS_LOCK.with(|x| x.get());
28
29    // acquire R-API lock
30    let _guard = if !has_lock {
31        Some(R_API_LOCK.lock().unwrap())
32    } else {
33        None
34    };
35
36    // this thread now has the lock
37    THREAD_HAS_LOCK.with(|x| x.set(true));
38
39    let result = f();
40
41    // release the R-API lock
42    if _guard.is_some() {
43        THREAD_HAS_LOCK.with(|x| x.set(false));
44    }
45
46    result
47}
48
49static mut R_ERROR_BUF: Option<std::ffi::CString> = None;
50
51/// Signals an error via [extendr_ffi::Rf_error]
52pub fn throw_r_error<S: AsRef<str>>(s: S) -> ! {
53    let s = s.as_ref();
54    unsafe {
55        R_ERROR_BUF = Some(crate::conditions::to_c_message(s));
56        let ptr = std::ptr::addr_of!(R_ERROR_BUF);
57        let msg = (*ptr).as_ref().map_or(c"".as_ptr(), |msg| msg.as_ptr());
58        Rf_error(c"%s".as_ptr(), msg);
59    };
60}
61
62/// An alias to [throw_r_error]
63pub fn stop<S: AsRef<str>>(s: S) -> ! {
64    throw_r_error(s)
65}
66
67/// Wrap an R function such as `Rf_findFunction` and convert errors and panics into results.
68/// ```rust,ignore
69/// use extendr_api::prelude::*;
70/// test! {
71///    let res = catch_r_error(|| unsafe {
72///        throw_r_error("bad things!");
73///        std::ptr::null_mut()
74///    });
75///    assert_eq!(res.is_ok(), false);
76/// }
77/// ```
78pub fn catch_r_error<F>(f: F) -> Result<SEXP>
79where
80    F: FnOnce() -> SEXP + Copy,
81    F: std::panic::UnwindSafe,
82{
83    use std::os::raw;
84
85    unsafe extern "C" fn do_call<F>(data: *mut raw::c_void) -> SEXP
86    where
87        F: FnOnce() -> SEXP + Copy,
88    {
89        let data = data as *const ();
90        let f: &F = &*(data as *const F);
91        f()
92    }
93
94    unsafe extern "C" fn do_cleanup(_: *mut raw::c_void, jump: Rboolean) {
95        if jump != Rboolean::FALSE {
96            panic!("R has thrown an error.");
97        }
98    }
99
100    single_threaded(|| unsafe {
101        let fun_ptr = do_call::<F> as *const ();
102        let clean_ptr = do_cleanup as *const ();
103        let x = false;
104        let fun = std::mem::transmute::<
105            *const (),
106            Option<unsafe extern "C" fn(*mut std::ffi::c_void) -> *mut extendr_ffi::SEXPREC>,
107        >(fun_ptr);
108        let cleanfun = std::mem::transmute::<
109            *const (),
110            std::option::Option<unsafe extern "C" fn(*mut std::ffi::c_void, extendr_ffi::Rboolean)>,
111        >(clean_ptr);
112        let data = &f as *const _ as _;
113        let cleandata = &x as *const _ as _;
114        let cont = R_MakeUnwindCont();
115        Rf_protect(cont);
116
117        // Note that catch_unwind does not work for 32 bit windows targets.
118        let res = match std::panic::catch_unwind(|| {
119            R_UnwindProtect(fun, data, cleanfun, cleandata, cont)
120        }) {
121            Ok(res) => Ok(res),
122            Err(_) => Err("Error in protected R code".into()),
123        };
124        Rf_unprotect(1);
125        res
126    })
127}
128
129/// This function registers a configurable print panic hook, for use in extendr-based R-packages.
130/// If the environment variable `EXTENDR_BACKTRACE` is set to either `true` or `1`,
131/// then it displays the entire Rust panic traceback (default hook), otherwise it omits the panic backtrace.
132#[no_mangle]
133pub extern "C" fn register_extendr_panic_hook() {
134    static RUN_ONCE: std::sync::Once = std::sync::Once::new();
135    RUN_ONCE.call_once_force(|x| {
136        // just ignore repeated calls to this function
137        if x.is_poisoned() {
138            println!("warning: extendr panic hook info registration was done more than once");
139            return;
140        }
141        let default_hook = std::panic::take_hook();
142        std::panic::set_hook(Box::new(move |x| {
143            let show_traceback = std::env::var("EXTENDR_BACKTRACE")
144                .map(|v| v.eq_ignore_ascii_case("true") || v == "1")
145                .unwrap_or(false);
146            if show_traceback {
147                default_hook(x)
148            }
149        }));
150    });
151}