Skip to main content

r/interpreter/builtins/
native_code.rs

1//! Native code builtins — .Call(), dyn.load(), dyn.unload(), etc.
2//!
3//! These replace the stubs in `stubs.rs` when the `native` feature is enabled.
4
5use std::path::PathBuf;
6
7use crate::interpreter::value::*;
8use crate::interpreter::BuiltinContext;
9use minir_macros::interpreter_builtin;
10
11// region: .Call
12
13/// .Call — invoke a compiled C function via the native code pipeline.
14///
15/// The first argument is the function name (character string).
16/// Remaining arguments are passed as SEXP values to the C function.
17///
18/// @param .NAME character string naming the C function
19/// @param ... arguments passed to the native function
20/// @return the value returned by the native function
21/// @namespace base
22#[interpreter_builtin(name = ".Call")]
23fn builtin_dot_call(
24    args: &[RValue],
25    _named: &[(String, RValue)],
26    ctx: &BuiltinContext,
27) -> Result<RValue, RError> {
28    if args.is_empty() {
29        return Err(RError::new(
30            RErrorKind::Argument,
31            ".Call requires at least one argument (the function name)".to_string(),
32        ));
33    }
34
35    // First arg is the symbol name — either a character string or a
36    // NativeSymbolInfo list (created by useDynLib in NAMESPACE).
37    let symbol_name = match &args[0] {
38        RValue::Vector(rv) => rv.as_character_scalar().ok_or_else(|| {
39            RError::new(
40                RErrorKind::Argument,
41                ".Call: first argument must be a character string or native symbol reference"
42                    .to_string(),
43            )
44        })?,
45        RValue::List(list) => {
46            // NativeSymbolInfo-like list — extract $name
47            list.values
48                .iter()
49                .find(|(k, _)| k.as_deref() == Some("name"))
50                .and_then(|(_, v)| v.as_vector()?.as_character_scalar())
51                .ok_or_else(|| {
52                    RError::new(
53                        RErrorKind::Argument,
54                        ".Call: native symbol reference must have a $name field".to_string(),
55                    )
56                })?
57        }
58        _ => {
59            return Err(RError::new(
60                RErrorKind::Argument,
61                ".Call: first argument must be a character string or native symbol reference"
62                    .to_string(),
63            ))
64        }
65    };
66
67    // Remaining args are passed to the native function
68    let native_args = &args[1..];
69
70    // Check for rlang FFI functions that we handle natively in Rust.
71    // This bypasses rlang's C code which uses r_abort() -> while(1) hang.
72    if let Some(result) = crate::interpreter::builtins::rlang_ffi::try_dispatch(
73        &symbol_name,
74        native_args,
75        _named,
76        ctx.env(),
77    ) {
78        return result;
79    }
80
81    ctx.interpreter().dot_call(&symbol_name, native_args)
82}
83
84// endregion
85
86// region: .External
87
88/// .External — invoke a compiled C function via the .External calling convention.
89///
90/// Like .Call but passes all arguments as a single pairlist SEXP.
91/// The C function signature is `SEXP fn(SEXP args)`.
92///
93/// @param .NAME character string naming the C function
94/// @param ... arguments passed to the native function
95/// @return the value returned by the native function
96/// @namespace base
97#[interpreter_builtin(name = ".External")]
98fn builtin_dot_external(
99    args: &[RValue],
100    _named: &[(String, RValue)],
101    ctx: &BuiltinContext,
102) -> Result<RValue, RError> {
103    if args.is_empty() {
104        return Err(RError::new(
105            RErrorKind::Argument,
106            ".External requires at least one argument (the function name)".to_string(),
107        ));
108    }
109
110    let symbol_name =
111        match &args[0] {
112            RValue::Vector(rv) => rv.as_character_scalar().ok_or_else(|| {
113                RError::new(
114                    RErrorKind::Argument,
115                    ".External: first argument must be a character string".to_string(),
116                )
117            })?,
118            RValue::List(list) => list
119                .values
120                .iter()
121                .find(|(k, _)| k.as_deref() == Some("name"))
122                .and_then(|(_, v)| v.as_vector()?.as_character_scalar())
123                .ok_or_else(|| {
124                    RError::new(
125                        RErrorKind::Argument,
126                        ".External: native symbol reference must have a $name field".to_string(),
127                    )
128                })?,
129            _ => return Err(RError::new(
130                RErrorKind::Argument,
131                ".External: first argument must be a character string or native symbol reference"
132                    .to_string(),
133            )),
134        };
135
136    let native_args = &args[1..];
137    ctx.interpreter().dot_external(&symbol_name, native_args)
138}
139
140// endregion
141
142// region: .External2
143
144/// .External2 — like .External but C function receives (call, op, args, env).
145///
146/// For our purposes, we dispatch the same as .External since we don't have
147/// a real call object or op to pass. The C function gets a pairlist of args.
148///
149/// @param .NAME character string naming the C function
150/// @param ... arguments passed to the native function
151/// @return the value returned by the native function
152/// @namespace base
153#[interpreter_builtin(name = ".External2")]
154fn builtin_dot_external2(
155    args: &[RValue],
156    _named: &[(String, RValue)],
157    ctx: &BuiltinContext,
158) -> Result<RValue, RError> {
159    if args.is_empty() {
160        return Err(RError::new(
161            RErrorKind::Argument,
162            ".External2 requires at least one argument (the function name)".to_string(),
163        ));
164    }
165
166    let symbol_name = match &args[0] {
167        RValue::Vector(rv) => rv.as_character_scalar().ok_or_else(|| {
168            RError::new(
169                RErrorKind::Argument,
170                ".External2: first argument must be a character string".to_string(),
171            )
172        })?,
173        RValue::List(list) => list
174            .values
175            .iter()
176            .find(|(k, _)| k.as_deref() == Some("name"))
177            .and_then(|(_, v)| v.as_vector()?.as_character_scalar())
178            .ok_or_else(|| {
179                RError::new(
180                    RErrorKind::Argument,
181                    ".External2: native symbol reference must have a $name field".to_string(),
182                )
183            })?,
184        _ => {
185            return Err(RError::new(
186                RErrorKind::Argument,
187                ".External2: first argument must be a character string".to_string(),
188            ))
189        }
190    };
191
192    // Check rlang FFI intercepts first
193    let native_args = &args[1..];
194    if let Some(result) = crate::interpreter::builtins::rlang_ffi::try_dispatch(
195        &symbol_name,
196        native_args,
197        _named,
198        ctx.env(),
199    ) {
200        return result;
201    }
202
203    // Dispatch as .External (pairlist calling convention)
204    ctx.interpreter().dot_external(&symbol_name, native_args)
205}
206
207// endregion
208
209// region: .C
210
211/// .C — invoke a compiled C function via the .C calling convention.
212///
213/// The first argument is the function name (character string or NativeSymbolInfo).
214/// Remaining arguments are R vectors whose raw data is passed directly to the
215/// C function as pointers (`double*`, `int*`, `char**`, etc.). The C function
216/// modifies the data in place, and the modified vectors are returned as a named list.
217///
218/// @param .NAME character string or native symbol reference naming the C function
219/// @param ... R vectors passed by pointer to the native function
220/// @return named list of the (possibly modified) arguments
221/// @namespace base
222#[interpreter_builtin(name = ".C")]
223fn builtin_dot_c(
224    args: &[RValue],
225    named: &[(String, RValue)],
226    ctx: &BuiltinContext,
227) -> Result<RValue, RError> {
228    if args.is_empty() {
229        return Err(RError::new(
230            RErrorKind::Argument,
231            ".C requires at least one argument (the function name)".to_string(),
232        ));
233    }
234
235    // First arg is the symbol name — either a character string or a
236    // NativeSymbolInfo list (created by useDynLib in NAMESPACE).
237    let symbol_name = match &args[0] {
238        RValue::Vector(rv) => rv.as_character_scalar().ok_or_else(|| {
239            RError::new(
240                RErrorKind::Argument,
241                ".C: first argument must be a character string or native symbol reference"
242                    .to_string(),
243            )
244        })?,
245        RValue::List(list) => {
246            // NativeSymbolInfo-like list — extract $name
247            list.values
248                .iter()
249                .find(|(k, _)| k.as_deref() == Some("name"))
250                .and_then(|(_, v)| v.as_vector()?.as_character_scalar())
251                .ok_or_else(|| {
252                    RError::new(
253                        RErrorKind::Argument,
254                        ".C: native symbol reference must have a $name field".to_string(),
255                    )
256                })?
257        }
258        _ => {
259            return Err(RError::new(
260                RErrorKind::Argument,
261                ".C: first argument must be a character string or native symbol reference"
262                    .to_string(),
263            ))
264        }
265    };
266
267    // Remaining positional args + named args are passed to the native function.
268    let native_args = &args[1..];
269
270    // Collect argument names. Positional args from index 1+ are unnamed;
271    // named args carry their names.
272    let mut all_args: Vec<RValue> = native_args.to_vec();
273    let mut arg_names: Vec<Option<String>> = vec![None; native_args.len()];
274
275    for (name, val) in named {
276        // Skip the PACKAGE argument — it's a hint for DLL lookup, not a data arg.
277        if name == "PACKAGE" {
278            continue;
279        }
280        arg_names.push(Some(name.clone()));
281        all_args.push(val.clone());
282    }
283
284    ctx.interpreter().dot_c(&symbol_name, &all_args, &arg_names)
285}
286
287// endregion
288
289// region: dyn.load / dyn.unload
290
291/// dyn.load — load a shared library (.so/.dylib).
292///
293/// @param x character string: path to the shared library
294/// @param local logical: whether to use local scope (ignored)
295/// @param now logical: whether to resolve symbols immediately (ignored)
296/// @param ... additional arguments (ignored)
297/// @return invisible NULL
298/// @namespace base
299#[interpreter_builtin(name = "dyn.load", min_args = 1)]
300fn builtin_dyn_load(
301    args: &[RValue],
302    _named: &[(String, RValue)],
303    ctx: &BuiltinContext,
304) -> Result<RValue, RError> {
305    tracing::debug!("dyn.load called");
306    let path = args[0]
307        .as_vector()
308        .and_then(|v| v.as_character_scalar())
309        .ok_or_else(|| {
310            RError::new(
311                RErrorKind::Argument,
312                "dyn.load: argument must be a file path (character string)".to_string(),
313            )
314        })?;
315
316    let dll_path = PathBuf::from(&path);
317    ctx.interpreter().dyn_load(&dll_path)?;
318    Ok(RValue::Null)
319}
320
321/// dyn.unload — unload a shared library.
322///
323/// @param x character string: path to the shared library
324/// @return invisible NULL
325/// @namespace base
326#[interpreter_builtin(name = "dyn.unload", min_args = 1)]
327fn builtin_dyn_unload(
328    args: &[RValue],
329    _named: &[(String, RValue)],
330    ctx: &BuiltinContext,
331) -> Result<RValue, RError> {
332    let path = args[0]
333        .as_vector()
334        .and_then(|v| v.as_character_scalar())
335        .ok_or_else(|| {
336            RError::new(
337                RErrorKind::Argument,
338                "dyn.unload: argument must be a file path (character string)".to_string(),
339            )
340        })?;
341
342    // Extract the library name from the path
343    let name = std::path::Path::new(&path)
344        .file_stem()
345        .and_then(|s| s.to_str())
346        .unwrap_or(&path);
347
348    ctx.interpreter().dyn_unload(name)?;
349    Ok(RValue::Null)
350}
351
352/// library.dynam — load a package's compiled code.
353///
354/// Called by `library()` when a package has `useDynLib` in NAMESPACE.
355/// Looks for the .so/.dylib in the package's `libs/` directory.
356///
357/// @param chname character: the shared library name (package name)
358/// @param package character: the package name
359/// @param lib.loc character: library path
360/// @return invisible NULL
361/// @namespace base
362#[interpreter_builtin(name = "library.dynam", min_args = 1)]
363fn builtin_library_dynam(
364    args: &[RValue],
365    _named: &[(String, RValue)],
366    ctx: &BuiltinContext,
367) -> Result<RValue, RError> {
368    let chname = args[0]
369        .as_vector()
370        .and_then(|v| v.as_character_scalar())
371        .ok_or_else(|| {
372            RError::new(
373                RErrorKind::Argument,
374                "library.dynam: 'chname' must be a character string".to_string(),
375            )
376        })?;
377
378    // Try to find the .so/.dylib in the package directory
379    let ext = if cfg!(target_os = "macos") {
380        "dylib"
381    } else {
382        "so"
383    };
384
385    // Search in loaded namespaces for the package directory
386    let namespaces = ctx.interpreter().loaded_namespaces.borrow();
387    if let Some(ns) = namespaces.get(&chname) {
388        let lib_path = ns.lib_path.join("libs").join(format!("{chname}.{ext}"));
389        if lib_path.is_file() {
390            drop(namespaces);
391            ctx.interpreter().dyn_load(&lib_path)?;
392            return Ok(RValue::Null);
393        }
394    }
395    drop(namespaces);
396
397    // If not found via namespace, try a direct load (the path might be absolute)
398    let direct = PathBuf::from(format!("{chname}.{ext}"));
399    if direct.is_file() {
400        ctx.interpreter().dyn_load(&direct)?;
401    }
402
403    Ok(RValue::Null)
404}
405
406/// library.dynam.unload — unload a package's compiled code.
407/// @namespace base
408#[interpreter_builtin(name = "library.dynam.unload", min_args = 1)]
409fn builtin_library_dynam_unload(
410    args: &[RValue],
411    _named: &[(String, RValue)],
412    ctx: &BuiltinContext,
413) -> Result<RValue, RError> {
414    let name = args[0]
415        .as_vector()
416        .and_then(|v| v.as_character_scalar())
417        .unwrap_or_default();
418    ctx.interpreter().dyn_unload(&name)?;
419    Ok(RValue::Null)
420}
421
422// endregion
423
424// region: Symbol inspection
425
426/// is.loaded — check if a native symbol is loaded.
427///
428/// @param symbol character: the symbol name
429/// @return logical
430/// @namespace base
431#[interpreter_builtin(name = "is.loaded", min_args = 1)]
432fn builtin_is_loaded(
433    args: &[RValue],
434    _named: &[(String, RValue)],
435    ctx: &BuiltinContext,
436) -> Result<RValue, RError> {
437    let name = args[0]
438        .as_vector()
439        .and_then(|v| v.as_character_scalar())
440        .unwrap_or_default();
441    let loaded = ctx.interpreter().is_symbol_loaded(&name);
442    Ok(RValue::vec(Vector::Logical(vec![Some(loaded)].into())))
443}
444
445/// getNativeSymbolInfo — get info about a loaded native symbol.
446/// @namespace base
447#[interpreter_builtin(name = "getNativeSymbolInfo", min_args = 1)]
448fn builtin_get_native_symbol_info(
449    args: &[RValue],
450    _named: &[(String, RValue)],
451    ctx: &BuiltinContext,
452) -> Result<RValue, RError> {
453    let name = args[0]
454        .as_vector()
455        .and_then(|v| v.as_character_scalar())
456        .unwrap_or_default();
457
458    // Check if the symbol exists
459    match ctx.interpreter().find_native_symbol(&name) {
460        Ok(_) => {
461            // Return a simple list with the symbol name
462            // (full NativeSymbolInfo struct is complex — this is a minimal impl)
463            Ok(RValue::List(RList::new(vec![(
464                Some("name".to_string()),
465                RValue::vec(Vector::Character(vec![Some(name)].into())),
466            )])))
467        }
468        Err(e) => Err(e),
469    }
470}
471
472// endregion