Skip to main content

r/interpreter/native/
dll.rs

1//! Dynamic library loading — dyn.load(), dyn.unload(), symbol lookup.
2//!
3//! Uses `libloading` to load shared libraries (.so on Linux, .dylib on macOS)
4//! and resolve function symbols for `.Call()` dispatch.
5//!
6//! The actual native function call goes through a C trampoline
7//! (`_minir_call_protected` in `csrc/native_trampoline.c`) which sets up
8//! `setjmp` so that `Rf_error()` in C code safely longjmps back instead of
9//! crashing. The trampoline handles variable argument counts (up to 16 SEXP args).
10
11use std::collections::HashMap;
12use std::ffi::CStr;
13use std::os::raw::c_char;
14use std::path::{Path, PathBuf};
15
16use libloading::{Library, Symbol};
17
18use super::convert;
19use super::sexp::{self, Sexp};
20use crate::interpreter::value::*;
21use crate::interpreter::Interpreter;
22
23// Thread-local interpreter reference for callbacks from C code.
24// Set before each .Call, cleared after.
25thread_local! {
26    static CURRENT_INTERP: std::cell::Cell<*const Interpreter> = const { std::cell::Cell::new(std::ptr::null()) };
27}
28
29fn callback_find_var(name: &str) -> Option<RValue> {
30    CURRENT_INTERP.with(|cell| {
31        let interp = cell.get();
32        if interp.is_null() {
33            return None;
34        }
35        let interp = unsafe { &*interp };
36        interp.global_env.get(name)
37    })
38}
39
40fn callback_define_var(name: &str, val: RValue) {
41    CURRENT_INTERP.with(|cell| {
42        let interp = cell.get();
43        if interp.is_null() {
44            return;
45        }
46        let interp = unsafe { &*interp };
47        interp.global_env.set(name.to_string(), val);
48    });
49}
50
51fn callback_eval_expr(expr: &RValue) -> Result<RValue, crate::interpreter::value::RError> {
52    use crate::interpreter::value::RError;
53    use crate::interpreter::CallFrame;
54    CURRENT_INTERP.with(|cell| {
55        let interp = cell.get();
56        if interp.is_null() {
57            return Err(RError::other(
58                "no interpreter available for Rf_eval".to_string(),
59            ));
60        }
61        let interp = unsafe { &*interp };
62
63        // Push a native boundary marker so tracebacks show the C→R transition
64        let boundary = CallFrame {
65            call: None,
66            function: RValue::Null,
67            env: interp.global_env.clone(),
68            formal_args: Default::default(),
69            supplied_args: Default::default(),
70            supplied_positional: Default::default(),
71            supplied_named: Default::default(),
72            supplied_arg_count: 0,
73            is_native_boundary: true,
74        };
75        interp.call_stack.borrow_mut().push(boundary);
76
77        let result = if let RValue::Language(ref lang) = expr {
78            interp
79                .eval_in(lang, &interp.global_env)
80                .map_err(RError::from)
81        } else if let Some(name) = expr.as_vector().and_then(|v| v.as_character_scalar()) {
82            interp
83                .global_env
84                .get(&name)
85                .ok_or_else(|| RError::other(format!("object '{name}' not found")))
86        } else {
87            Ok(expr.clone())
88        };
89
90        interp.call_stack.borrow_mut().pop();
91        result
92    })
93}
94
95fn callback_parse_text(text: &str) -> Result<RValue, crate::interpreter::value::RError> {
96    use crate::interpreter::value::{Language, RError};
97    // Parse the R source text into an AST
98    let ast = crate::parser::parse_program(text)
99        .map_err(|e| RError::other(format!("parse error: {e}")))?;
100    Ok(RValue::Language(Language::new(ast)))
101}
102
103// region: C trampoline types
104
105/// Signature of `R_init_<pkgname>(DllInfo*)` package init function.
106type PkgInitFn = unsafe extern "C" fn(*mut u8);
107
108// endregion
109
110// region: CBuffer — C-compatible buffers for .C() calling convention
111
112/// A C-compatible buffer for passing data to .C() functions.
113///
114/// .C() passes raw pointers to C functions:
115/// - Double → `*mut f64`
116/// - Integer → `*mut i32` (converted from miniR's i64)
117/// - Logical → `*mut i32` (TRUE=1, FALSE=0, NA=NA_INTEGER)
118/// - Character → `*mut *mut c_char` (array of null-terminated C strings)
119/// - Raw → `*mut u8`
120///
121/// After the call, the C function may have modified the buffers in place.
122/// `to_rvalue()` reads back the (possibly modified) data.
123enum CBuffer {
124    Double {
125        data: Vec<f64>,
126    },
127    Integer {
128        data: Vec<i32>,
129    },
130    Logical {
131        data: Vec<i32>,
132    },
133    Character {
134        /// Pointers to null-terminated C strings (owned by `_owned_strings`).
135        ptrs: Vec<*mut c_char>,
136        /// Backing storage for the C strings — kept alive for the call duration.
137        /// Not read directly; exists to prevent deallocation while `ptrs` are live.
138        _owned_strings: Vec<std::ffi::CString>,
139    },
140    Raw {
141        data: Vec<u8>,
142    },
143}
144
145impl CBuffer {
146    /// Convert an RValue to a C-compatible buffer.
147    fn from_rvalue(val: &RValue) -> Result<Self, String> {
148        match val {
149            RValue::Vector(rv) => match &rv.inner {
150                Vector::Double(d) => {
151                    let data: Vec<f64> = d.iter_opt().map(|v| v.unwrap_or(sexp::NA_REAL)).collect();
152                    Ok(CBuffer::Double { data })
153                }
154                Vector::Integer(int) => {
155                    let data: Vec<i32> = int
156                        .iter_opt()
157                        .map(|v| match v {
158                            Some(i) => i32::try_from(i).unwrap_or(sexp::NA_INTEGER),
159                            None => sexp::NA_INTEGER,
160                        })
161                        .collect();
162                    Ok(CBuffer::Integer { data })
163                }
164                Vector::Logical(l) => {
165                    let data: Vec<i32> = (0..l.len())
166                        .map(|i| match l[i] {
167                            Some(true) => 1i32,
168                            Some(false) => 0i32,
169                            None => sexp::NA_LOGICAL,
170                        })
171                        .collect();
172                    Ok(CBuffer::Logical { data })
173                }
174                Vector::Character(c) => {
175                    let mut owned_strings = Vec::with_capacity(c.len());
176                    let mut ptrs = Vec::with_capacity(c.len());
177                    for i in 0..c.len() {
178                        let cstr = match &c[i] {
179                            Some(s) => std::ffi::CString::new(s.as_str()).unwrap_or_else(|_| {
180                                std::ffi::CString::new("").expect("empty CString")
181                            }),
182                            None => std::ffi::CString::new("NA").expect("NA CString"),
183                        };
184                        owned_strings.push(cstr);
185                    }
186                    // Build pointer array after all CStrings are in the Vec
187                    // (so they don't move).
188                    for cstr in &owned_strings {
189                        ptrs.push(cstr.as_ptr() as *mut c_char);
190                    }
191                    Ok(CBuffer::Character {
192                        ptrs,
193                        _owned_strings: owned_strings,
194                    })
195                }
196                Vector::Raw(r) => Ok(CBuffer::Raw { data: r.clone() }),
197                Vector::Complex(_) => Err(
198                    "complex vectors are not supported by .C() — use .Call() instead".to_string(),
199                ),
200            },
201            RValue::Null => {
202                // NULL is valid in .C — pass as empty double buffer
203                Ok(CBuffer::Double { data: Vec::new() })
204            }
205            _ => Err(format!(
206                "unsupported argument type for .C(): {}",
207                val.type_name()
208            )),
209        }
210    }
211
212    /// Get a void pointer to the buffer data.
213    fn as_void_ptr(&mut self) -> *mut u8 {
214        match self {
215            CBuffer::Double { data } => data.as_mut_ptr() as *mut u8,
216            CBuffer::Integer { data } => data.as_mut_ptr() as *mut u8,
217            CBuffer::Logical { data } => data.as_mut_ptr() as *mut u8,
218            CBuffer::Character { ptrs, .. } => ptrs.as_mut_ptr() as *mut u8,
219            CBuffer::Raw { data } => data.as_mut_ptr(),
220        }
221    }
222
223    /// Convert the (possibly modified) buffer back to an RValue.
224    fn to_rvalue(&self) -> RValue {
225        match self {
226            CBuffer::Double { data } => {
227                let vals: Vec<Option<f64>> = data
228                    .iter()
229                    .map(|&v| if sexp::is_na_real(v) { None } else { Some(v) })
230                    .collect();
231                RValue::vec(Vector::Double(vals.into()))
232            }
233            CBuffer::Integer { data } => {
234                let vals: Vec<Option<i64>> = data
235                    .iter()
236                    .map(|&v| {
237                        if v == sexp::NA_INTEGER {
238                            None
239                        } else {
240                            Some(i64::from(v))
241                        }
242                    })
243                    .collect();
244                RValue::vec(Vector::Integer(vals.into()))
245            }
246            CBuffer::Logical { data } => {
247                let vals: Vec<Option<bool>> = data
248                    .iter()
249                    .map(|&v| {
250                        if v == sexp::NA_LOGICAL {
251                            None
252                        } else {
253                            Some(v != 0)
254                        }
255                    })
256                    .collect();
257                RValue::vec(Vector::Logical(vals.into()))
258            }
259            CBuffer::Character { ptrs, .. } => {
260                let vals: Vec<Option<String>> = ptrs
261                    .iter()
262                    .map(|&p| {
263                        if p.is_null() {
264                            None
265                        } else {
266                            // Safety: the C function may have modified the string
267                            // but we still own the buffer. Read it back.
268                            let cstr = unsafe { CStr::from_ptr(p) };
269                            Some(cstr.to_str().unwrap_or("").to_string())
270                        }
271                    })
272                    .collect();
273                RValue::vec(Vector::Character(vals.into()))
274            }
275            CBuffer::Raw { data } => RValue::vec(Vector::Raw(data.clone())),
276        }
277    }
278}
279
280// endregion
281
282// region: LoadedDll
283
284/// A loaded dynamic library and its resolved symbols.
285pub struct LoadedDll {
286    /// Path to the .so/.dylib file.
287    pub path: PathBuf,
288    /// Short name (e.g. "myPkg" from "myPkg.so").
289    pub name: String,
290    /// The underlying library handle.
291    lib: Library,
292    /// Cached symbol addresses: function name → raw pointer.
293    symbols: HashMap<String, *const ()>,
294    /// .Call methods registered via R_registerRoutines during R_init_<pkg>.
295    pub registered_calls: HashMap<String, *const ()>,
296    /// .C methods registered via R_registerRoutines during R_init_<pkg>.
297    pub registered_c_methods: HashMap<String, *const ()>,
298}
299
300// Safety: LoadedDll is only used from a single interpreter thread.
301// The Library handle and symbol pointers are stable once loaded.
302unsafe impl Send for LoadedDll {}
303
304impl LoadedDll {
305    /// Load a shared library from the given path, then call R_init_<pkgname>
306    /// if it exists (to register routines).
307    pub fn load(path: &Path) -> Result<Self, String> {
308        let name = path
309            .file_stem()
310            .and_then(|s| s.to_str())
311            .unwrap_or("unknown")
312            .to_string();
313
314        // Safety: loading a shared library can execute arbitrary code (init functions).
315        // We trust that package .so files are safe — they were compiled from the
316        // package's own C source code.
317        let lib = unsafe { Library::new(path) }
318            .map_err(|e| format!("dyn.load(\"{}\") failed: {e}", path.display()))?;
319
320        let mut dll = LoadedDll {
321            path: path.to_path_buf(),
322            name: name.clone(),
323            lib,
324            symbols: HashMap::new(),
325            registered_calls: HashMap::new(),
326            registered_c_methods: HashMap::new(),
327        };
328
329        // Call R_init_<pkgname> if it exists — this triggers R_registerRoutines
330        dll.call_pkg_init(&name);
331
332        Ok(dll)
333    }
334
335    /// Call the package init function `R_init_<name>(DllInfo*)` if present.
336    fn call_pkg_init(&mut self, pkg_name: &str) {
337        let init_name = format!("R_init_{pkg_name}");
338        if let Ok(ptr) = self.get_symbol(&init_name) {
339            unsafe {
340                let init: PkgInitFn = std::mem::transmute(ptr);
341                // Pass a null DllInfo* — our runtime ignores it
342                init(std::ptr::null_mut());
343            }
344            // After init, collect registered .Call methods
345            self.collect_registered_calls();
346        }
347    }
348
349    /// Read registered .Call and .C methods from the Rust runtime's registry.
350    fn collect_registered_calls(&mut self) {
351        // R_registerRoutines (called by R_init_<pkg>) stores registrations
352        // in the Rust runtime's shared registry.
353        for (name, ptr) in super::runtime::REGISTERED_CALLS
354            .lock()
355            .expect("lock registered calls")
356            .iter()
357        {
358            self.registered_calls.insert(name.clone(), ptr.0);
359        }
360        for (name, ptr) in super::runtime::REGISTERED_C_METHODS
361            .lock()
362            .expect("lock registered C methods")
363            .iter()
364        {
365            self.registered_c_methods.insert(name.clone(), ptr.0);
366        }
367    }
368
369    /// Look up a .C method symbol by name. Checks registered .C methods first,
370    /// then falls back to dynamic symbol lookup.
371    pub fn get_c_symbol(&mut self, name: &str) -> Result<*const (), String> {
372        if let Some(&ptr) = self.registered_c_methods.get(name) {
373            return Ok(ptr);
374        }
375        // Fall back to dlsym — many packages don't register .C methods
376        self.get_symbol(name)
377    }
378
379    /// Look up a function symbol by name. Checks registered .Call routines first,
380    /// then falls back to dynamic symbol lookup. Caches the result.
381    pub fn get_symbol(&mut self, name: &str) -> Result<*const (), String> {
382        // Check registered .Call methods first
383        if let Some(&ptr) = self.registered_calls.get(name) {
384            return Ok(ptr);
385        }
386
387        // Check cache
388        if let Some(&ptr) = self.symbols.get(name) {
389            return Ok(ptr);
390        }
391
392        let c_name =
393            std::ffi::CString::new(name).map_err(|_| format!("invalid symbol name: {name}"))?;
394
395        // Safety: we trust the symbol exists and is a valid function pointer
396        let sym: Symbol<*const ()> = unsafe {
397            self.lib
398                .get(c_name.as_bytes_with_nul())
399                .map_err(|e| format!("symbol '{name}' not found in {}: {e}", self.path.display()))?
400        };
401
402        let ptr = *sym;
403        self.symbols.insert(name.to_string(), ptr);
404        Ok(ptr)
405    }
406}
407
408impl std::fmt::Debug for LoadedDll {
409    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
410        f.debug_struct("LoadedDll")
411            .field("name", &self.name)
412            .field("path", &self.path)
413            .field("symbols", &self.symbols.keys().collect::<Vec<_>>())
414            .field(
415                "registered_calls",
416                &self.registered_calls.keys().collect::<Vec<_>>(),
417            )
418            .field(
419                "registered_c_methods",
420                &self.registered_c_methods.keys().collect::<Vec<_>>(),
421            )
422            .finish()
423    }
424}
425
426// endregion
427
428// region: Interpreter DLL state
429
430impl Interpreter {
431    /// Load a shared library and register it. Returns the DLL name.
432    pub(crate) fn dyn_load(&self, path: &Path) -> Result<String, RError> {
433        // Set interpreter callbacks before loading — R_init_<pkg> may call
434        // Rf_eval, R_ParseVector, etc. during initialization.
435        CURRENT_INTERP.with(|cell| cell.set(self as *const Interpreter));
436        super::runtime::set_callbacks(super::runtime::InterpreterCallbacks {
437            find_var: Some(callback_find_var),
438            define_var: Some(callback_define_var),
439            eval_expr: Some(callback_eval_expr),
440            parse_text: Some(callback_parse_text),
441        });
442
443        let dll = LoadedDll::load(path).map_err(|e| RError::new(RErrorKind::Other, e))?;
444        let name = dll.name.clone();
445        self.loaded_dlls.borrow_mut().push(dll);
446
447        // DON'T clear callbacks — .onLoad may call .Call which needs them.
448        // Callbacks are cleared after .Call returns in dot_call().
449
450        Ok(name)
451    }
452
453    /// Unload a shared library by name.
454    pub(crate) fn dyn_unload(&self, name: &str) -> Result<(), RError> {
455        let mut dlls = self.loaded_dlls.borrow_mut();
456        let pos = dlls.iter().position(|d| d.name == name);
457        match pos {
458            Some(i) => {
459                dlls.remove(i);
460                Ok(())
461            }
462            None => Err(RError::new(
463                RErrorKind::Other,
464                format!("shared object '{name}' was not loaded"),
465            )),
466        }
467    }
468
469    /// Check if a symbol is loaded in any DLL.
470    pub(crate) fn is_symbol_loaded(&self, name: &str) -> bool {
471        let mut dlls = self.loaded_dlls.borrow_mut();
472        dlls.iter_mut().any(|dll| dll.get_symbol(name).is_ok())
473    }
474
475    /// Look up a symbol across all loaded DLLs. Returns the function pointer
476    /// and the index of the DLL that contains it.
477    fn find_native_symbol_with_dll(&self, name: &str) -> Result<(*const (), usize), RError> {
478        let mut dlls = self.loaded_dlls.borrow_mut();
479        for (i, dll) in dlls.iter_mut().enumerate().rev() {
480            if let Ok(ptr) = dll.get_symbol(name) {
481                return Ok((ptr, i));
482            }
483        }
484        Err(RError::new(
485            RErrorKind::Other,
486            format!("symbol '{name}' not found in any loaded DLL"),
487        ))
488    }
489
490    /// Look up a symbol across all loaded DLLs. Returns the function pointer.
491    pub(crate) fn find_native_symbol(&self, name: &str) -> Result<*const (), RError> {
492        self.find_native_symbol_with_dll(name).map(|(ptr, _)| ptr)
493    }
494
495    /// Look up a .C symbol across all loaded DLLs — checks registered .C methods first.
496    fn find_c_symbol(&self, name: &str) -> Result<*const (), RError> {
497        let mut dlls = self.loaded_dlls.borrow_mut();
498        for dll in dlls.iter_mut().rev() {
499            if let Ok(ptr) = dll.get_c_symbol(name) {
500                return Ok(ptr);
501            }
502        }
503        Err(RError::new(
504            RErrorKind::Other,
505            format!("symbol '{name}' not found in any loaded DLL"),
506        ))
507    }
508
509    /// Execute a `.Call()` invocation using the C trampoline for error safety.
510    ///
511    /// Flow:
512    /// 1. Convert RValue args → SEXP (using C allocator)
513    /// 2. Look up the native function symbol
514    /// 3. Look up the C trampoline `_minir_call_protected` in the same DLL
515    /// 4. Call the trampoline (which does setjmp + dispatch)
516    /// 5. If the C function called Rf_error(), the trampoline returns 1 and
517    ///    we convert the error message to an RError
518    /// 6. Convert the result SEXP → RValue
519    /// 7. Free all C-side allocations via `_minir_free_allocs`
520    /// 8. Free Rust-side input SEXPs
521    pub(crate) fn dot_call(&self, symbol_name: &str, args: &[RValue]) -> Result<RValue, RError> {
522        let fn_ptr = self.find_native_symbol(symbol_name)?;
523
524        // Convert RValue args to SEXPs (allocated with C allocator)
525        let sexp_args: Vec<Sexp> = args.iter().map(convert::rvalue_to_sexp).collect();
526
527        // Set interpreter callbacks so C code can call back for Rf_findVar, etc.
528        CURRENT_INTERP.with(|cell| cell.set(self as *const Interpreter));
529        super::runtime::set_callbacks(super::runtime::InterpreterCallbacks {
530            find_var: Some(callback_find_var),
531            define_var: Some(callback_define_var),
532            eval_expr: Some(callback_eval_expr),
533            parse_text: Some(callback_parse_text),
534        });
535
536        // The trampoline and error accessors are now in the binary
537        // (compiled from csrc/native_trampoline.c via build.rs).
538        extern "C" {
539            fn _minir_call_protected(
540                fn_ptr: *const (),
541                args: *const Sexp,
542                nargs: i32,
543                result: *mut Sexp,
544            ) -> i32;
545            fn _minir_get_error_msg() -> *const c_char;
546            fn _minir_bt_count() -> i32;
547            fn _minir_bt_frames() -> *const *const std::ffi::c_void;
548        }
549
550        let mut result_sexp: Sexp = sexp::R_NIL_VALUE;
551        let nargs = i32::try_from(sexp_args.len()).unwrap_or(0);
552        let error_code = unsafe {
553            _minir_call_protected(
554                fn_ptr,
555                sexp_args.as_ptr(),
556                nargs,
557                &mut result_sexp as *mut Sexp,
558            )
559        };
560
561        // Check if Rf_error was called
562        if error_code != 0 {
563            let error_msg = unsafe {
564                let msg_ptr = _minir_get_error_msg();
565                if msg_ptr.is_null() {
566                    "unknown error in native code".to_string()
567                } else {
568                    CStr::from_ptr(msg_ptr)
569                        .to_str()
570                        .unwrap_or("unknown error")
571                        .to_string()
572                }
573            };
574
575            // Capture native backtrace from the C trampoline
576            let bt_count = unsafe { _minir_bt_count() } as usize;
577            if bt_count > 0 {
578                let bt_raw = unsafe { std::slice::from_raw_parts(_minir_bt_frames(), bt_count) };
579                let native_bt = crate::interpreter::NativeBacktrace {
580                    frames: bt_raw.iter().map(|p| *p as usize).collect(),
581                };
582                *self.pending_native_backtrace.borrow_mut() = Some(native_bt);
583            }
584
585            // Clean up before returning error
586            super::runtime::clear_callbacks();
587            CURRENT_INTERP.with(|cell| cell.set(std::ptr::null()));
588            super::runtime::free_allocs();
589            unsafe {
590                for s in sexp_args {
591                    sexp::free_sexp(s);
592                }
593            }
594
595            return Err(RError::new(RErrorKind::Other, error_msg));
596        }
597
598        // Convert result to RValue (copies all data)
599        let result = unsafe { convert::sexp_to_rvalue(result_sexp) };
600
601        // Clear interpreter callbacks
602        super::runtime::clear_callbacks();
603        CURRENT_INTERP.with(|cell| cell.set(std::ptr::null()));
604
605        // Free runtime allocations (result SEXP + any intermediates)
606        super::runtime::free_allocs();
607
608        // Free Rust-allocated input SEXPs. Skip external pointers and environments.
609        unsafe {
610            for (s, arg) in sexp_args.into_iter().zip(args.iter()) {
611                match arg {
612                    RValue::List(list)
613                        if list
614                            .attrs
615                            .as_ref()
616                            .is_some_and(|a| a.contains_key(".sexp_ptr")) =>
617                    {
618                        continue; // external pointer — owned by C
619                    }
620                    RValue::Environment(_) => {
621                        // Don't free the Environment box — it's shared via Rc
622                        // Just free the SexpRec shell
623                        (*s).data = std::ptr::null_mut();
624                        sexp::free_sexp(s);
625                    }
626                    _ => sexp::free_sexp(s),
627                }
628            }
629        }
630
631        Ok(result)
632    }
633
634    /// Execute a `.External()` invocation.
635    ///
636    /// `.External()` passes a single SEXP pairlist to the C function.
637    /// The pairlist's first CAR is the function symbol; CDR is the argument chain.
638    pub(crate) fn dot_external(
639        &self,
640        symbol_name: &str,
641        args: &[RValue],
642    ) -> Result<RValue, RError> {
643        let fn_ptr = self.find_native_symbol(symbol_name)?;
644
645        // Build a pairlist: first node is a symbol for the function name,
646        // then each argument is a node.
647        let func_sym = super::runtime::Rf_install(
648            std::ffi::CString::new(symbol_name)
649                .unwrap_or_default()
650                .as_ptr(),
651        );
652        let pairlist = super::runtime::Rf_cons(func_sym, unsafe { super::runtime::R_NilValue });
653
654        // Append arguments in reverse order
655        let mut tail = pairlist;
656        for arg in args {
657            let sexp_arg = convert::rvalue_to_sexp(arg);
658            let node = super::runtime::Rf_cons(sexp_arg, unsafe { super::runtime::R_NilValue });
659            // Set CDR of tail to node
660            unsafe {
661                let pd = (*tail).data as *mut sexp::PairlistData;
662                if !pd.is_null() {
663                    (*pd).cdr = node;
664                }
665            }
666            tail = node;
667        }
668
669        // Set interpreter callbacks
670        CURRENT_INTERP.with(|cell| cell.set(self as *const Interpreter));
671        super::runtime::set_callbacks(super::runtime::InterpreterCallbacks {
672            find_var: Some(callback_find_var),
673            define_var: Some(callback_define_var),
674            eval_expr: Some(callback_eval_expr),
675            parse_text: Some(callback_parse_text),
676        });
677
678        extern "C" {
679            fn _minir_call_protected(
680                fn_ptr: *const (),
681                args: *const Sexp,
682                nargs: i32,
683                result: *mut Sexp,
684            ) -> i32;
685            fn _minir_get_error_msg() -> *const c_char;
686            fn _minir_bt_count() -> i32;
687            fn _minir_bt_frames() -> *const *const std::ffi::c_void;
688        }
689
690        let mut result_sexp: Sexp = sexp::R_NIL_VALUE;
691        let sexp_args = [pairlist];
692        let error_code =
693            unsafe { _minir_call_protected(fn_ptr, sexp_args.as_ptr(), 1, &mut result_sexp) };
694
695        if error_code != 0 {
696            let error_msg = unsafe {
697                let msg_ptr = _minir_get_error_msg();
698                if msg_ptr.is_null() {
699                    "unknown error in native code".to_string()
700                } else {
701                    CStr::from_ptr(msg_ptr)
702                        .to_str()
703                        .unwrap_or("unknown error")
704                        .to_string()
705                }
706            };
707
708            // Capture native backtrace from the C trampoline
709            let bt_count = unsafe { _minir_bt_count() } as usize;
710            if bt_count > 0 {
711                let bt_raw = unsafe { std::slice::from_raw_parts(_minir_bt_frames(), bt_count) };
712                let native_bt = crate::interpreter::NativeBacktrace {
713                    frames: bt_raw.iter().map(|p| *p as usize).collect(),
714                };
715                *self.pending_native_backtrace.borrow_mut() = Some(native_bt);
716            }
717
718            super::runtime::clear_callbacks();
719            CURRENT_INTERP.with(|cell| cell.set(std::ptr::null()));
720            super::runtime::free_allocs();
721            return Err(RError::new(RErrorKind::Other, error_msg));
722        }
723
724        let result = unsafe { convert::sexp_to_rvalue(result_sexp) };
725        super::runtime::clear_callbacks();
726        CURRENT_INTERP.with(|cell| cell.set(std::ptr::null()));
727        super::runtime::free_allocs();
728
729        Ok(result)
730    }
731
732    /// Execute a `.C()` invocation using the C trampoline for error safety.
733    ///
734    /// `.C()` is simpler than `.Call()` — it passes pointers to raw data
735    /// buffers directly to C functions. The C function receives `double*`,
736    /// `int*`, `char**`, etc. and modifies them in place. After the call,
737    /// the modified buffers are read back into R values.
738    ///
739    /// Flow:
740    /// 1. Convert each RValue arg to a C-compatible buffer
741    /// 2. Look up the native function symbol
742    /// 3. Call the trampoline with void* pointers
743    /// 4. Read back modified buffers into R values
744    /// 5. Return a named list of the (possibly modified) arguments
745    pub(crate) fn dot_c(
746        &self,
747        symbol_name: &str,
748        args: &[RValue],
749        arg_names: &[Option<String>],
750    ) -> Result<RValue, RError> {
751        let fn_ptr = self.find_c_symbol(symbol_name)?;
752
753        // Convert each arg to a C-compatible buffer and collect void* pointers.
754        // Each CBuffer owns the memory; we read it back after the call.
755        let mut buffers: Vec<CBuffer> = Vec::with_capacity(args.len());
756        for (i, arg) in args.iter().enumerate() {
757            buffers.push(CBuffer::from_rvalue(arg).map_err(|e| {
758                RError::new(RErrorKind::Argument, format!(".C: argument {}: {e}", i + 1))
759            })?);
760        }
761
762        let mut ptrs: Vec<*mut u8> = buffers.iter_mut().map(|b| b.as_void_ptr()).collect();
763
764        // Set interpreter callbacks so C code can call back for Rf_findVar, etc.
765        CURRENT_INTERP.with(|cell| cell.set(self as *const Interpreter));
766        super::runtime::set_callbacks(super::runtime::InterpreterCallbacks {
767            find_var: Some(callback_find_var),
768            define_var: Some(callback_define_var),
769            eval_expr: Some(callback_eval_expr),
770            parse_text: Some(callback_parse_text),
771        });
772
773        extern "C" {
774            fn _minir_dotC_call_protected(fn_ptr: *const (), args: *mut *mut u8, nargs: i32)
775                -> i32;
776            fn _minir_get_error_msg() -> *const c_char;
777            fn _minir_bt_count() -> i32;
778            fn _minir_bt_frames() -> *const *const std::ffi::c_void;
779        }
780
781        let nargs = i32::try_from(ptrs.len()).unwrap_or(0);
782        let error_code = unsafe { _minir_dotC_call_protected(fn_ptr, ptrs.as_mut_ptr(), nargs) };
783
784        if error_code != 0 {
785            let error_msg = unsafe {
786                let msg_ptr = _minir_get_error_msg();
787                if msg_ptr.is_null() {
788                    "unknown error in native code".to_string()
789                } else {
790                    CStr::from_ptr(msg_ptr)
791                        .to_str()
792                        .unwrap_or("unknown error")
793                        .to_string()
794                }
795            };
796
797            // Capture native backtrace from the C trampoline
798            let bt_count = unsafe { _minir_bt_count() } as usize;
799            if bt_count > 0 {
800                let bt_raw = unsafe { std::slice::from_raw_parts(_minir_bt_frames(), bt_count) };
801                let native_bt = crate::interpreter::NativeBacktrace {
802                    frames: bt_raw.iter().map(|p| *p as usize).collect(),
803                };
804                *self.pending_native_backtrace.borrow_mut() = Some(native_bt);
805            }
806
807            super::runtime::clear_callbacks();
808            CURRENT_INTERP.with(|cell| cell.set(std::ptr::null()));
809            super::runtime::free_allocs();
810
811            return Err(RError::new(RErrorKind::Other, error_msg));
812        }
813
814        // Read back modified buffers into R values
815        let mut result_values: Vec<(Option<String>, RValue)> = Vec::with_capacity(buffers.len());
816        for (i, buf) in buffers.iter().enumerate() {
817            let name = arg_names.get(i).and_then(|n| n.clone());
818            result_values.push((name, buf.to_rvalue()));
819        }
820
821        // Clear interpreter callbacks
822        super::runtime::clear_callbacks();
823        CURRENT_INTERP.with(|cell| cell.set(std::ptr::null()));
824        super::runtime::free_allocs();
825
826        Ok(RValue::List(RList::new(result_values)))
827    }
828
829    /// Find miniR's `include/` directory containing Rinternals.h.
830    ///
831    /// Search order:
832    /// 1. `MINIR_INCLUDE` environment variable
833    /// 2. `<exe_dir>/../include` (installed layout)
834    /// 3. `<working_dir>/include` (development layout)
835    pub(crate) fn find_include_dir(&self) -> Option<std::path::PathBuf> {
836        // Check env var first
837        if let Some(dir) = self.get_env_var("MINIR_INCLUDE") {
838            let p = std::path::PathBuf::from(dir);
839            if p.join("miniR").join("Rinternals.h").is_file() {
840                return Some(p);
841            }
842        }
843
844        // Check relative to executable
845        if let Ok(exe) = std::env::current_exe() {
846            if let Some(exe_dir) = exe.parent() {
847                let p = exe_dir.join("../include");
848                if p.join("miniR").join("Rinternals.h").is_file() {
849                    return Some(p);
850                }
851            }
852        }
853
854        // Check working directory (development layout)
855        let wd = self.get_working_dir();
856        let p = wd.join("include");
857        if p.join("miniR").join("Rinternals.h").is_file() {
858            return Some(p);
859        }
860
861        // Check CARGO_MANIFEST_DIR for test/dev builds
862        let manifest = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
863        let p = manifest.join("include");
864        if p.join("miniR").join("Rinternals.h").is_file() {
865            return Some(p);
866        }
867
868        None
869    }
870
871    /// Load native code for a package based on its useDynLib directives.
872    ///
873    /// For each useDynLib directive in the NAMESPACE:
874    /// 1. Look for a pre-compiled .so/.dylib in `<pkg_dir>/libs/`
875    /// 2. If not found, compile `<pkg_dir>/src/*.c` on demand
876    /// 3. Load the shared library via dyn_load
877    pub(crate) fn load_package_native_code(
878        &self,
879        pkg_name: &str,
880        pkg_dir: &std::path::Path,
881        dyn_libs: &[crate::interpreter::packages::namespace::DynLibDirective],
882    ) -> Result<(), RError> {
883        if dyn_libs.is_empty() {
884            return Ok(());
885        }
886
887        let ext = if cfg!(target_os = "macos") {
888            "dylib"
889        } else {
890            "so"
891        };
892
893        for directive in dyn_libs {
894            let lib_name = &directive.library;
895
896            // 1. Check for pre-compiled library in libs/
897            let precompiled = pkg_dir.join("libs").join(format!("{lib_name}.{ext}"));
898            if precompiled.is_file() {
899                self.dyn_load(&precompiled)?;
900                continue;
901            }
902
903            // 2. Compile from src/ on demand
904            let src_dir = pkg_dir.join("src");
905            if !src_dir.is_dir() {
906                tracing::warn!(
907                    "useDynLib({lib_name}): no precompiled library and no src/ directory in {}",
908                    pkg_dir.display()
909                );
910                continue;
911            }
912
913            let include_dir = self.find_include_dir().ok_or_else(|| {
914                RError::other(format!(
915                    "cannot compile native code for '{pkg_name}': \
916                     miniR include directory not found (set MINIR_INCLUDE env var)"
917                ))
918            })?;
919
920            // Resolve LinkingTo include paths from DESCRIPTION
921            let linking_to_includes = self.resolve_linking_to_includes(pkg_dir);
922
923            // Compile into a temporary output directory under the package
924            let output_dir = pkg_dir.join("libs");
925            let compile = |out_dir: &std::path::Path| {
926                super::compile::compile_package_with_deps(
927                    &src_dir,
928                    lib_name,
929                    out_dir,
930                    &include_dir,
931                    &linking_to_includes,
932                )
933                .map_err(|e| {
934                    RError::other(format!(
935                        "compilation of native code for '{pkg_name}' failed: {e}"
936                    ))
937                })
938            };
939
940            if std::fs::create_dir_all(&output_dir).is_err() {
941                // If we can't write to pkg_dir/libs, use temp dir
942                let output_dir = self.temp_dir.path().join(format!("native-{pkg_name}"));
943                std::fs::create_dir_all(&output_dir)
944                    .map_err(|e| RError::other(format!("cannot create output directory: {e}")))?;
945                let lib_path = compile(&output_dir)?;
946                self.dyn_load(&lib_path)?;
947                continue;
948            }
949
950            let lib_path = compile(&output_dir)?;
951            self.dyn_load(&lib_path)?;
952        }
953
954        Ok(())
955    }
956
957    /// Resolve include paths for LinkingTo dependencies.
958    ///
959    /// Reads the package's DESCRIPTION, finds LinkingTo packages, and returns
960    /// their `inst/include` directories (or `include/` at package root).
961    fn resolve_linking_to_includes(&self, pkg_dir: &std::path::Path) -> Vec<std::path::PathBuf> {
962        let desc_path = pkg_dir.join("DESCRIPTION");
963        let desc_text = match std::fs::read_to_string(&desc_path) {
964            Ok(t) => t,
965            Err(_) => return Vec::new(),
966        };
967        let desc = match crate::interpreter::packages::description::PackageDescription::parse(
968            &desc_text,
969        ) {
970            Ok(d) => d,
971            Err(_) => return Vec::new(),
972        };
973
974        let mut includes = Vec::new();
975        for dep in &desc.linking_to {
976            // Find the dependency's package directory
977            if let Some(dep_dir) = self.find_package_dir(&dep.package) {
978                // R packages export headers from inst/include/ (installed) or include/ (source)
979                let inst_include = dep_dir.join("inst").join("include");
980                if inst_include.is_dir() {
981                    includes.push(inst_include);
982                } else {
983                    let include = dep_dir.join("include");
984                    if include.is_dir() {
985                        includes.push(include);
986                    }
987                }
988            }
989        }
990        includes
991    }
992}
993
994// endregion