From 22142048008de0717aa1c51f3de0b53a8219821d Mon Sep 17 00:00:00 2001 From: Mossa Date: Tue, 30 Jun 2026 20:15:41 +0200 Subject: [PATCH] Add backports module for R 4.5 C-API compliance R's C API is stabilizing: several entry points used to access environments, closures, variables and the data pointer became non-API in R 4.5, replaced by new compliant functions. Code that needs to build against both old and new R has to branch per version. Port extendr-ffi's `backports` module (verbatim, with attribution): a set of small `unsafe fn` wrappers (`get_parent_env`, `get_var`, `get_var_safe`, `get_var_in_frame`, `get_closure_env/body/formals`, `dataptr`, `is_data_frame`) that dispatch to the pre- or post-4.5 entry point. The module declares its own cfg-gated `extern` block, so it does not depend on which symbols the generated bindings happen to include. build.rs now emits `r_4_4` / `r_4_5` cfg flags (with matching `rustc-check-cfg`) for the module to gate on. Co-Authored-By: Claude Opus 4.8 --- build.rs | 13 +++ src/backports.rs | 239 +++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 + 3 files changed, 254 insertions(+) create mode 100644 src/backports.rs diff --git a/build.rs b/build.rs index edbb34b6..6d58fcd3 100644 --- a/build.rs +++ b/build.rs @@ -324,6 +324,19 @@ fn set_r_version_vars(ver: &RVersionInfo) { println!("cargo:r_version_minor={}", ver.minor); // Becomes DEP_R_R_VERSION_MINOR for clients println!("cargo:r_version_patch={}", ver.patch); // Becomes DEP_R_R_VERSION_PATCH for clients println!("cargo:r_version_devel={}", ver.devel); // Becomes DEP_R_R_VERSION_DEVEL for clients + + // Version cfg flags, for use inside this crate (e.g. the `backports` + // module, which needs to pick the right entry point per R version). + println!("cargo:rustc-check-cfg=cfg(r_4_4)"); + println!("cargo:rustc-check-cfg=cfg(r_4_5)"); + let major: u32 = ver.major.parse().unwrap_or(0); + let minor: u32 = ver.minor.parse().unwrap_or(0); + if (major, minor) >= (4, 4) { + println!("cargo:rustc-cfg=r_4_4"); + } + if (major, minor) >= (4, 5) { + println!("cargo:rustc-cfg=r_4_5"); + } } /// Retrieve bindings from cache, if available. Errors out otherwise. diff --git a/src/backports.rs b/src/backports.rs new file mode 100644 index 00000000..b9dd8e78 --- /dev/null +++ b/src/backports.rs @@ -0,0 +1,239 @@ +//! Backport functions +//! +//! R's C API is changing and is stabilizing. As such some functions +//! that were available in previous versions of R are not available +//! in later versions of R, or they cause a warning in `R CMD check`. +//! +//! Use the functions in this module to ensure backwards compatibility. +//! +//! Ported from `extendr-ffi` (`src/backports.rs`). Gated on the `r_4_5` +//! cfg emitted by `build.rs`. + +// R 4.5 backports notes saved in wayback machine here: +// https://web.archive.org/web/20250325171443/https://rstudio.github.io/r-manuals/r-exts/The-R-API.html#moving-into-c-api-compliance + +use crate::{Rboolean, SEXP}; + +#[cfg(not(r_4_5))] +use crate::R_UnboundValue; + +extern "C" { + #[cfg(not(r_4_5))] + fn ENCLOS(x: SEXP) -> SEXP; + + #[cfg(r_4_5)] + fn R_ParentEnv(x: SEXP) -> SEXP; + + #[cfg(not(r_4_5))] + fn Rf_findVar(arg1: SEXP, arg2: SEXP) -> SEXP; + + #[cfg(r_4_5)] + fn R_getVar(arg1: SEXP, arg2: SEXP) -> SEXP; + + #[cfg(not(r_4_5))] + fn Rf_findVarInFrame(arg1: SEXP, arg2: SEXP) -> SEXP; + + #[cfg(r_4_5)] + fn R_getVarEx(arg1: SEXP, arg2: SEXP) -> SEXP; + + #[cfg(not(r_4_5))] + fn CLOENV(x: SEXP) -> SEXP; + + #[cfg(r_4_5)] + fn R_ClosureEnv(x: SEXP) -> SEXP; + + #[cfg(not(r_4_5))] + fn BODY(x: SEXP) -> SEXP; + + #[cfg(r_4_5)] + fn R_ClosureBody(x: SEXP) -> SEXP; + + #[cfg(not(r_4_5))] + fn FORMALS(x: SEXP) -> SEXP; + + #[cfg(r_4_5)] + fn R_ClosureFormals(x: SEXP) -> SEXP; + + #[cfg(not(r_4_5))] + fn DATAPTR(x: SEXP) -> *mut ::std::os::raw::c_void; + + #[cfg(r_4_5)] + fn DATAPTR_RO(x: SEXP) -> *const ::std::os::raw::c_void; + + #[cfg(not(r_4_5))] + fn Rf_isFrame(x: SEXP) -> Rboolean; + + #[cfg(r_4_5)] + fn Rf_isDataFrame(x: SEXP) -> Rboolean; +} + +/// Returns the enclosing environment of env, which will usually be of type ENVSXP, except for the special environment R_EmptyEnv, which terminates the environment chain; its enclosing environment is R_NilValue. +/// +/// # Safety +/// +/// This function dereferences a raw SEXP pointer. +/// The caller must ensure that `x` is a valid SEXP pointer to an environment. +#[inline] +pub unsafe fn get_parent_env(x: SEXP) -> SEXP { + #[cfg(not(r_4_5))] + { + ENCLOS(x) + } + #[cfg(r_4_5)] + { + R_ParentEnv(x) + } +} + +/// Returns a variable from an environment +/// +/// # Safety +/// +/// This function dereferences raw SEXP pointers. +/// The caller must ensure that `symbol` and `env` are valid SEXP pointers. +#[inline] +pub unsafe fn get_var(symbol: SEXP, env: SEXP) -> SEXP { + #[cfg(not(r_4_5))] + { + Rf_findVar(symbol, env) + } + #[cfg(r_4_5)] + { + R_getVar(symbol, env) + } +} + +/// Returns a variable from an environment, or `None` if unbound. +/// +/// On R < 4.5, uses `Rf_findVar` and checks against `R_UnboundValue`. +/// On R >= 4.5, uses `R_getVar` which signals an error if not found; +/// callers should wrap this with `catch_r_error` if needed. +/// +/// # Safety +/// +/// This function dereferences raw SEXP pointers. +/// The caller must ensure that `symbol` and `env` are valid SEXP pointers. +#[inline] +pub unsafe fn get_var_safe(symbol: SEXP, env: SEXP) -> Option { + #[cfg(not(r_4_5))] + { + let var = Rf_findVar(symbol, env); + if var == R_UnboundValue { + None + } else { + Some(var) + } + } + #[cfg(r_4_5)] + { + Some(R_getVar(symbol, env)) + } +} + +/// Returns the value of the requested variable in an environment +/// +/// # Safety +/// +/// This function dereferences raw SEXP pointers. +/// The caller must ensure that `symbol` and `env` are valid SEXP pointers. +#[inline] +pub unsafe fn get_var_in_frame(symbol: SEXP, env: SEXP) -> SEXP { + #[cfg(not(r_4_5))] + { + Rf_findVarInFrame(symbol, env) + } + #[cfg(r_4_5)] + { + R_getVarEx(env, symbol) + } +} + +/// Return the environment of a closure +/// +/// # Safety +/// +/// This function dereferences a raw SEXP pointer. +/// The caller must ensure that `x` is a valid SEXP pointer to a closure. +#[inline] +pub unsafe fn get_closure_env(x: SEXP) -> SEXP { + #[cfg(not(r_4_5))] + { + CLOENV(x) + } + #[cfg(r_4_5)] + { + R_ClosureEnv(x) + } +} + +/// Return the body of a closure +/// +/// # Safety +/// +/// This function dereferences a raw SEXP pointer. +/// The caller must ensure that `x` is a valid SEXP pointer to a closure. +#[inline] +pub unsafe fn get_closure_body(x: SEXP) -> SEXP { + #[cfg(not(r_4_5))] + { + BODY(x) + } + #[cfg(r_4_5)] + { + R_ClosureBody(x) + } +} + +/// Access a closure's arguments +/// +/// # Safety +/// +/// This function dereferences a raw SEXP pointer. +/// The caller must ensure that `x` is a valid SEXP pointer to a closure. +#[inline] +pub unsafe fn get_closure_formals(x: SEXP) -> SEXP { + #[cfg(not(r_4_5))] + { + FORMALS(x) + } + #[cfg(r_4_5)] + { + R_ClosureFormals(x) + } +} + +/// Access a DATAPTR +/// +/// # Safety +/// +/// This function dereferences a raw SEXP pointer. +/// The caller must ensure that `x` is a valid SEXP pointer. +#[inline] +pub unsafe fn dataptr(x: SEXP) -> *const ::std::os::raw::c_void { + #[cfg(not(r_4_5))] + { + DATAPTR(x) as *const _ + } + #[cfg(r_4_5)] + { + DATAPTR_RO(x) + } +} + +/// Check is data.frame +/// +/// # Safety +/// +/// This function dereferences a raw SEXP pointer. +/// The caller must ensure that `x` is a valid SEXP pointer. +#[inline] +pub unsafe fn is_data_frame(x: SEXP) -> Rboolean { + #[cfg(not(r_4_5))] + { + Rf_isFrame(x) + } + #[cfg(r_4_5)] + { + Rf_isDataFrame(x) + } +} diff --git a/src/lib.rs b/src/lib.rs index 273f76e4..48c05f80 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -66,6 +66,8 @@ include!(concat!(env!("OUT_DIR"), "/bindings.rs")); +pub mod backports; + #[non_exhaustive] #[repr(transparent)] #[derive(Debug)]