Repository: extendr/extendr
Pull request #888 (2025-03-18 20:48:08)

fix: Backports, R-devel WARNINGs
This PR addresses new WARNINGs in r-devel which require creating backports. 

Creating backports and new binding versions using libR-sys is a bit too much of a lift and is becoming too frequent of a task. To that end this PR introduces a new crate `extendr-ffi` and removes the dependency on `libR-sys` for both `extendr-api` and `extendr-engine`. 

`extendr-ffi` is a hand-curated set of bindings taken from `libR-sys`. It contains _only_ the bindings that are used by extendr and extendr-engine. Additionally it adds a `non-api` feature which is used by the extendr-api `non-api` feature. Previously the `non-api` feature in extendr-api could not compile due to manual removal of non-API functions from libR-sys. 

The new approach using extendr-api resolves this. Additionally, there is no need to use precomputed bindings simplifying the build process _greatly_ across all distributions. I suspect this will mean that integration with webR will be much simpler. 

Using `extendr-ffi` also can provide a path forward to closing https://github.com/extendr/extendr/issues/583

## Fixes 

- Closes https://github.com/extendr/extendr/issues/896 and all sub-issues
- Closes https://github.com/extendr/extendr/issues/889
- Closes https://github.com/extendr/extendr/issues/364
- Closes https://github.com/extendr/extendr/issues/677

## Deadline

CRAN packages that use extendr have a deadline of April 14th to remediate the issues. As such there is a bit of urgency to ensure that this gets addressed so we can try and make PRs to affected users' GitHub repositories.


Patch:
diff --git a/.gitignore b/.gitignore
index 2f08e91ee..35811622a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -41,3 +41,4 @@ extendr.Rproj
 **/extendrtests_[0-9].[0-9].[0-9].tar.gz
 **/.cargo/*
 .vscode/settings.json
+.zed/settings.json
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2ee76699c..6da9d4285 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@
 
 ### Changed
 
+- `extendr-api` and `extendr-engine` its dependency on `libR-sys` instead using the new `extendr-ffi` which is smaller and provided backports.
 - Breaking change: Adding `#[extendr]` above impl blocks no longer provides a default implementation for `impl From<T> for Robj` and `impl TryFrom<Robj> for T`. Instead, users may annotate their custom structs and enums with `#[extendr]` to get these default implementations. Note that a user can still derive their own implementations for these traits, and in this case should forego annotating their type with `#[extendr]`. With this change, users may use `#[extendr]` on multiple `impl <T>` blocks so long as they are contained within their own modules. <https://github.com/extendr/extendr/pull/882>
 - Enhancement: string comparisons are now implemented for `Rstr` using R's string intern mechanism. Making string comparisons _much_ faster. [[#845]](https://github.com/extendr/extendr/pull/845)
 - Breaking change: `RMatrix::get_rownames` and `RMatrix::get_colnames` now both
diff --git a/Cargo.toml b/Cargo.toml
index 2106660ac..146471dac 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,10 +1,16 @@
 [workspace]
 resolver = "2"
 
-members = ["extendr-api", "extendr-engine", "extendr-macros", "xtask"]
+members = [
+    "extendr-api",
+    "extendr-engine",
+    "extendr-ffi",
+    "extendr-macros",
+    "xtask",
+]
 
 [workspace.package]
-version = "0.7.1"
+version = "0.8.0"
 authors = [
     "andy-thomason <andy@andythomason.com>",
     "Thomas Down",
@@ -20,9 +26,5 @@ repository = "https://github.com/extendr/extendr"
 
 [workspace.dependencies]
 # When updating extendr's version, this version also needs to be updated
-extendr-macros = { path = "./extendr-macros", version = "0.7.1" }
-
-# When uncommenting this, do not forget to uncomment the same line in
-# ./tests/extendrtests/src/rust/Cargo.toml, and "Run R integration tests using
-# {rextendr}" on .github/workflows/test.yml !
-libR-sys = { version = "0.7.1" }
+extendr-macros = { path = "./extendr-macros", version = "0.8.0" }
+extendr-ffi = { path = "./extendr-ffi" }
diff --git a/extendr-api/Cargo.toml b/extendr-api/Cargo.toml
index e699b5bf0..8226bd86c 100644
--- a/extendr-api/Cargo.toml
+++ b/extendr-api/Cargo.toml
@@ -11,7 +11,7 @@ repository.workspace = true
 rust-version = "1.65"
 
 [dependencies]
-libR-sys = { workspace = true }
+extendr-ffi = { workspace = true }
 extendr-macros = { workspace = true }
 once_cell = "1"
 paste = "1.0.5"
@@ -26,6 +26,7 @@ faer = { version = "0.20", optional = true }
 extendr-engine = { path = "../extendr-engine" }
 rstest = "0.22.0"
 
+
 [features]
 
 # Features to modify behaviour of returning Result<T,E> from extendr to R, instead of unwrap-throw_r_error().
@@ -36,7 +37,6 @@ result_condition = []
 
 # This dummy feature enables all features that increase the functionality of
 # extendr, via conversions or R features. Features that change behaviour
-# but do not add functionality (such as `libR-sys/use-bindgen`) are excluded
 full-functionality = [
     "graphics",
     "either",
@@ -50,7 +50,7 @@ full-functionality = [
 # of non-API items in packages. You may enable this feature, to generate
 # bindings to these non-API items, and their usage in extendr
 # BEWARE: this feature does not work, as `bindgen` use in extendr is deprecated
-non-api = []
+non-api = ["extendr-ffi/non-api"]
 
 # libc is needed to allocate a DevDesc (c.f., https://bugs.r-project.org/show_bug.cgi?id=18292)
 graphics = ["libc"]
@@ -67,6 +67,7 @@ tests-graphics = ["tests-minimal", "graphics"]
 # Literally all features to test
 tests-all = ["tests", "graphics"]
 
+
 [package.metadata.docs.rs]
 features = ["full-functionality"]
 
diff --git a/extendr-api/README.md b/extendr-api/README.md
index 38228c015..d8fecc349 100644
--- a/extendr-api/README.md
+++ b/extendr-api/README.md
@@ -262,8 +262,7 @@ extendr-api
 
 Finally, there are parts of R's API that are deemed non-API, in that R packages
 on CRAN are recommended not to have these available in packages. If you want
-to have access to them, you may use `non-api` to expose them. However, it requires
-setting up `bindgen`, and specifically following setup instructions for [`libR-sys`](https://github.com/extendr/libR-sys/?tab=readme-ov-file#building-bindings-from-source-advanced).
+to have access to them, you may use the `non-api` feature to expose them.
 
 ## License
 
diff --git a/extendr-api/build.rs b/extendr-api/build.rs
index cace92cf4..86b20ba1d 100644
--- a/extendr-api/build.rs
+++ b/extendr-api/build.rs
@@ -1,9 +1,15 @@
 use std::env;
 
 fn main() {
+    println!("cargo:rustc-check-cfg=cfg(use_objsxp)");
+    println!("cargo:rustc-check-cfg=cfg(use_r_newenv)");
+    println!("cargo:rustc-check-cfg=cfg(use_r_ge_version_14)");
+    println!("cargo:rustc-check-cfg=cfg(use_r_ge_version_15)");
+    println!("cargo:rustc-check-cfg=cfg(use_r_altlist)");
+
     // The R version information is needed to handle the API differences
-    // between versions. `These DEP_R_R_VERSION_*` are provided by libR-sys
-    // (for more details, please refer to libR-sys's `build.rs`).
+    // between versions. `These DEP_R_R_VERSION_*` are provided by extendr-ffi
+    // (for more details, please refer to extendr-ffi's `build.rs`).
     // The current approach is to add a config flag per feature. When there are
     // too many features, we might need to consider switching to per-version
     // config flags (e.g. `r410`).
diff --git a/extendr-api/src/functions.rs b/extendr-api/src/functions.rs
index c1283069a..937420f64 100644
--- a/extendr-api/src/functions.rs
+++ b/extendr-api/src/functions.rs
@@ -1,5 +1,9 @@
 use crate as extendr_api;
 use crate::*;
+use extendr_ffi::{
+    R_BaseEnv, R_BaseNamespace, R_BlankScalarString, R_BlankString, R_EmptyEnv, R_GetCurrentEnv,
+    R_GlobalEnv, R_NaString, R_NamespaceRegistry, R_NilValue, R_Srcref, R_dot_Generic,
+};
 
 #[cfg(feature = "non-api")]
 /// Get a global variable from global_env() and ancestors.
@@ -119,6 +123,7 @@ pub fn empty_env() -> Environment {
 /// ```
 #[cfg(use_r_newenv)]
 pub fn new_env(parent: Environment, hash: bool, capacity: i32) -> Environment {
+    use extendr_ffi::R_NewEnv;
     single_threaded(|| unsafe {
         let env = R_NewEnv(parent.robj.get(), hash as i32, capacity);
         Robj::from_sexp(env).try_into().unwrap()
@@ -203,7 +208,7 @@ pub fn blank_scalar_string() -> Robj {
 /// ```
 pub fn parse(code: &str) -> Result<Expressions> {
     single_threaded(|| unsafe {
-        use libR_sys::*;
+        use extendr_ffi::{ParseStatus, R_NilValue, R_ParseVector};
         let mut status = ParseStatus::PARSE_NULL;
         let status_ptr = &mut status as *mut _;
         let codeobj: Robj = code.into();
diff --git a/extendr-api/src/graphics/device_descriptor.rs b/extendr-api/src/graphics/device_descriptor.rs
index 548d4f167..783646852 100644
--- a/extendr-api/src/graphics/device_descriptor.rs
+++ b/extendr-api/src/graphics/device_descriptor.rs
@@ -74,7 +74,7 @@ pub(crate) enum DevCapLocator {
     Yes = 2,
 }
 
-/// A builder of [libR_sys::DevDesc].
+/// A builder of [extendr_ffi::DevDesc].
 ///
 // # Design notes (which feels a bit too internal to be exposed as an official document)
 //
diff --git a/extendr-api/src/graphics/device_driver.rs b/extendr-api/src/graphics/device_driver.rs
index 62aa2a5d3..3fb0e6d06 100644
--- a/extendr-api/src/graphics/device_driver.rs
+++ b/extendr-api/src/graphics/device_driver.rs
@@ -1,10 +1,11 @@
-use core::slice;
-
-use crate::*;
-use libR_sys::*;
-
 use super::{device_descriptor::*, Device, Raster, TextMetric};
-
+use crate::*;
+use core::slice;
+use extendr_ffi::{
+    pDevDesc, pGEcontext, DevDesc, GEaddDevice2, GEcreateDevDesc, GEinitDisplayList,
+    R_CheckDeviceAvailable, R_GE_checkVersionOrDie, R_GE_definitions, R_GE_gcontext, R_GE_version,
+    R_NilValue, Rboolean,
+};
 /// The underlying C structure `DevDesc` has two fields related to clipping:
 ///
 /// - `canClip`
@@ -908,9 +909,8 @@ pub trait DeviceDriver: std::marker::Sized {
                 (*p_dev_desc).fillStroke = None;
 
                 (*p_dev_desc).capabilities = None;
-            }
-        } // unsafe ends here
-
+            } // unsafe ends here
+        }
         let device_name = CString::new(device_name).unwrap();
 
         single_threaded(|| unsafe {
diff --git a/extendr-api/src/graphics/mod.rs b/extendr-api/src/graphics/mod.rs
index e078f862b..3d0ecaf3d 100644
--- a/extendr-api/src/graphics/mod.rs
+++ b/extendr-api/src/graphics/mod.rs
@@ -63,7 +63,7 @@
 //!     let device_driver = MyDevice {
 //!         welcome_message: welcome_message.as_str(),
 //!     };
-//!     
+//!
 //!     let device_descriptor = DeviceDescriptor::new();
 //!     let device = device_driver.create_device::<MyDevice>(device_descriptor, "my device");
 //! }
@@ -77,10 +77,9 @@
 //! ```
 
 use crate::*;
-use libR_sys::*;
 
 // These are used in the callback functions.
-pub use libR_sys::{DevDesc, R_GE_gcontext};
+pub use extendr_ffi::{cetype_t, graphics::*, R_NilValue, Rf_NoDevices, Rf_NumDevices};
 
 pub mod color;
 pub mod device_descriptor;
@@ -326,7 +325,7 @@ impl Context {
     /// Set the line end type.
     /// ```ignore
     ///   LineEnd::RoundCap
-    ///   LineEnd::ButtCap  
+    ///   LineEnd::ButtCap
     ///   LineEnd::SquareCap
     /// ```
     pub fn line_end(&mut self, lend: LineEnd) -> &mut Self {
diff --git a/extendr-api/src/io/load.rs b/extendr-api/src/io/load.rs
index 260c22acb..b78e6a6b6 100644
--- a/extendr-api/src/io/load.rs
+++ b/extendr-api/src/io/load.rs
@@ -1,9 +1,8 @@
+use super::PstreamFormat;
 use crate::{catch_r_error, error::Error, error::Result, robj::Robj};
-use libR_sys::*;
+use extendr_ffi::{R_NilValue, R_Unserialize, R_inpstream_st, R_inpstream_t, SEXP};
 use std::io::Read;
 
-use super::PstreamFormat;
-
 pub struct ReadHook {
     func: Option<unsafe extern "C" fn(arg1: SEXP, arg2: SEXP) -> SEXP>,
     data: SEXP,
diff --git a/extendr-api/src/io/mod.rs b/extendr-api/src/io/mod.rs
index b3a348905..96383b7de 100644
--- a/extendr-api/src/io/mod.rs
+++ b/extendr-api/src/io/mod.rs
@@ -1,4 +1,4 @@
-pub type PstreamFormat = libR_sys::R_pstream_format_t;
+pub type PstreamFormat = extendr_ffi::R_pstream_format_t;
 
 mod load;
 mod save;
diff --git a/extendr-api/src/io/save.rs b/extendr-api/src/io/save.rs
index a892685d8..03aa4c69a 100644
--- a/extendr-api/src/io/save.rs
+++ b/extendr-api/src/io/save.rs
@@ -1,11 +1,13 @@
 //! Wrapper for R output streams.
 
+use super::PstreamFormat;
 use crate::{catch_r_error, error::Error, error::Result, robj::GetSexp};
-use libR_sys::*;
+use extendr_ffi::{
+    R_NilValue, R_Serialize, R_outpstream_st, R_outpstream_t, R_pstream_data_t, R_pstream_format_t,
+    SEXP,
+};
 use std::io::Write;
 
-use super::PstreamFormat;
-
 /// The hook will convert some objects into strings.
 pub struct WriteHook {
     pub func: unsafe extern "C" fn(arg1: SEXP, arg2: SEXP) -> SEXP,
@@ -47,7 +49,7 @@ impl<W: Write> OutStream<W> {
                 unsafe { (None, R_NilValue) }
             };
 
-            let r_state = libR_sys::R_outpstream_st {
+            let r_state = extendr_ffi::R_outpstream_st {
                 data: std::ptr::null_mut(),
                 type_: format as R_pstream_format_t,
                 version,
diff --git a/extendr-api/src/iter.rs b/extendr-api/src/iter.rs
index 425dd9d7d..f7c7d42c6 100644
--- a/extendr-api/src/iter.rs
+++ b/extendr-api/src/iter.rs
@@ -1,7 +1,7 @@
 use crate::*;
 
+use extendr_ffi::{R_NaString, R_NilValue, Rf_isFactor, INTEGER, STRING_ELT, TYPEOF};
 use wrapper::symbol::levels_symbol;
-
 /// Iterator over name-value pairs in lists.
 pub type NamedListIter = std::iter::Zip<StrIter, ListIter>;
 
diff --git a/extendr-api/src/lang_macros.rs b/extendr-api/src/lang_macros.rs
index d669b24b6..fcf7d6ce4 100644
--- a/extendr-api/src/lang_macros.rs
+++ b/extendr-api/src/lang_macros.rs
@@ -4,8 +4,7 @@
 use crate::robj::GetSexp;
 use crate::robj::Robj;
 use crate::single_threaded;
-use libR_sys::*;
-
+use extendr_ffi::{R_NilValue, Rf_cons, Rf_lang1, SETCDR, SET_TAG, SEXP};
 /// Convert a list of tokens to an array of tuples.
 #[doc(hidden)]
 #[macro_export]
diff --git a/extendr-api/src/lib.rs b/extendr-api/src/lib.rs
index 5aba4b0c5..1282bf9c8 100644
--- a/extendr-api/src/lib.rs
+++ b/extendr-api/src/lib.rs
@@ -314,9 +314,20 @@ pub mod io;
 pub mod iter;
 pub mod lang_macros;
 pub mod metadata;
+pub mod na;
+pub mod optional;
 pub mod ownership;
 pub mod prelude;
 pub mod rmacros;
+pub mod robj;
+pub mod scalar;
+pub mod thread_safety;
+pub mod wrapper;
+
+pub use robj::Robj;
+pub use std::convert::{TryFrom, TryInto};
+pub use std::ops::Deref;
+pub use std::ops::DerefMut;
 
 #[cfg(feature = "serde")]
 pub mod serializer;
@@ -327,23 +338,8 @@ pub mod deserializer;
 #[cfg(feature = "graphics")]
 pub mod graphics;
 
-pub mod robj;
-pub mod scalar;
-pub mod thread_safety;
-pub mod wrapper;
-
-pub mod na;
-
-pub mod optional;
-
 pub(crate) mod conversions;
 
-pub use std::convert::{TryFrom, TryInto};
-pub use std::ops::Deref;
-pub use std::ops::DerefMut;
-
-pub use robj::Robj;
-
 //////////////////////////////////////////////////
 // Note these pub use statements are deprecated
 //
@@ -361,6 +357,7 @@ pub use wrapper::*;
 
 pub use extendr_macros::*;
 
+use extendr_ffi::SEXPTYPE;
 use scalar::Rbool;
 
 //////////////////////////////////////////////////
@@ -391,25 +388,22 @@ pub use std::collections::HashMap;
 
 /// This is needed for the generation of wrappers.
 #[doc(hidden)]
-pub use libR_sys::DllInfo;
+pub use extendr_ffi::DllInfo;
 
 /// This is necessary for `#[extendr]`-impl
 #[doc(hidden)]
-pub use libR_sys::R_ExternalPtrAddr;
+pub use extendr_ffi::R_ExternalPtrAddr;
 
 /// This is used in `#[extendr(use_rng = true)]` on `fn`-items.
 #[doc(hidden)]
-pub use libR_sys::GetRNGstate;
+pub use extendr_ffi::GetRNGstate;
 
 /// This is used in `#[extendr(use_rng = true)]` on `fn`-items.
 #[doc(hidden)]
-pub use libR_sys::PutRNGstate;
-
-#[doc(hidden)]
-pub use libR_sys::SEXP;
+pub use extendr_ffi::PutRNGstate;
 
 #[doc(hidden)]
-use libR_sys::*;
+pub use extendr_ffi::SEXP;
 
 #[doc(hidden)]
 use std::ffi::CString;
@@ -425,12 +419,12 @@ pub struct CallMethod {
 
 unsafe fn make_method_def(
     cstrings: &mut Vec<std::ffi::CString>,
-    rmethods: &mut Vec<libR_sys::R_CallMethodDef>,
+    rmethods: &mut Vec<extendr_ffi::R_CallMethodDef>,
     func: &metadata::Func,
     wrapped_name: &str,
 ) {
     cstrings.push(std::ffi::CString::new(wrapped_name).unwrap());
-    rmethods.push(libR_sys::R_CallMethodDef {
+    rmethods.push(extendr_ffi::R_CallMethodDef {
         name: cstrings.last().unwrap().as_ptr(),
         fun: Some(std::mem::transmute(func.func_ptr)),
         numArgs: func.args.len() as i32,
@@ -440,7 +434,7 @@ unsafe fn make_method_def(
 // Internal function used to implement the .Call interface.
 // This is called from the code generated by the #[extendr] attribute.
 #[doc(hidden)]
-pub unsafe fn register_call_methods(info: *mut libR_sys::DllInfo, metadata: Metadata) {
+pub unsafe fn register_call_methods(info: *mut extendr_ffi::DllInfo, metadata: Metadata) {
     let mut rmethods = Vec::new();
     let mut cstrings = Vec::new();
     for func in metadata.functions {
@@ -455,13 +449,13 @@ pub unsafe fn register_call_methods(info: *mut libR_sys::DllInfo, metadata: Meta
         }
     }
 
-    rmethods.push(libR_sys::R_CallMethodDef {
+    rmethods.push(extendr_ffi::R_CallMethodDef {
         name: std::ptr::null(),
         fun: None,
         numArgs: 0,
     });
 
-    libR_sys::R_registerRoutines(
+    extendr_ffi::R_registerRoutines(
         info,
         std::ptr::null(),
         rmethods.as_ptr(),
@@ -470,8 +464,8 @@ pub unsafe fn register_call_methods(info: *mut libR_sys::DllInfo, metadata: Meta
     );
 
     // This seems to allow both symbols and strings,
-    libR_sys::R_useDynamicSymbols(info, Rboolean::FALSE);
-    libR_sys::R_forceSymbols(info, Rboolean::FALSE);
+    extendr_ffi::R_useDynamicSymbols(info, extendr_ffi::Rboolean::FALSE);
+    extendr_ffi::R_forceSymbols(info, extendr_ffi::Rboolean::FALSE);
 }
 
 /// Type of R objects used by [Robj::rtype].
@@ -539,73 +533,70 @@ pub enum Rany<'a> {
 /// Convert extendr's Rtype to R's SEXPTYPE.
 /// Panics if the type is Unknown.
 pub fn rtype_to_sxp(rtype: Rtype) -> SEXPTYPE {
-    use Rtype::*;
-    use SEXPTYPE::*;
+    use extendr_ffi::SEXPTYPE;
     match rtype {
-        Null => NILSXP,
-        Symbol => SYMSXP,
-        Pairlist => LISTSXP,
-        Function => CLOSXP,
-        Environment => ENVSXP,
-        Promise => PROMSXP,
-        Language => LANGSXP,
-        Special => SPECIALSXP,
-        Builtin => BUILTINSXP,
-        Rstr => CHARSXP,
-        Logicals => LGLSXP,
-        Integers => INTSXP,
-        Doubles => REALSXP,
-        Complexes => CPLXSXP,
-        Strings => STRSXP,
-        Dot => DOTSXP,
-        Any => ANYSXP,
-        List => VECSXP,
-        Expressions => EXPRSXP,
-        Bytecode => BCODESXP,
-        ExternalPtr => EXTPTRSXP,
-        WeakRef => WEAKREFSXP,
-        Raw => RAWSXP,
+        Rtype::Null => SEXPTYPE::NILSXP,
+        Rtype::Symbol => SEXPTYPE::SYMSXP,
+        Rtype::Pairlist => SEXPTYPE::LISTSXP,
+        Rtype::Function => SEXPTYPE::CLOSXP,
+        Rtype::Environment => SEXPTYPE::ENVSXP,
+        Rtype::Promise => SEXPTYPE::PROMSXP,
+        Rtype::Language => SEXPTYPE::LANGSXP,
+        Rtype::Special => SEXPTYPE::SPECIALSXP,
+        Rtype::Builtin => SEXPTYPE::BUILTINSXP,
+        Rtype::Rstr => SEXPTYPE::CHARSXP,
+        Rtype::Logicals => SEXPTYPE::LGLSXP,
+        Rtype::Integers => SEXPTYPE::INTSXP,
+        Rtype::Doubles => SEXPTYPE::REALSXP,
+        Rtype::Complexes => SEXPTYPE::CPLXSXP,
+        Rtype::Strings => SEXPTYPE::STRSXP,
+        Rtype::Dot => SEXPTYPE::DOTSXP,
+        Rtype::Any => SEXPTYPE::ANYSXP,
+        Rtype::List => SEXPTYPE::VECSXP,
+        Rtype::Expressions => SEXPTYPE::EXPRSXP,
+        Rtype::Bytecode => SEXPTYPE::BCODESXP,
+        Rtype::ExternalPtr => SEXPTYPE::EXTPTRSXP,
+        Rtype::WeakRef => SEXPTYPE::WEAKREFSXP,
+        Rtype::Raw => SEXPTYPE::RAWSXP,
         #[cfg(not(use_objsxp))]
-        S4 => S4SXP,
+        Rtype::S4 => SEXPTYPE::S4SXP,
         #[cfg(use_objsxp)]
-        S4 => OBJSXP,
-        Unknown => panic!("attempt to use Unknown Rtype"),
+        Rtype::S4 => SEXPTYPE::OBJSXP,
+        Rtype::Unknown => panic!("attempt to use Unknown Rtype"),
     }
 }
 
 /// Convert R's SEXPTYPE to extendr's Rtype.
 pub fn sxp_to_rtype(sxptype: SEXPTYPE) -> Rtype {
-    use Rtype::*;
-    use SEXPTYPE::*;
     match sxptype {
-        NILSXP => Null,
-        SYMSXP => Symbol,
-        LISTSXP => Pairlist,
-        CLOSXP => Function,
-        ENVSXP => Environment,
-        PROMSXP => Promise,
-        LANGSXP => Language,
-        SPECIALSXP => Special,
-        BUILTINSXP => Builtin,
-        CHARSXP => Rstr,
-        LGLSXP => Logicals,
-        INTSXP => Integers,
-        REALSXP => Doubles,
-        CPLXSXP => Complexes,
-        STRSXP => Strings,
-        DOTSXP => Dot,
-        ANYSXP => Any,
-        VECSXP => List,
-        EXPRSXP => Expressions,
-        BCODESXP => Bytecode,
-        EXTPTRSXP => ExternalPtr,
-        WEAKREFSXP => WeakRef,
-        RAWSXP => Raw,
+        SEXPTYPE::NILSXP => Rtype::Null,
+        SEXPTYPE::SYMSXP => Rtype::Symbol,
+        SEXPTYPE::LISTSXP => Rtype::Pairlist,
+        SEXPTYPE::CLOSXP => Rtype::Function,
+        SEXPTYPE::ENVSXP => Rtype::Environment,
+        SEXPTYPE::PROMSXP => Rtype::Promise,
+        SEXPTYPE::LANGSXP => Rtype::Language,
+        SEXPTYPE::SPECIALSXP => Rtype::Special,
+        SEXPTYPE::BUILTINSXP => Rtype::Builtin,
+        SEXPTYPE::CHARSXP => Rtype::Rstr,
+        SEXPTYPE::LGLSXP => Rtype::Logicals,
+        SEXPTYPE::INTSXP => Rtype::Integers,
+        SEXPTYPE::REALSXP => Rtype::Doubles,
+        SEXPTYPE::CPLXSXP => Rtype::Complexes,
+        SEXPTYPE::STRSXP => Rtype::Strings,
+        SEXPTYPE::DOTSXP => Rtype::Dot,
+        SEXPTYPE::ANYSXP => Rtype::Any,
+        SEXPTYPE::VECSXP => Rtype::List,
+        SEXPTYPE::EXPRSXP => Rtype::Expressions,
+        SEXPTYPE::BCODESXP => Rtype::Bytecode,
+        SEXPTYPE::EXTPTRSXP => Rtype::ExternalPtr,
+        SEXPTYPE::WEAKREFSXP => Rtype::WeakRef,
+        SEXPTYPE::RAWSXP => Rtype::Raw,
         #[cfg(not(use_objsxp))]
-        S4SXP => S4,
+        SEXPTYPE::S4SXP => Rtype::S4,
         #[cfg(use_objsxp)]
-        OBJSXP => S4,
-        _ => Unknown,
+        SEXPTYPE::OBJSXP => Rtype::S4,
+        _ => Rtype::Unknown,
     }
 }
 
@@ -614,7 +605,7 @@ const PRINTF_NO_FMT_CSTRING: &[std::os::raw::c_char] = &[37, 115, 0]; // same as
 pub fn print_r_output<T: Into<Vec<u8>>>(s: T) {
     let cs = CString::new(s).expect("NulError");
     unsafe {
-        Rprintf(PRINTF_NO_FMT_CSTRING.as_ptr(), cs.as_ptr());
+        extendr_ffi::Rprintf(PRINTF_NO_FMT_CSTRING.as_ptr(), cs.as_ptr());
     }
 }
 
@@ -622,7 +613,7 @@ pub fn print_r_output<T: Into<Vec<u8>>>(s: T) {
 pub fn print_r_error<T: Into<Vec<u8>>>(s: T) {
     let cs = CString::new(s).expect("NulError");
     unsafe {
-        REprintf(PRINTF_NO_FMT_CSTRING.as_ptr(), cs.as_ptr());
+        extendr_ffi::REprintf(PRINTF_NO_FMT_CSTRING.as_ptr(), cs.as_ptr());
     }
 }
 
diff --git a/extendr-api/src/na.rs b/extendr-api/src/na.rs
index ca7fd7f2b..890d50631 100644
--- a/extendr-api/src/na.rs
+++ b/extendr-api/src/na.rs
@@ -1,4 +1,4 @@
-use libR_sys::{R_IsNA, R_NaReal};
+use extendr_ffi::{R_IsNA, R_NaReal};
 use once_cell::sync::Lazy;
 use std::alloc::{self, Layout};
 
diff --git a/extendr-api/src/ownership.rs b/extendr-api/src/ownership.rs
index 1ce0276ef..fa11cd0d3 100644
--- a/extendr-api/src/ownership.rs
+++ b/extendr-api/src/ownership.rs
@@ -12,7 +12,7 @@ use once_cell::sync::Lazy;
 use std::collections::hash_map::{Entry, HashMap};
 use std::sync::Mutex;
 
-use libR_sys::{
+use extendr_ffi::{
     R_NilValue, R_PreserveObject, R_ReleaseObject, R_xlen_t, Rf_allocVector, Rf_protect,
     Rf_unprotect, LENGTH, SET_VECTOR_ELT, SEXP, SEXPTYPE, VECTOR_ELT,
 };
@@ -23,7 +23,7 @@ mod send_sexp {
     //! This can lead to soundness issues, therefore accessing the `SEXP` has
     //! to happen through the unsafe method [`SendSEXP::inner`].
     //!
-    use libR_sys::SEXP;
+    use extendr_ffi::SEXP;
 
     /// A wrapper around R's pointer type `SEXP` that is `Send`.
     #[derive(Debug, Clone, PartialEq, Eq, Hash)]
@@ -272,7 +272,7 @@ mod test {
     use super::*;
     use crate as extendr_api;
     use crate::*;
-    use libR_sys::{Rf_ScalarInteger, Rf_protect};
+    use extendr_ffi::{Rf_ScalarInteger, Rf_protect};
 
     #[test]
     fn basic_test() {
diff --git a/extendr-api/src/robj/into_robj.rs b/extendr-api/src/robj/into_robj.rs
index 4c3f5bd23..7a0fcdcfc 100644
--- a/extendr-api/src/robj/into_robj.rs
+++ b/extendr-api/src/robj/into_robj.rs
@@ -1,7 +1,10 @@
 use super::*;
 use crate::scalar::Scalar;
 use crate::single_threaded;
-
+use extendr_ffi::{
+    cetype_t, R_BlankString, R_NaInt, R_NaReal, R_NaString, R_NilValue, Rcomplex, Rf_mkCharLenCE,
+    COMPLEX, INTEGER, LOGICAL, RAW, REAL, SET_STRING_ELT, SEXPTYPE,
+};
 mod repeat_into_robj;
 
 /// Returns an `CHARSXP` based on the provided `&str`.
@@ -728,12 +731,12 @@ mod test {
         test! {
             let int_vec = vec![3,4,0,-2];
             let int_vec_robj: Robj = int_vec.clone().into();
-            // unsafe { libR_sys::Rf_PrintValue(int_vec_robj.get())}
+            // unsafe { extendr_ffi::Rf_PrintValue(int_vec_robj.get())}
             assert_eq!(int_vec_robj.as_integer_slice().unwrap(), &int_vec);
 
             let rint_vec = vec![Rint::new(3), Rint::new(4), Rint::new(0), Rint::new(-2)];
             let rint_vec_robj: Robj = rint_vec.into();
-            // unsafe { libR_sys::Rf_PrintValue(rint_vec_robj.get())}
+            // unsafe { extendr_ffi::Rf_PrintValue(rint_vec_robj.get())}
             assert_eq!(rint_vec_robj.as_integer_slice().unwrap(), &int_vec);
         }
     }
diff --git a/extendr-api/src/robj/into_robj/repeat_into_robj.rs b/extendr-api/src/robj/into_robj/repeat_into_robj.rs
index 5024999df..5d99b3ce8 100644
--- a/extendr-api/src/robj/into_robj/repeat_into_robj.rs
+++ b/extendr-api/src/robj/into_robj/repeat_into_robj.rs
@@ -1,5 +1,5 @@
 use super::*;
-
+use extendr_ffi::Rcomplex;
 //TODO: It could be an idea to make this a attribute macro for users,
 // if they wish to specialise their own types as representable in R.
 
diff --git a/extendr-api/src/robj/mod.rs b/extendr-api/src/robj/mod.rs
index 5b1389b13..1a0b1173b 100644
--- a/extendr-api/src/robj/mod.rs
+++ b/extendr-api/src/robj/mod.rs
@@ -14,18 +14,20 @@ use std::iter::IntoIterator;
 use std::ops::{Range, RangeInclusive};
 use std::os::raw;
 
-use libR_sys::*;
-use SEXPTYPE::*;
+use extendr_ffi::{
+    dataptr, R_IsNA, R_NilValue, R_compute_identical, R_tryEval, Rboolean, Rcomplex, Rf_getAttrib,
+    Rf_setAttrib, Rf_xlength, COMPLEX, INTEGER, LOGICAL, PRINTNAME, RAW, REAL, SEXPTYPE,
+    SEXPTYPE::*, STRING_ELT, STRING_PTR_RO, TYPEOF, XLENGTH,
+};
 
+use crate::scalar::{Rbool, Rfloat, Rint};
+use crate::*;
 pub use into_robj::*;
 pub use iter::*;
 pub use operators::Operators;
 use prelude::{c64, Rcplx};
 pub use rinternals::Rinternals;
 
-use crate::scalar::{Rbool, Rfloat, Rint};
-use crate::*;
-
 mod debug;
 mod into_robj;
 mod operators;
@@ -165,7 +167,7 @@ pub trait Slices: GetSexp {
     /// Creating this slice will also instantiate an Altrep objects.
     unsafe fn as_typed_slice_raw<T>(&self) -> &[T] {
         let len = XLENGTH(self.get()) as usize;
-        let data = DATAPTR_RO(self.get()) as *const T;
+        let data = dataptr(self.get()) as *const T;
         std::slice::from_raw_parts(data, len)
     }
 
@@ -178,7 +180,7 @@ pub trait Slices: GetSexp {
     /// Not all objects (especially not list and strings) support this.
     unsafe fn as_typed_slice_raw_mut<T>(&mut self) -> &mut [T] {
         let len = XLENGTH(self.get()) as usize;
-        let data = DATAPTR(self.get_mut()) as *mut T;
+        let data = dataptr(self.get_mut()) as *mut T;
         std::slice::from_raw_parts_mut(data, len)
     }
 }
@@ -356,14 +358,14 @@ impl Robj {
                 let sexp = self.get();
                 use SEXPTYPE::*;
                 match self.sexptype() {
-                    STRSXP => STRING_ELT(sexp, 0) == libR_sys::R_NaString,
-                    INTSXP => *(INTEGER(sexp)) == libR_sys::R_NaInt,
-                    LGLSXP => *(LOGICAL(sexp)) == libR_sys::R_NaInt,
+                    STRSXP => STRING_ELT(sexp, 0) == extendr_ffi::R_NaString,
+                    INTSXP => *(INTEGER(sexp)) == extendr_ffi::R_NaInt,
+                    LGLSXP => *(LOGICAL(sexp)) == extendr_ffi::R_NaInt,
                     REALSXP => R_IsNA(*(REAL(sexp))) != 0,
                     CPLXSXP => R_IsNA((*COMPLEX(sexp)).r) != 0,
                     // a character vector contains `CHARSXP`, and thus you
                     // seldom have `Robj`'s that are `CHARSXP` themselves
-                    CHARSXP => sexp == libR_sys::R_NaString,
+                    CHARSXP => sexp == extendr_ffi::R_NaString,
                     _ => false,
                 }
             }
diff --git a/extendr-api/src/robj/operators.rs b/extendr-api/src/robj/operators.rs
index 96abe2b03..f3b54725a 100644
--- a/extendr-api/src/robj/operators.rs
+++ b/extendr-api/src/robj/operators.rs
@@ -1,7 +1,7 @@
 use crate as extendr_api;
 use crate::*;
+use extendr_ffi::Rf_lcons;
 use std::ops::{Add, Div, Mul, Sub};
-
 ///////////////////////////////////////////////////////////////
 /// The following impls add operators to Robj.
 ///
diff --git a/extendr-api/src/robj/rinternals.rs b/extendr-api/src/robj/rinternals.rs
index c584c4bd6..65ed06b79 100644
--- a/extendr-api/src/robj/rinternals.rs
+++ b/extendr-api/src/robj/rinternals.rs
@@ -1,5 +1,16 @@
 use crate::*;
-
+use extendr_ffi::{
+    get_var, R_CFinalizer_t, R_ExternalPtrAddr, R_ExternalPtrProtected, R_ExternalPtrTag,
+    R_GetCurrentSrcref, R_GetSrcFilename, R_IsNamespaceEnv, R_IsPackageEnv, R_MakeExternalPtr,
+    R_MissingArg, R_NamespaceEnvSpec, R_PackageEnvName, R_RegisterCFinalizerEx, R_UnboundValue,
+    R_xlen_t, Rboolean, Rf_PairToVectorList, Rf_VectorToPairList, Rf_allocMatrix, Rf_allocVector,
+    Rf_asChar, Rf_asCharacterFactor, Rf_coerceVector, Rf_conformable, Rf_duplicate, Rf_findFun,
+    Rf_isArray, Rf_isComplex, Rf_isEnvironment, Rf_isExpression, Rf_isFactor, Rf_isFrame,
+    Rf_isFunction, Rf_isInteger, Rf_isLanguage, Rf_isList, Rf_isLogical, Rf_isMatrix, Rf_isNewList,
+    Rf_isNull, Rf_isNumber, Rf_isObject, Rf_isPrimitive, Rf_isReal, Rf_isS4, Rf_isString,
+    Rf_isSymbol, Rf_isTs, Rf_isUserBinop, Rf_isVector, Rf_isVectorAtomic, Rf_isVectorList,
+    Rf_isVectorizable, Rf_ncols, Rf_nrows, Rf_xlengthgets, ALTREP, TYPEOF,
+};
 ///////////////////////////////////////////////////////////////
 /// The following impls wrap specific Rinternals.h functions.
 ///
@@ -187,7 +198,7 @@ pub trait Rinternals: Types + Conversions {
         // }
         unsafe {
             let sexp = self.get();
-            if let Ok(var) = catch_r_error(|| Rf_findVar(key.get(), sexp)) {
+            if let Ok(var) = catch_r_error(|| get_var(key.get(), sexp)) {
                 if var != R_UnboundValue {
                     Ok(Robj::from_sexp(var))
                 } else {
@@ -357,13 +368,13 @@ pub trait Rinternals: Types + Conversions {
     #[cfg(feature = "non-api")]
     /// Return true if this is a valid string.
     fn is_valid_string(&self) -> bool {
-        unsafe { Rf_isValidString(self.get()).into() }
+        unsafe { extendr_ffi::Rf_isValidString(self.get()).into() }
     }
 
     #[cfg(feature = "non-api")]
     /// Return true if this is a valid string.
     fn is_valid_string_f(&self) -> bool {
-        unsafe { Rf_isValidStringF(self.get()).into() }
+        unsafe { extendr_ffi::Rf_isValidStringF(self.get()).into() }
     }
 
     /// Return true if this is a vector.
diff --git a/extendr-api/src/scalar/rcplx_default.rs b/extendr-api/src/scalar/rcplx_default.rs
index 7b1b6ef91..b3a2c9c4f 100644
--- a/extendr-api/src/scalar/rcplx_default.rs
+++ b/extendr-api/src/scalar/rcplx_default.rs
@@ -1,6 +1,7 @@
 use crate::scalar::Rfloat;
 use crate::scalar::Scalar;
 use crate::*;
+use extendr_ffi::{R_IsNA, R_NaReal, Rcomplex};
 
 #[allow(non_camel_case_types)]
 #[derive(Clone, Copy, Debug, Default, PartialEq)]
diff --git a/extendr-api/src/scalar/rcplx_full.rs b/extendr-api/src/scalar/rcplx_full.rs
index a7f1d4fec..9d308b8d5 100644
--- a/extendr-api/src/scalar/rcplx_full.rs
+++ b/extendr-api/src/scalar/rcplx_full.rs
@@ -1,6 +1,7 @@
 use crate::scalar::macros::*;
 use crate::scalar::{Rfloat, Scalar};
 use crate::*;
+use extendr_ffi::{R_IsNA, R_NaReal, Rcomplex};
 use std::convert::TryFrom;
 use std::ops::{Add, Div, Mul, Neg, Sub};
 use std::ops::{AddAssign, DivAssign, MulAssign, SubAssign};
diff --git a/extendr-api/src/thread_safety.rs b/extendr-api/src/thread_safety.rs
index fc3c515d5..7ddef7cdf 100644
--- a/extendr-api/src/thread_safety.rs
+++ b/extendr-api/src/thread_safety.rs
@@ -1,6 +1,8 @@
 //! Provide limited protection for multithreaded access to the R API.
-
 use crate::*;
+use extendr_ffi::{
+    R_MakeUnwindCont, R_UnwindProtect, Rboolean, Rf_error, Rf_protect, Rf_unprotect,
+};
 use std::cell::Cell;
 use std::sync::Mutex;
 
@@ -57,7 +59,7 @@ where
         Ok(res) => res,
         Err(_) => {
             let err_str = CString::new(err_str).unwrap();
-            unsafe { libR_sys::Rf_error(err_str.as_ptr()) }
+            unsafe { Rf_error(err_str.as_ptr()) }
         }
     }
 }
@@ -68,7 +70,7 @@ pub fn throw_r_error<S: AsRef<str>>(s: S) -> ! {
     let s = s.as_ref();
     unsafe {
         R_ERROR_BUF = Some(std::ffi::CString::new(s).unwrap());
-        libR_sys::Rf_error(R_ERROR_BUF.as_ref().unwrap().as_ptr());
+        Rf_error(R_ERROR_BUF.as_ref().unwrap().as_ptr());
     };
 }
 
diff --git a/extendr-api/src/wrapper/altrep.rs b/extendr-api/src/wrapper/altrep.rs
index 3274ea5e2..7890cfb96 100644
--- a/extendr-api/src/wrapper/altrep.rs
+++ b/extendr-api/src/wrapper/altrep.rs
@@ -1,6 +1,6 @@
-use prelude::{Rbool, Rcplx, Rfloat, Rint, Scalar};
-
 use super::*;
+use extendr_ffi::*;
+use prelude::{Rbool, Rcplx, Rfloat, Rint, Scalar};
 
 macro_rules! make_from_iterator {
     ($fn_name : ident, $make_class : ident, $impl : ident, $scalar_type : ident, $prim_type : ty) => {
@@ -58,6 +58,7 @@ pub trait AltrepImpl: Clone + std::fmt::Debug {
         obj_flags: i32,
         levels: i32,
     ) -> Robj {
+        use extendr_ffi::{SETLEVELS, SET_ATTRIB, SET_OBJECT};
         let res = Self::unserialize(class, state);
         if !res.is_null() {
             single_threaded(|| unsafe {
@@ -122,9 +123,9 @@ pub trait AltrepImpl: Clone + std::fmt::Debug {
             if data2 == R_NilValue || TYPEOF(data2) != TYPEOF(x) {
                 let data2 = manifest(x);
                 R_set_altrep_data2(x, data2);
-                DATAPTR(data2) as *mut u8
+                dataptr(data2) as *mut u8
             } else {
-                DATAPTR(data2) as *mut u8
+                dataptr(data2) as *mut u8
             }
         })
     }
@@ -137,7 +138,7 @@ pub trait AltrepImpl: Clone + std::fmt::Debug {
             if data2 == R_NilValue || TYPEOF(data2) != TYPEOF(x) {
                 std::ptr::null()
             } else {
-                DATAPTR(data2) as *const u8
+                dataptr(data2) as *const u8
             }
         }
     }
diff --git a/extendr-api/src/wrapper/complexes.rs b/extendr-api/src/wrapper/complexes.rs
index 448ddc726..5ce3994f6 100644
--- a/extendr-api/src/wrapper/complexes.rs
+++ b/extendr-api/src/wrapper/complexes.rs
@@ -1,5 +1,6 @@
 use super::scalar::{c64, Rcplx};
 use super::*;
+use extendr_ffi::{dataptr, R_xlen_t, Rcomplex, COMPLEX_GET_REGION, SEXPTYPE::CPLXSXP};
 use std::iter::FromIterator;
 
 /// An obscure `NA`-aware wrapper for R's complex vectors.
@@ -11,13 +12,12 @@ use std::iter::FromIterator;
 ///     let mut vec = (0..5).map(|i| c64::from(i as f64)).collect::<Complexes>();
 ///     assert_eq!(vec.len(), 5);
 /// }
-/// ```  
+/// ```
 #[derive(PartialEq, Clone)]
 pub struct Complexes {
     pub(crate) robj: Robj,
 }
 
-use libR_sys::SEXPTYPE::CPLXSXP;
 macros::gen_vector_wrapper_impl!(
     vector_type: Complexes,
     scalar_type: Rcplx,
@@ -62,7 +62,7 @@ impl Deref for Complexes {
     /// Treat Complexes as if it is a slice, like `Vec<Rcplx>`
     fn deref(&self) -> &Self::Target {
         unsafe {
-            let ptr = DATAPTR_RO(self.get()) as *const Rcplx;
+            let ptr = dataptr(self.get()) as *const Rcplx;
             std::slice::from_raw_parts(ptr, self.len())
         }
     }
@@ -72,7 +72,7 @@ impl DerefMut for Complexes {
     /// Treat Complexes as if it is a mutable slice, like `Vec<Rcplx>`
     fn deref_mut(&mut self) -> &mut Self::Target {
         unsafe {
-            let ptr = DATAPTR(self.get_mut()) as *mut Rcplx;
+            let ptr = dataptr(self.get_mut()) as *mut Rcplx;
             std::slice::from_raw_parts_mut(ptr, self.len())
         }
     }
diff --git a/extendr-api/src/wrapper/doubles.rs b/extendr-api/src/wrapper/doubles.rs
index b9cb106f1..8e1f80679 100644
--- a/extendr-api/src/wrapper/doubles.rs
+++ b/extendr-api/src/wrapper/doubles.rs
@@ -1,5 +1,8 @@
 use super::scalar::{Rfloat, Scalar};
 use super::*;
+use extendr_ffi::{
+    dataptr, R_xlen_t, REAL_GET_REGION, REAL_IS_SORTED, REAL_NO_NA, SET_REAL_ELT, SEXPTYPE::REALSXP,
+};
 use std::iter::FromIterator;
 
 /// An obscure `NA`-aware wrapper for R's double vectors.
@@ -14,13 +17,12 @@ use std::iter::FromIterator;
 ///     let sum = vec.iter().sum::<Rfloat>();
 ///     assert_eq!(sum, 60.0);
 /// }
-/// ```  
+/// ```
 #[derive(PartialEq, Clone)]
 pub struct Doubles {
     pub(crate) robj: Robj,
 }
 
-use libR_sys::SEXPTYPE::REALSXP;
 macros::gen_vector_wrapper_impl!(
     vector_type: Doubles,
     scalar_type: Rfloat,
@@ -74,7 +76,7 @@ impl Deref for Doubles {
     /// Treat Doubles as if it is a slice, like `Vec<Rfloat>`
     fn deref(&self) -> &Self::Target {
         unsafe {
-            let ptr = DATAPTR_RO(self.get()) as *const Rfloat;
+            let ptr = dataptr(self.get()) as *const Rfloat;
             std::slice::from_raw_parts(ptr, self.len())
         }
     }
@@ -84,7 +86,7 @@ impl DerefMut for Doubles {
     /// Treat Doubles as if it is a mutable slice, like `Vec<Rfloat>`
     fn deref_mut(&mut self) -> &mut Self::Target {
         unsafe {
-            let ptr = DATAPTR(self.get_mut()) as *mut Rfloat;
+            let ptr = dataptr(self.get_mut()) as *mut Rfloat;
             std::slice::from_raw_parts_mut(ptr, self.len())
         }
     }
diff --git a/extendr-api/src/wrapper/environment.rs b/extendr-api/src/wrapper/environment.rs
index 0b906fe86..206af3580 100644
--- a/extendr-api/src/wrapper/environment.rs
+++ b/extendr-api/src/wrapper/environment.rs
@@ -1,5 +1,7 @@
 use super::*;
-
+use extendr_ffi::{
+    get_parent_env, get_var_in_frame, R_BaseEnv, R_EmptyEnv, R_GlobalEnv, Rf_defineVar,
+};
 #[derive(PartialEq, Clone)]
 pub struct Environment {
     pub(crate) robj: Robj,
@@ -75,7 +77,7 @@ impl Environment {
     pub fn parent(&self) -> Option<Environment> {
         unsafe {
             let sexp = self.robj.get();
-            let robj = Robj::from_sexp(ENCLOS(sexp));
+            let robj = Robj::from_sexp(get_parent_env(sexp));
             robj.try_into().ok()
         }
     }
@@ -85,7 +87,7 @@ impl Environment {
     pub fn set_parent(&mut self, parent: Environment) -> &mut Self {
         single_threaded(|| unsafe {
             let sexp = self.robj.get_mut();
-            SET_ENCLOS(sexp, parent.robj.get());
+            extendr_ffi::SET_ENCLOS(sexp, parent.robj.get());
         });
         self
     }
@@ -95,7 +97,7 @@ impl Environment {
     pub fn envflags(&self) -> i32 {
         unsafe {
             let sexp = self.robj.get();
-            ENVFLAGS(sexp)
+            extendr_ffi::ENVFLAGS(sexp)
         }
     }
 
@@ -104,7 +106,7 @@ impl Environment {
     pub unsafe fn set_envflags(&mut self, flags: i32) -> &mut Self {
         single_threaded(|| unsafe {
             let sexp = self.robj.get_mut();
-            SET_ENVFLAGS(sexp, flags);
+            extendr_ffi::SET_ENVFLAGS(sexp, flags);
         });
         self
     }
@@ -113,8 +115,8 @@ impl Environment {
     /// Iterate over an environment.
     pub fn iter(&self) -> EnvIter {
         unsafe {
-            let hashtab = Robj::from_sexp(HASHTAB(self.get()));
-            let frame = Robj::from_sexp(FRAME(self.get()));
+            let hashtab = Robj::from_sexp(extendr_ffi::HASHTAB(self.get()));
+            let frame = Robj::from_sexp(extendr_ffi::FRAME(self.get()));
             if hashtab.is_null() && frame.is_pairlist() {
                 EnvIter {
                     hash_table: ListIter::new(),
@@ -175,7 +177,7 @@ impl Environment {
     pub fn local<K: Into<Robj>>(&self, key: K) -> Result<Robj> {
         let key = key.into();
         if key.is_symbol() {
-            unsafe { Ok(Robj::from_sexp(Rf_findVarInFrame(self.get(), key.get()))) }
+            unsafe { Ok(Robj::from_sexp(get_var_in_frame(self.get(), key.get()))) }
         } else {
             Err(Error::NotFound(key))
         }
diff --git a/extendr-api/src/wrapper/externalptr.rs b/extendr-api/src/wrapper/externalptr.rs
index 28865b68e..1ecf83929 100644
--- a/extendr-api/src/wrapper/externalptr.rs
+++ b/extendr-api/src/wrapper/externalptr.rs
@@ -18,8 +18,11 @@
 //! two instances of `ExternalPtr<T>` by value, `a.as_ref() == b.as_ref()`.
 //!
 use super::*;
+use extendr_ffi::{
+    R_ClearExternalPtr, R_ExternalPtrAddr, R_ExternalPtrProtected, R_ExternalPtrTag,
+    R_MakeExternalPtr, R_NilValue, R_SetExternalPtrTag,
+};
 use std::{any::Any, fmt::Debug};
-
 /// Wrapper for creating R objects containing any Rust object.
 ///
 /// ```
diff --git a/extendr-api/src/wrapper/function.rs b/extendr-api/src/wrapper/function.rs
index 12f574f6e..e09e09586 100644
--- a/extendr-api/src/wrapper/function.rs
+++ b/extendr-api/src/wrapper/function.rs
@@ -1,5 +1,5 @@
 use super::*;
-
+use extendr_ffi::{get_closure_body, get_closure_env, get_closure_formals, Rf_lcons};
 /// Wrapper for creating functions (CLOSSXP).
 /// ```
 /// use extendr_api::prelude::*;
@@ -43,11 +43,11 @@ impl Function {
     /// ```
     pub fn from_parts(formals: Pairlist, body: Language, env: Environment) -> Result<Self> {
         single_threaded(|| unsafe {
-            let sexp = Rf_allocSExp(SEXPTYPE::CLOSXP);
+            let sexp = extendr_ffi::Rf_allocSExp(SEXPTYPE::CLOSXP);
             let robj = Robj::from_sexp(sexp);
-            SET_FORMALS(sexp, formals.get());
-            SET_BODY(sexp, body.get());
-            SET_CLOENV(sexp, env.get());
+            extendr_ffi::SET_FORMALS(sexp, formals.get());
+            extendr_ffi::SET_BODY(sexp, body.get());
+            extendr_ffi::SET_CLOENV(sexp, env.get());
             Ok(Function { robj })
         })
     }
@@ -72,7 +72,11 @@ impl Function {
         unsafe {
             if self.rtype() == Rtype::Function {
                 let sexp = self.robj.get();
-                Some(Robj::from_sexp(FORMALS(sexp)).try_into().unwrap())
+                Some(
+                    Robj::from_sexp(get_closure_formals(sexp))
+                        .try_into()
+                        .unwrap(),
+                )
             } else {
                 None
             }
@@ -84,7 +88,7 @@ impl Function {
         unsafe {
             if self.rtype() == Rtype::Function {
                 let sexp = self.robj.get();
-                Some(Robj::from_sexp(BODY(sexp)))
+                Some(Robj::from_sexp(get_closure_body(sexp)))
             } else {
                 None
             }
@@ -97,7 +101,7 @@ impl Function {
             if self.rtype() == Rtype::Function {
                 let sexp = self.robj.get();
                 Some(
-                    Robj::from_sexp(CLOENV(sexp))
+                    Robj::from_sexp(get_closure_env(sexp))
                         .try_into()
                         .expect("Should be an environment"),
                 )
diff --git a/extendr-api/src/wrapper/integers.rs b/extendr-api/src/wrapper/integers.rs
index f9cce8352..841e1bd02 100644
--- a/extendr-api/src/wrapper/integers.rs
+++ b/extendr-api/src/wrapper/integers.rs
@@ -1,5 +1,8 @@
 use super::scalar::{Rint, Scalar};
 use super::*;
+use extendr_ffi::{
+    dataptr, R_xlen_t, INTEGER_GET_REGION, INTEGER_IS_SORTED, INTEGER_NO_NA, SET_INTEGER_ELT,
+};
 use std::iter::FromIterator;
 
 /// An obscure `NA`-aware wrapper for R's integer vectors.
@@ -14,13 +17,13 @@ use std::iter::FromIterator;
 ///     let sum = vec.iter().sum::<Rint>();
 ///     assert_eq!(sum, 60);
 /// }
-/// ```  
+/// ```
 #[derive(PartialEq, Clone)]
 pub struct Integers {
     pub(crate) robj: Robj,
 }
 
-use libR_sys::SEXPTYPE::INTSXP;
+use extendr_ffi::SEXPTYPE::INTSXP;
 macros::gen_vector_wrapper_impl!(
     vector_type: Integers, // Implements for
     scalar_type: Rint,     // Element type
@@ -74,7 +77,7 @@ impl Deref for Integers {
     /// Treat Integers as if it is a slice, like `Vec<Rint>`
     fn deref(&self) -> &Self::Target {
         unsafe {
-            let ptr = DATAPTR_RO(self.get()) as *const Rint;
+            let ptr = dataptr(self.get()) as *const Rint;
             std::slice::from_raw_parts(ptr, self.len())
         }
     }
@@ -84,7 +87,7 @@ impl DerefMut for Integers {
     /// Treat Integers as if it is a mutable slice, like `Vec<Rint>`
     fn deref_mut(&mut self) -> &mut Self::Target {
         unsafe {
-            let ptr = DATAPTR(self.get_mut()) as *mut Rint;
+            let ptr = dataptr(self.get_mut()) as *mut Rint;
             std::slice::from_raw_parts_mut(ptr, self.len())
         }
     }
@@ -178,7 +181,7 @@ mod tests {
         test! {
             let int_vec = vec![3,4,0,-2];
             let int_vec_robj: Robj = int_vec.clone().try_into().unwrap();
-            // unsafe { libR_sys::Rf_PrintValue(rint_vec_robj.get())}
+            // unsafe { extendr_ffi::Rf_PrintValue(rint_vec_robj.get())}
             assert_eq!(int_vec_robj.as_integer_slice().unwrap(), &int_vec);
         }
     }
diff --git a/extendr-api/src/wrapper/lang.rs b/extendr-api/src/wrapper/lang.rs
index f791b9b6f..54176abd0 100644
--- a/extendr-api/src/wrapper/lang.rs
+++ b/extendr-api/src/wrapper/lang.rs
@@ -1,5 +1,5 @@
 use super::*;
-
+use extendr_ffi::{R_NilValue, Rf_lcons, Rf_protect, Rf_unprotect};
 /// Wrapper for creating language objects.
 /// ```
 /// use extendr_api::prelude::*;
diff --git a/extendr-api/src/wrapper/list.rs b/extendr-api/src/wrapper/list.rs
index aba7293ee..88ab8a343 100644
--- a/extendr-api/src/wrapper/list.rs
+++ b/extendr-api/src/wrapper/list.rs
@@ -1,8 +1,8 @@
+use super::*;
 use crate::robj::Attributes;
+use extendr_ffi::{dataptr, R_xlen_t, SET_VECTOR_ELT, VECTOR_ELT};
 use std::iter::FromIterator;
 
-use super::*;
-
 #[derive(PartialEq, Clone)]
 pub struct List {
     pub(crate) robj: Robj,
@@ -145,7 +145,7 @@ impl List {
     /// Get the list a slice of `Robj`s.
     pub fn as_slice(&self) -> &[Robj] {
         unsafe {
-            let data = DATAPTR(self.robj.get()) as *const Robj;
+            let data = dataptr(self.robj.get()) as *const Robj;
             let len = self.robj.len();
             std::slice::from_raw_parts(data, len)
         }
@@ -224,7 +224,7 @@ impl IntoIterator for List {
 ///       }
 ///     }
 ///     assert_eq!(total, 3);
-///    
+///
 ///     for name in my_list.names().unwrap() {
 ///        assert!(name == "a" || name == "b")
 ///     }
diff --git a/extendr-api/src/wrapper/logicals.rs b/extendr-api/src/wrapper/logicals.rs
index 8cf361716..a9e3914c1 100644
--- a/extendr-api/src/wrapper/logicals.rs
+++ b/extendr-api/src/wrapper/logicals.rs
@@ -1,5 +1,6 @@
 use super::scalar::{Rbool, Scalar};
 use super::*;
+use extendr_ffi::{dataptr, R_xlen_t, LOGICAL_GET_REGION, SET_INTEGER_ELT, SEXPTYPE};
 use std::iter::FromIterator;
 
 /// An obscure `NA`-aware wrapper for R's logical vectors.
@@ -15,7 +16,7 @@ use std::iter::FromIterator;
 ///     // Logicals behaves like &[Rbool]
 ///     assert_eq!(vec[1], false);
 /// }
-/// ```  
+/// ```
 #[derive(PartialEq, Clone)]
 pub struct Logicals {
     pub(crate) robj: Robj,
@@ -65,7 +66,7 @@ impl Deref for Logicals {
     /// Treat Logicals as if it is a slice, like `Vec<Rint>`
     fn deref(&self) -> &Self::Target {
         unsafe {
-            let ptr = DATAPTR_RO(self.get()) as *const Rbool;
+            let ptr = dataptr(self.get()) as *const Rbool;
             std::slice::from_raw_parts(ptr, self.len())
         }
     }
@@ -75,7 +76,7 @@ impl DerefMut for Logicals {
     /// Treat Logicals as if it is a mutable slice, like `Vec<Rint>`
     fn deref_mut(&mut self) -> &mut Self::Target {
         unsafe {
-            let ptr = DATAPTR(self.get_mut()) as *mut Rbool;
+            let ptr = dataptr(self.get_mut()) as *mut Rbool;
             std::slice::from_raw_parts_mut(ptr, self.len())
         }
     }
diff --git a/extendr-api/src/wrapper/macros.rs b/extendr-api/src/wrapper/macros.rs
index 4751ac7fe..d60238b01 100644
--- a/extendr-api/src/wrapper/macros.rs
+++ b/extendr-api/src/wrapper/macros.rs
@@ -103,12 +103,13 @@ macro_rules! gen_vector_wrapper_impl {
                 #[doc = "}"]
                 #[doc = "```"]
                 pub fn elt(&self, index: usize) -> $scalar_type {
+                    use extendr_ffi::{R_xlen_t};
                     // Defensive check for oob
                     // This check will not be needed in later releases of R
                     if(index >= self.len()) {
                         <$scalar_type>::na()
                     } else {
-                        unsafe { [<$r_prefix _ELT>](self.get(), index as R_xlen_t).into() }
+                        unsafe { extendr_ffi::[<$r_prefix _ELT>](self.get(), index as R_xlen_t).into() }
                     }
                 }
             }
diff --git a/extendr-api/src/wrapper/matrix.rs b/extendr-api/src/wrapper/matrix.rs
index 909a6a403..72b29d48a 100644
--- a/extendr-api/src/wrapper/matrix.rs
+++ b/extendr-api/src/wrapper/matrix.rs
@@ -2,6 +2,10 @@
 use self::robj::{AsTypedSlice, Robj};
 use super::*;
 use crate::scalar::Scalar;
+use extendr_ffi::{
+    Rf_GetArrayDimnames, Rf_GetColNames, Rf_GetRowNames, Rf_dimgets, Rf_dimnamesgets, Rf_namesgets,
+    TYPEOF,
+};
 use std::ops::{Index, IndexMut};
 
 /// Wrapper for creating and using matrices and arrays.
@@ -125,7 +129,9 @@ impl<T> RMatrix<T> {
                     let colnames = Robj::from_sexp(maybe_colnames);
                     Some(std::mem::transmute(colnames))
                 }
-                _ => unreachable!("This should not have occurred. Please report an error at https://github.com/extendr/extendr/issues"),
+                _ => unreachable!(
+                    "This should not have occurred. Please report an error at https://github.com/extendr/extendr/issues"
+                ),
             }
         }
     }
@@ -138,7 +144,9 @@ impl<T> RMatrix<T> {
                     let rownames = Robj::from_sexp(maybe_rownames);
                     Some(std::mem::transmute(rownames))
                 }
-                _ => unreachable!("This should not have occurred. Please report an error at https://github.com/extendr/extendr/issues"),
+                _ => unreachable!(
+                    "This should not have occurred. Please report an error at https://github.com/extendr/extendr/issues"
+                ),
             }
         }
     }
@@ -628,7 +636,7 @@ where
     ///    matrix[[1, 0, 0]] = 2.;
     ///    matrix[[2, 0, 0]] = 3.;
     ///    matrix[[0, 1, 0]] = 4.;
-    ///    assert_eq!(matrix.as_real_slice().unwrap(), 
+    ///    assert_eq!(matrix.as_real_slice().unwrap(),
     ///        &[1., 2., 3., 4., 0., 0., 0., 0., 0., 0., 0., 0.]);
     /// }
     /// ```
@@ -643,7 +651,6 @@ where
     }
 }
 
-
 impl<T, D> Deref for RArray<T, D> {
     type Target = Robj;
 
@@ -672,6 +679,7 @@ mod tests {
     use super::*;
     use crate as extendr_api;
     use extendr_engine::with_r;
+    use extendr_ffi::Rf_PrintValue;
     use prelude::{Rcplx, Rfloat, Rint};
 
     #[test]
diff --git a/extendr-api/src/wrapper/mod.rs b/extendr-api/src/wrapper/mod.rs
index c7d66415a..2bbd306cc 100644
--- a/extendr-api/src/wrapper/mod.rs
+++ b/extendr-api/src/wrapper/mod.rs
@@ -3,7 +3,6 @@
 
 use crate::robj::{GetSexp, Rinternals};
 use crate::*;
-use libR_sys::*;
 
 pub mod altrep;
 pub mod complexes;
diff --git a/extendr-api/src/wrapper/pairlist.rs b/extendr-api/src/wrapper/pairlist.rs
index 5cff86424..a4ec3601b 100644
--- a/extendr-api/src/wrapper/pairlist.rs
+++ b/extendr-api/src/wrapper/pairlist.rs
@@ -1,6 +1,9 @@
 //! A pairlist is a linked list of values with optional symbol tags.
 
 use super::*;
+use extendr_ffi::{
+    R_NilValue, Rf_cons, Rf_protect, Rf_unprotect, CAR, CDR, PRINTNAME, SET_TAG, TAG, TYPEOF,
+};
 
 #[derive(PartialEq, Clone)]
 pub struct Pairlist {
diff --git a/extendr-api/src/wrapper/primitive.rs b/extendr-api/src/wrapper/primitive.rs
index 56a70b38d..2ded7afc7 100644
--- a/extendr-api/src/wrapper/primitive.rs
+++ b/extendr-api/src/wrapper/primitive.rs
@@ -25,7 +25,7 @@ impl Primitive {
         single_threaded(|| unsafe {
             // Primitives have a special "SYMVALUE" entry in their symbol.
             let sym = Symbol::from_string(val);
-            let symvalue = Robj::from_sexp(SYMVALUE(sym.get()));
+            let symvalue = Robj::from_sexp(extendr_ffi::SYMVALUE(sym.get()));
             if symvalue.is_primitive() {
                 Ok(Primitive { robj: symvalue })
             } else {
diff --git a/extendr-api/src/wrapper/promise.rs b/extendr-api/src/wrapper/promise.rs
index 8b14f9062..b527a5371 100644
--- a/extendr-api/src/wrapper/promise.rs
+++ b/extendr-api/src/wrapper/promise.rs
@@ -20,11 +20,11 @@ impl Promise {
     #[cfg(feature = "non-api")]
     pub fn from_parts(code: Robj, environment: Environment) -> Result<Self> {
         single_threaded(|| unsafe {
-            let sexp = Rf_allocSExp(SEXPTYPE::PROMSXP);
+            let sexp = extendr_ffi::Rf_allocSExp(SEXPTYPE::PROMSXP);
             let robj = Robj::from_sexp(sexp);
-            SET_PRCODE(sexp, code.get());
-            SET_PRENV(sexp, environment.robj.get());
-            SET_PRVALUE(sexp, R_UnboundValue);
+            extendr_ffi::SET_PRCODE(sexp, code.get());
+            extendr_ffi::SET_PRENV(sexp, environment.robj.get());
+            extendr_ffi::SET_PRVALUE(sexp, extendr_ffi::R_UnboundValue);
             Ok(Promise { robj })
         })
     }
@@ -34,7 +34,7 @@ impl Promise {
     pub fn code(&self) -> Robj {
         unsafe {
             let sexp = self.robj.get();
-            Robj::from_sexp(PRCODE(sexp))
+            Robj::from_sexp(extendr_ffi::PRCODE(sexp))
         }
     }
 
@@ -43,7 +43,9 @@ impl Promise {
     pub fn environment(&self) -> Environment {
         unsafe {
             let sexp = self.robj.get();
-            Robj::from_sexp(PRENV(sexp)).try_into().unwrap()
+            Robj::from_sexp(extendr_ffi::PRENV(sexp))
+                .try_into()
+                .unwrap()
         }
     }
 
@@ -52,7 +54,7 @@ impl Promise {
     pub fn value(&self) -> Robj {
         unsafe {
             let sexp = self.robj.get();
-            Robj::from_sexp(PRVALUE(sexp))
+            Robj::from_sexp(extendr_ffi::PRVALUE(sexp))
         }
     }
 
@@ -61,7 +63,7 @@ impl Promise {
     pub fn seen(&self) -> i32 {
         unsafe {
             let sexp = self.robj.get();
-            PRSEEN(sexp)
+            extendr_ffi::PRSEEN(sexp)
         }
     }
 
diff --git a/extendr-api/src/wrapper/rstr.rs b/extendr-api/src/wrapper/rstr.rs
index ded920a7b..763f47e17 100644
--- a/extendr-api/src/wrapper/rstr.rs
+++ b/extendr-api/src/wrapper/rstr.rs
@@ -1,5 +1,5 @@
 use super::*;
-
+use extendr_ffi::{R_BlankString, R_NaString, R_NilValue, Rf_xlength, R_CHAR, SEXPTYPE, TYPEOF};
 /// Wrapper for creating CHARSXP objects.
 /// These are used only as the contents of a character
 /// vector.
diff --git a/extendr-api/src/wrapper/s4.rs b/extendr-api/src/wrapper/s4.rs
index aaa1413ca..d63c0c7f4 100644
--- a/extendr-api/src/wrapper/s4.rs
+++ b/extendr-api/src/wrapper/s4.rs
@@ -28,6 +28,7 @@
 //!
 
 use super::*;
+use extendr_ffi::{R_do_slot, R_do_slot_assign, R_has_slot};
 
 #[derive(PartialEq, Clone)]
 pub struct S4 {
diff --git a/extendr-api/src/wrapper/strings.rs b/extendr-api/src/wrapper/strings.rs
index f6a1ff954..acd14e228 100644
--- a/extendr-api/src/wrapper/strings.rs
+++ b/extendr-api/src/wrapper/strings.rs
@@ -1,8 +1,10 @@
+use super::*;
+use extendr_ffi::{
+    R_xlen_t, SET_STRING_ELT, STRING_ELT, STRING_IS_SORTED, STRING_NO_NA, STRING_PTR_RO,
+};
 use std::convert::From;
 use std::iter::FromIterator;
 
-use super::*;
-
 #[derive(PartialEq, Clone)]
 pub struct Strings {
     pub(crate) robj: Robj,
diff --git a/extendr-api/src/wrapper/symbol.rs b/extendr-api/src/wrapper/symbol.rs
index 4d5968845..e26d4cb36 100644
--- a/extendr-api/src/wrapper/symbol.rs
+++ b/extendr-api/src/wrapper/symbol.rs
@@ -1,5 +1,14 @@
 use super::*;
-
+// NOTE all of these symbol constants should be in their own module
+use extendr_ffi::{
+    R_BaseSymbol, R_BraceSymbol, R_Bracket2Symbol, R_BracketSymbol, R_ClassSymbol, R_DeviceSymbol,
+    R_DimNamesSymbol, R_DimSymbol, R_DollarSymbol, R_DotsSymbol, R_DoubleColonSymbol,
+    R_LastvalueSymbol, R_LevelsSymbol, R_MissingArg, R_ModeSymbol, R_NaRmSymbol, R_NameSymbol,
+    R_NamesSymbol, R_NamespaceEnvSymbol, R_PackageSymbol, R_PreviousSymbol, R_QuoteSymbol,
+    R_RowNamesSymbol, R_SeedsSymbol, R_SortListSymbol, R_SourceSymbol, R_SpecSymbol,
+    R_TripleColonSymbol, R_TspSymbol, R_UnboundValue, R_dot_Method, R_dot_defined,
+    R_dot_packageName, R_dot_target, PRINTNAME, TYPEOF,
+};
 /// Wrapper for creating symbol objects.
 ///
 /// ```
diff --git a/extendr-api/src/wrapper/wrapper_macros.rs b/extendr-api/src/wrapper/wrapper_macros.rs
index 17a5aa736..f598164c8 100644
--- a/extendr-api/src/wrapper/wrapper_macros.rs
+++ b/extendr-api/src/wrapper/wrapper_macros.rs
@@ -1,9 +1,10 @@
 use super::*;
 use crate as extendr_api;
+use extendr_ffi::{R_xlen_t, SET_VECTOR_ELT};
 
 pub(crate) fn make_symbol(name: &str) -> SEXP {
     let name = CString::new(name).unwrap();
-    unsafe { libR_sys::Rf_install(name.as_ptr()) }
+    unsafe { extendr_ffi::Rf_install(name.as_ptr()) }
 }
 
 pub(crate) fn make_vector<T>(sexptype: SEXPTYPE, values: T) -> Robj
diff --git a/extendr-engine/Cargo.toml b/extendr-engine/Cargo.toml
index 5ed5646c6..12dfd60e1 100644
--- a/extendr-engine/Cargo.toml
+++ b/extendr-engine/Cargo.toml
@@ -8,7 +8,7 @@ license.workspace = true
 repository.workspace = true
 
 [dependencies]
-libR-sys = { workspace = true }
+extendr-ffi = { workspace = true }
 ctor = "0.2.4"
 
 [features]
diff --git a/extendr-engine/build.rs b/extendr-engine/build.rs
index 54163ed1e..183700640 100644
--- a/extendr-engine/build.rs
+++ b/extendr-engine/build.rs
@@ -1,8 +1,10 @@
-use std::env;
-
 fn main() {
-    println!(
-        "cargo:rustc-env=R_HOME={}",
-        env::var("DEP_R_R_HOME").unwrap()
-    );
+    // use R_HOME if provided
+    let r_home = match std::env::var("R_HOME") {
+        Ok(v) => v,
+        // otherwise use the DEP_R_R_HOME from extendr-ffi build.rs
+        Err(_) => std::env::var("DEP_R_R_HOME").expect("failed to get DEP_R_R_HOME"),
+    };
+    // set the environment variable accordingly
+    println!("cargo:rustc-env=R_HOME={}", r_home);
 }
diff --git a/extendr-engine/src/lib.rs b/extendr-engine/src/lib.rs
index 9315ca4fe..871ecc4d3 100644
--- a/extendr-engine/src/lib.rs
+++ b/extendr-engine/src/lib.rs
@@ -1,8 +1,8 @@
 //! Embeds a a single R process
 //!
 //! Using R's C-API requires the embedding of the R runtime.
-//! Thus, when using bindings in `libR-sys`, it is necessary that
-//! either the a R process is the caller, or that the process instantiates
+//! Thus, when using bindings provided by `extendr-ffi`, it is necessary that
+//! either an R process is the caller, or that the process instantiates
 //! an accompanying R process. Otherwise, a run-time error occurs e.g.
 //! `(signal: 11, SIGSEGV: invalid memory reference)` or
 //!
@@ -20,18 +20,18 @@
 //! #[test]
 //! fn testing_r_code() {
 //!     with_r(|| {
-//!     
+//!
 //!     });
 //! }
 //! ```
 //!
 //! Similarly with `test!` that is available in `extendr_api`, one may
 //!
-//! ```no_run   
+//! ```no_run
 //! #[test]
 //! fn testing_r_code() {
 //!     test! {
-//!     
+//!
 //!     };
 //! }
 //! ```
@@ -61,7 +61,9 @@
 //
 //
 
-use libR_sys::*;
+use extendr_ffi::{
+    setup_Rmainloop, R_CStackLimit, R_CleanTempDir, R_RunExitFinalizers, Rf_initialize_R,
+};
 use std::os::raw;
 use std::sync::Once;
 
@@ -80,7 +82,7 @@ pub fn start_r() {
     START_R.call_once(|| {
         unsafe {
             if std::env::var("R_HOME").is_err() {
-                // env! gets the build-time R_HOME stored by libR-sys
+                // env! gets the build-time R_HOME stored by extendr-ffi
                 std::env::set_var("R_HOME", env!("R_HOME"));
             }
 
diff --git a/extendr-ffi/.gitignore b/extendr-ffi/.gitignore
new file mode 100644
index 000000000..76b565eff
--- /dev/null
+++ b/extendr-ffi/.gitignore
@@ -0,0 +1,2 @@
+.env
+/target
diff --git a/extendr-ffi/Cargo.toml b/extendr-ffi/Cargo.toml
new file mode 100644
index 000000000..493a2f2e7
--- /dev/null
+++ b/extendr-ffi/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "extendr-ffi"
+version = "0.8.0"
+edition = "2021"
+links = "R"
+
+[features]
+default = []
+non-api = []
+
+[build-dependencies]
+build-print = "0.1.1"
diff --git a/extendr-ffi/README.md b/extendr-ffi/README.md
new file mode 100644
index 000000000..c15c425c5
--- /dev/null
+++ b/extendr-ffi/README.md
@@ -0,0 +1,7 @@
+## extendr-ffi
+
+`extendr-ffi` is a hand curated subset of the bindings provided by libR-sys. Supporting R version 4.2 and onwards. extendr-ffi provides backports to ensure that `extendr-api` can be used across multiple versions of R.
+
+## Motivation
+
+extendr has historically relied on [libR-sys](https://github.com/extendr/libR-sys) to interface with R's C API. As R has moved towards standardizing and stabilizing the C API, relying on libR-sys' generated bindings has presented challenges. Among them are that the bindings are cumbersome to generate, platform specific, and does not provide backports to address the maturing R C API.
diff --git a/extendr-ffi/build.rs b/extendr-ffi/build.rs
new file mode 100644
index 000000000..8185fb9d8
--- /dev/null
+++ b/extendr-ffi/build.rs
@@ -0,0 +1,220 @@
+use build_print::{error, info, warn};
+use std::{
+    error::Error,
+    fs::read_to_string,
+    num::ParseIntError,
+    path::{Path, PathBuf},
+    process::Command,
+    str::FromStr,
+};
+
+// The environmental variables that are usually set by R. These might be needed
+// to set manually if we compile extendr-ffi outside of an R session.
+// c.f., https://stat.ethz.ch/R-manual/R-devel/library/base/html/EnvVar.html
+const ENVVAR_R_HOME: &str = "R_HOME";
+const ENVVAR_R_INCLUDE_DIR: &str = "R_INCLUDE_DIR";
+
+struct Version {
+    major: u8,
+    minor: u8,
+    patch: u8,
+}
+
+impl FromStr for Version {
+    type Err = ParseIntError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let parts = s.splitn(3, ".").collect::<Vec<_>>();
+        Ok(Self {
+            major: parts[0].parse::<u8>()?,
+            minor: parts[1].parse::<u8>()?,
+            patch: parts[2].parse::<u8>()?,
+        })
+    }
+}
+
+fn read_r_ver(path: &Path) -> Result<Version, Box<dyn Error>> {
+    let ver_h = path.join("Rversion.h");
+    info!("Attempting to read version from {}", ver_h.display());
+    let content = read_to_string(ver_h).map_err(|e| {
+        error!("Unable to read Rversion.h: {}", e.to_string());
+        e
+    })?;
+
+    let major = content
+        .lines()
+        .find(|line| line.starts_with("#define R_MAJOR"))
+        .and_then(|line| line.split_whitespace().nth(2))
+        .and_then(|v| v.trim_matches('"').to_string().into())
+        .unwrap_or_default();
+
+    let minor = content
+        .lines()
+        .find(|line| line.starts_with("#define R_MINOR"))
+        .and_then(|line| line.split_whitespace().nth(2))
+        .and_then(|v| v.trim_matches('"').to_string().into())
+        .unwrap_or_default();
+
+    let version = Version::from_str(&format!("{major}.{minor}")).map_err(|e| {
+        error!("Unable to parse R version: {}", e.to_string());
+        e
+    })?;
+
+    info!(
+        "R version found is: {}.{}.{}",
+        version.major, version.minor, version.patch
+    );
+
+    Ok(version)
+}
+
+impl Version {
+    fn get_r_include() -> Result<String, Box<dyn Error>> {
+        let r_cmd = match cfg!(windows) {
+            true => "R.exe",
+            false => "R",
+        };
+
+        let mut cmd = Command::new(r_cmd);
+        // print the include dir from R
+        cmd.args([
+            "--vanilla",
+            "-s",
+            "-e",
+            "cat(normalizePath(R.home('include')))",
+        ]);
+
+        info!("Running R command: {:?}", cmd);
+
+        let out = cmd.output()?.stdout;
+
+        let res = String::from_utf8(out).map_err(|e| {
+            error!("Unable to parse R output");
+            e
+        })?;
+
+        info!("R command output: {:?}", res);
+        Ok(res)
+    }
+
+    fn try_new() -> Result<Self, Box<dyn Error>> {
+        let include_dir = match std::env::var(ENVVAR_R_INCLUDE_DIR) {
+            Ok(v) => {
+                let v = if v.is_empty() {
+                    warn!("R_INCLUDE_DIR environment variable is empty.");
+                    Self::get_r_include()?
+                } else {
+                    v
+                };
+                info!("R_INCLUDE_DIR: {v}");
+                v
+            }
+            Err(_) => {
+                warn!("R_INCLUDE_DIR not found. Likely being built outside of R.");
+                Self::get_r_include()?
+            }
+        };
+
+        let r_ver = read_r_ver(&Path::new(&include_dir))?;
+
+        Ok(r_ver)
+    }
+}
+
+struct InstallationPaths {
+    r_home: PathBuf,
+    version: Version,
+}
+
+impl InstallationPaths {
+    fn get_r_home() -> Result<String, Box<dyn Error>> {
+        let r_cmd = match cfg!(windows) {
+            true => "R.exe",
+            false => "R",
+        };
+
+        let mut cmd = Command::new(r_cmd);
+        cmd.args(["-s", "-e", "cat(normalizePath(R.home()))"]);
+        info!("Running R command: {cmd:?}");
+        let res = String::from_utf8(cmd.output()?.stdout)?;
+        info!("R_HOME found at {res}");
+        Ok(res)
+    }
+
+    fn try_new() -> Result<Self, Box<dyn Error>> {
+        // If R_HOME is unset then we try and call R directly.
+        let r_home = match std::env::var(ENVVAR_R_HOME) {
+            Err(_) => {
+                warn!("R_HOME not found. Trying to fetch from R directly.");
+                Self::get_r_home()?
+            }
+            Ok(v) => {
+                let v = if v.is_empty() {
+                    warn!("R_HOME is empty. Trying to fetch from R directly");
+                    Self::get_r_home()?
+                } else {
+                    v
+                };
+                info!("R_HOME: {v}");
+                v
+            }
+        };
+
+        Ok(InstallationPaths {
+            r_home: Path::new(&r_home).to_path_buf(),
+            version: Version::try_new()?,
+        })
+    }
+}
+
+fn main() -> Result<(), Box<dyn Error>> {
+    println!("cargo:rustc-check-cfg=cfg(r_4_4)");
+    println!("cargo:rustc-check-cfg=cfg(r_4_5)");
+
+    // Fetch R_HOME and R version
+    let r_paths = InstallationPaths::try_new()?;
+
+    // Used by extendr-engine becomes DEP_R_R_HOME for clients
+    println!("cargo:r_home={}", r_paths.r_home.display());
+    println!("cargo:rustc-env=R_HOME={}", r_paths.r_home.display());
+    // used by extendr-api
+    println!("cargo:r_version_major={}", r_paths.version.major);
+    println!("cargo:r_version_minor={}", r_paths.version.minor);
+    println!("cargo:r_version_patch={}", r_paths.version.patch);
+
+    let pkg_target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap();
+
+    let libdir = match (cfg!(windows), pkg_target_arch.as_str()) {
+        // For Windows
+        (true, "x86_64") => Path::new(&r_paths.r_home).join("bin").join("x64"),
+        (true, "x86") => Path::new(&r_paths.r_home).join("bin").join("i386"),
+        (true, _) => panic!("Unknown architecture"),
+        // For Unix-alike
+        (false, _) => Path::new(&r_paths.r_home).join("lib"),
+    };
+
+    info!("R library paths determined to be at: {libdir:?}");
+
+    if let Ok(r_library) = libdir.canonicalize() {
+        println!("cargo:rustc-link-search={}", r_library.display());
+    }
+
+    println!("cargo:rustc-link-lib=dylib=R");
+
+    // Set R version specfic config flags
+    // use r_4_4 config
+    if (r_paths.version.major, r_paths.version.minor) >= (4, 4) {
+        println!("cargo:rustc-cfg=r_4_4")
+    }
+
+    // use r_4_5 config
+    if (r_paths.version.major, r_paths.version.minor) >= (4, 5) {
+        println!("cargo:rustc-cfg=r_4_5")
+    }
+
+    // Only re-run if the include directory changes
+    println!("cargo:rerun-if-env-changed=R_INCLUDE_DIR");
+
+    build_print::custom_println!(" Success:", green, "extendr-ffi has been built!");
+    Ok(())
+}
diff --git a/extendr-ffi/src/altrep.rs b/extendr-ffi/src/altrep.rs
new file mode 100644
index 000000000..aee194707
--- /dev/null
+++ b/extendr-ffi/src/altrep.rs
@@ -0,0 +1,399 @@
+//! ALTREP support
+//!
+//! The core functions to support ALTREP in R.
+use super::*;
+
+#[allow(non_camel_case_types)]
+/// Must return an R value of SEXPTYPE type containing the result of conversion of x into that type, ignoring the attributes.
+/// May return a null pointer, making R perform the conversion as usual (see coerceVector). The default method returns a null pointer.
+pub type R_altrep_Coerce_method_t =
+    ::std::option::Option<unsafe extern "C" fn(arg1: SEXP, arg2: SEXPTYPE) -> SEXP>;
+
+#[repr(C)]
+#[derive(Debug, Copy, Clone)]
+pub struct R_altrep_class_t {
+    pub ptr: SEXP,
+}
+
+/// Like the Unserialize method above, but must also take care of setting the attributes, the levels field and the object bit on the return value. This is likely impossible to implement within the confines of the R API.
+/// The default method calls the Unserialize method, sets the attributes, the object bit, and the levels field, and returns the result.
+pub type R_altrep_UnserializeEX_method_t = ::std::option::Option<
+    unsafe extern "C" fn(
+        arg1: SEXP,
+        arg2: SEXP,
+        arg3: SEXP,
+        arg4: ::std::os::raw::c_int,
+        arg5: ::std::os::raw::c_int,
+    ) -> SEXP,
+>;
+
+/// Must construct and return an ALTREP object of the given class given the data previously prepared by the Serialized_state method above.
+/// The default method signals an R-level error.
+pub type R_altrep_Unserialize_method_t =
+    ::std::option::Option<unsafe extern "C" fn(arg1: SEXP, arg2: SEXP) -> SEXP>;
+
+/// Must prepare and return a representation of x that can be given to the Unserialize methods below in order to recreate it.
+///
+/// May return a null pointer, making R materialise the object when saving it. The default method returns a null pointer.
+pub type R_altrep_Serialized_state_method_t =
+    ::std::option::Option<unsafe extern "C" fn(arg1: SEXP) -> SEXP>;
+
+///  Like the Duplicate method above, but must duplicate the attributes in the same manner.
+///  May return a null pointer, in which case R will produce a materialised copy of the object by itself. The default method calls the Duplicate method, duplicates the attributes of x and installs them onto the return value.
+pub type R_altrep_DuplicateEX_method_t =
+    ::std::option::Option<unsafe extern "C" fn(arg1: SEXP, arg2: Rboolean) -> SEXP>;
+
+/// Must return a copy (see duplicate) of x that can be altered independently of x, ignoring the attributes. R will duplicate and install the attributes by itself.
+///
+/// May return a null pointer, in which case R will produce a materialised copy of the object by itself. The default method returns a null pointer.
+pub type R_altrep_Duplicate_method_t =
+    ::std::option::Option<unsafe extern "C" fn(arg1: SEXP, arg2: Rboolean) -> SEXP>;
+
+/// This method will be called from .Internal(inspect(...)) after printing the standard SEXP header contents; it can then print (see Rprintf) additional information specific to this ALTREP class. When starting a new line, this method must respect the output indentation pre. Printing the vector elements, if done, should be subject to the recommended vector printing length pvec. The method should call inspect_subtree for child elements, subject to the limits in deep.
+/// If the return value is TRUE, R will consider the value inspected and won’t access its contents; otherwise R will then inspect the contents of x as if it was an ordinary value. Either way, R will then take care of inspecting the attributes of x.
+/// The default method returns FALSE.
+/// Versions: Appeared in 3.5.0.
+pub type R_altrep_Inspect_method_t = ::std::option::Option<
+    unsafe extern "C" fn(
+        arg1: SEXP,
+        arg2: ::std::os::raw::c_int,
+        arg3: ::std::os::raw::c_int,
+        arg4: ::std::os::raw::c_int,
+        arg5: ::std::option::Option<
+            unsafe extern "C" fn(
+                arg1: SEXP,
+                arg2: ::std::os::raw::c_int,
+                arg3: ::std::os::raw::c_int,
+                arg4: ::std::os::raw::c_int,
+            ),
+        >,
+    ) -> Rboolean,
+>;
+
+/// Must return the length of the compact-representation vector, or signal an error. The default method signals an error.
+pub type R_altrep_Length_method_t =
+    ::std::option::Option<unsafe extern "C" fn(arg1: SEXP) -> R_xlen_t>;
+
+/// Must return a pointer to the start of the buffer of type corresponding to the SEXPTYPE of x and containing XLENGTH(x) elements (see DATAPTR_RO) corresponding to the contents of x. May signal an R error instead.
+///  The default method always signals an error.
+pub type R_altvec_Dataptr_method_t = ::std::option::Option<
+    unsafe extern "C" fn(arg1: SEXP, arg2: Rboolean) -> *mut ::std::os::raw::c_void,
+>;
+
+/// Like the Dataptr method above, but the method may return a null pointer to indicate a preference for *_Elt (see REAL_ELT) and *_Get_region (see REAL_GET_REGION) methods instead of full buffer access. The return value will not be used to modify x.
+/// The default method always returns a null pointer.
+pub type R_altvec_Dataptr_or_null_method_t =
+    ::std::option::Option<unsafe extern "C" fn(arg1: SEXP) -> *const ::std::os::raw::c_void>;
+/// Must allocate and return a new vector containing the result of subsetting, `x[indx]`, following the usual semantics of the [ operator for numeric subscripts. For values outside the range of x, missing values must be returned (R_NilValue for VECSXP lists, 0 for RAWSXP vectors). May also return a null pointer (see above).
+///
+/// The default method always returns a null pointer.
+/// Versions: Appeared in 3.5.0.
+pub type R_altvec_Extract_subset_method_t =
+    ::std::option::Option<unsafe extern "C" fn(arg1: SEXP, arg2: SEXP, arg3: SEXP) -> SEXP>;
+/// Must return `x[i]` for the 0-based index i. The default method returns `INTEGER(x)[i]`.
+pub type R_altinteger_Elt_method_t = ::std::option::Option<
+    unsafe extern "C" fn(arg1: SEXP, arg2: R_xlen_t) -> ::std::os::raw::c_int,
+>;
+/// Must copy up to n elements, starting at 0-based index i, into `buf[n]`, and return the number of elements copied. The default method defers to INTEGER_ELT and hence the Elt method above.
+pub type R_altinteger_Get_region_method_t = ::std::option::Option<
+    unsafe extern "C" fn(
+        arg1: SEXP,
+        arg2: R_xlen_t,
+        arg3: R_xlen_t,
+        arg4: *mut ::std::os::raw::c_int,
+    ) -> R_xlen_t,
+>;
+/// Must return one of the sortedness constants: SORTED_DECR, UNKNOWN_SORTEDNESS, SORTED_INCR. See INTEGER_IS_SORTED. The default method always returns UNKNOWN_SORTEDNESS.
+pub type R_altinteger_Is_sorted_method_t =
+    ::std::option::Option<unsafe extern "C" fn(arg1: SEXP) -> ::std::os::raw::c_int>;
+/// Must return 1 if x is known not to store any missing values (see INTEGER_NO_NA), otherwise 0 (including if it’s unknown). The default method always returns 0.
+pub type R_altinteger_No_NA_method_t =
+    ::std::option::Option<unsafe extern "C" fn(arg1: SEXP) -> ::std::os::raw::c_int>;
+/// Must calculate and return the specified summary of the contents of x, ignoring missing values if narm is TRUE. May return a null pointer, in which case R will compute the summary by accessing the elements of the vector.
+pub type R_altinteger_Sum_method_t =
+    ::std::option::Option<unsafe extern "C" fn(arg1: SEXP, arg2: Rboolean) -> SEXP>;
+/// Must calculate and return the specified summary of the contents of x, ignoring missing values if narm is TRUE. May return a null pointer, in which case R will compute the summary by accessing the elements of the vector.
+pub type R_altinteger_Min_method_t =
+    ::std::option::Option<unsafe extern "C" fn(arg1: SEXP, arg2: Rboolean) -> SEXP>;
+/// Must calculate and return the specified summary of the contents of x, ignoring missing values if narm is TRUE. May return a null pointer, in which case R will compute the summary by accessing the elements of the vector.
+pub type R_altinteger_Max_method_t =
+    ::std::option::Option<unsafe extern "C" fn(arg1: SEXP, arg2: Rboolean) -> SEXP>;
+/// Must return `x[i]` for the 0-based index i. The default method returns `REAL(x)[i]`.
+pub type R_altreal_Elt_method_t =
+    ::std::option::Option<unsafe extern "C" fn(arg1: SEXP, arg2: R_xlen_t) -> f64>;
+/// Must copy up to n elements, starting at 0-based index i, into `buf[n]`, and return the number of elements copied. The default method defers to REAL_ELT and hence the Elt method above.
+pub type R_altreal_Get_region_method_t = ::std::option::Option<
+    unsafe extern "C" fn(arg1: SEXP, arg2: R_xlen_t, arg3: R_xlen_t, arg4: *mut f64) -> R_xlen_t,
+>;
+///Must return one of the sortedness constants: SORTED_DECR, UNKNOWN_SORTEDNESS, SORTED_INCR. See REAL_IS_SORTED. The default method always returns UNKNOWN_SORTEDNESS.
+pub type R_altreal_Is_sorted_method_t =
+    ::std::option::Option<unsafe extern "C" fn(arg1: SEXP) -> ::std::os::raw::c_int>;
+/// Must return 1 if x is known not to store any missing values (see REAL_NO_NA), otherwise 0 (including if it’s unknown). The default method always returns 0.
+pub type R_altreal_No_NA_method_t =
+    ::std::option::Option<unsafe extern "C" fn(arg1: SEXP) -> ::std::os::raw::c_int>;
+/// Must calculate and return the specified summary of the contents of x, ignoring missing values if narm is TRUE. May return a null pointer, in which case R will compute the summary by accessing the elements of the vector.
+pub type R_altreal_Sum_method_t =
+    ::std::option::Option<unsafe extern "C" fn(arg1: SEXP, arg2: Rboolean) -> SEXP>;
+/// Must calculate and return the specified summary of the contents of x, ignoring missing values if narm is TRUE. May return a null pointer, in which case R will compute the summary by accessing the elements of the vector.
+pub type R_altreal_Min_method_t =
+    ::std::option::Option<unsafe extern "C" fn(arg1: SEXP, arg2: Rboolean) -> SEXP>;
+/// Must calculate and return the specified summary of the contents of x, ignoring missing values if narm is TRUE. May return a null pointer, in which case R will compute the summary by accessing the elements of the vector.
+pub type R_altreal_Max_method_t =
+    ::std::option::Option<unsafe extern "C" fn(arg1: SEXP, arg2: Rboolean) -> SEXP>;
+/// Must return `x[i]` for the 0-based index i. The default method returns `LOGICAL(x)[i]`.
+pub type R_altlogical_Elt_method_t = ::std::option::Option<
+    unsafe extern "C" fn(arg1: SEXP, arg2: R_xlen_t) -> ::std::os::raw::c_int,
+>;
+/// Must copy up to n elements, starting at 0-based index i, into `buf[n]`, and return the number of elements copied. The default method defers to LOGICAL_ELT and hence the Elt method above.
+pub type R_altlogical_Get_region_method_t = ::std::option::Option<
+    unsafe extern "C" fn(
+        arg1: SEXP,
+        arg2: R_xlen_t,
+        arg3: R_xlen_t,
+        arg4: *mut ::std::os::raw::c_int,
+    ) -> R_xlen_t,
+>;
+/// Must return one of the sortedness constants: SORTED_DECR, UNKNOWN_SORTEDNESS, SORTED_INCR. See INTEGER_IS_SORTED. The default method always returns UNKNOWN_SORTEDNESS
+pub type R_altlogical_Is_sorted_method_t =
+    ::std::option::Option<unsafe extern "C" fn(arg1: SEXP) -> ::std::os::raw::c_int>;
+/// Must return 1 if x is known not to store any missing values (see LOGICAL_NO_NA), otherwise 0 (including if it’s unknown). The default method always returns 0.
+pub type R_altlogical_No_NA_method_t =
+    ::std::option::Option<unsafe extern "C" fn(arg1: SEXP) -> ::std::os::raw::c_int>;
+/// Must calculate and return the specified summary of the contents of x, ignoring missing values if narm is TRUE. May return a null pointer, in which case R will compute the summary by accessing the elements of the vector.
+/// Versions: Appeared in 3.6.0.
+pub type R_altlogical_Sum_method_t =
+    ::std::option::Option<unsafe extern "C" fn(arg1: SEXP, arg2: Rboolean) -> SEXP>;
+/// Must return `x[i]` for the 0-based index i. The default method returns `RAW(x)[i]`.
+pub type R_altraw_Elt_method_t =
+    ::std::option::Option<unsafe extern "C" fn(arg1: SEXP, arg2: R_xlen_t) -> Rbyte>;
+/// Must copy up to n elements, starting at 0-based index i, into `buf[n]`, and return the number of elements copied. The default method defers to `RAW_ELT` and hence the Elt method above.
+/// Versions: Appeared in 3.6.0.
+pub type R_altraw_Get_region_method_t = ::std::option::Option<
+    unsafe extern "C" fn(arg1: SEXP, arg2: R_xlen_t, arg3: R_xlen_t, arg4: *mut Rbyte) -> R_xlen_t,
+>;
+/// Must return `x[i]` for the 0-based index i. The default method returns `COMPLEX(x)[i]`.
+pub type R_altcomplex_Elt_method_t =
+    ::std::option::Option<unsafe extern "C" fn(arg1: SEXP, arg2: R_xlen_t) -> Rcomplex>;
+/// Must copy up to n elements, starting at 0-based index i, into `buf[n]`, and return the number of elements copied. The default method defers to COMPLEX_ELT and hence the Elt method above.
+/// Versions: Appeared in 3.6.0.
+pub type R_altcomplex_Get_region_method_t = ::std::option::Option<
+    unsafe extern "C" fn(
+        arg1: SEXP,
+        arg2: R_xlen_t,
+        arg3: R_xlen_t,
+        arg4: *mut Rcomplex,
+    ) -> R_xlen_t,
+>;
+/// Must return `x[i]` (for 0-based i) or signal an error. The default method always raises an error.
+pub type R_altstring_Elt_method_t =
+    ::std::option::Option<unsafe extern "C" fn(arg1: SEXP, arg2: R_xlen_t) -> SEXP>;
+/// Must set `x[i] <- v` (for 0-based i), even if it’s not compatible with the chosen compact representation, or signal an error. The default method always raises an error.
+pub type R_altstring_Set_elt_method_t =
+    ::std::option::Option<unsafe extern "C" fn(arg1: SEXP, arg2: R_xlen_t, arg3: SEXP)>;
+/// Must return one of the sortedness constants: SORTED_DECR, UNKNOWN_SORTEDNESS, SORTED_INCR. See STRING_IS_SORTED. The default method always returns UNKNOWN_SORTEDNESS.
+pub type R_altstring_Is_sorted_method_t =
+    ::std::option::Option<unsafe extern "C" fn(arg1: SEXP) -> ::std::os::raw::c_int>;
+/// Must return 1 if x is known not to store any missing values (see STRING_NO_NA), otherwise 0 (including if it’s unknown). The default method always returns 0.
+pub type R_altstring_No_NA_method_t =
+    ::std::option::Option<unsafe extern "C" fn(arg1: SEXP) -> ::std::os::raw::c_int>;
+/// Must return `x[[i]]` for the 0-based index i or raise an error.
+pub type R_altlist_Elt_method_t =
+    ::std::option::Option<unsafe extern "C" fn(arg1: SEXP, arg2: R_xlen_t) -> SEXP>;
+/// Must set `x[[i]] <- v` for the 0-based index i, altering x, or raise an error.
+pub type R_altlist_Set_elt_method_t =
+    ::std::option::Option<unsafe extern "C" fn(arg1: SEXP, arg2: R_xlen_t, arg3: SEXP)>;
+
+extern "C" {
+    /// Returns a nonzero value if x is a “compact representation” (ALTREP) value. Access to the data pointers of ALTREP values may be problematic and should be replaced with access to individual elements (see REAL_ELT) or vector subregions (see REAL_GET_REGION). See also: <https://svn.r-project.org/R/branches/ALTREP/ALTREP.html>
+    pub fn ALTREP(x: SEXP) -> ::std::os::raw::c_int;
+    /// Returns the ALTREP class object containing the methods set up for use on ax. The return value is of unspecified SEXPTYPE. Behaviour is undefined if ax is not an ALTREP object.
+    pub fn ALTREP_CLASS(x: SEXP) -> SEXP;
+    /// Returns one of the two ALTREP instance variables belonging to ax. Behaviour is undefined if ax is not an ALTREP object.
+    pub fn R_altrep_data1(x: SEXP) -> SEXP;
+    /// Returns one of the two ALTREP instance variables belonging to ax. Behaviour is undefined if ax is not an ALTREP object.
+    pub fn R_altrep_data2(x: SEXP) -> SEXP;
+    /// Allocates, constructs and returns a compact-representation value. It will have the TYPEOF() of the corresponding class and will use the methods in aclass and the instance variables to respond to calls such as XLENGTH().
+    pub fn R_new_altrep(aclass: R_altrep_class_t, data1: SEXP, data2: SEXP) -> SEXP;
+    /// Allocates, registers and returns an ALTREP class with the given names.
+    /// In addition to the methods given here, compact representation string vectors must implement the common “altrep” methods (see R_set_altrep_..._method) and the common “altvec” methods (see R_set_altvec_..._method).
+    pub fn R_make_altstring_class(
+        cname: *const ::std::os::raw::c_char,
+        pname: *const ::std::os::raw::c_char,
+        info: *mut DllInfo,
+    ) -> R_altrep_class_t;
+    /// Allocates, registers and returns an ALTREP class with the given names.
+
+    /// In addition to the methods given here, compact representation integer vectors must implement the common “altrep” methods (see R_set_altrep_..._method) and the common “altvec” methods (see R_set_altvec_..._method).
+    pub fn R_make_altinteger_class(
+        cname: *const ::std::os::raw::c_char,
+        pname: *const ::std::os::raw::c_char,
+        info: *mut DllInfo,
+    ) -> R_altrep_class_t;
+    /// Allocates, registers and returns an ALTREP class with the given names.
+
+    /// In addition to the methods given here, compact representation integer vectors must implement the common “altrep” methods (see R_set_altrep_..._method) and the common “altvec” methods (see R_set_altvec_..._method).
+    pub fn R_make_altreal_class(
+        cname: *const ::std::os::raw::c_char,
+        pname: *const ::std::os::raw::c_char,
+        info: *mut DllInfo,
+    ) -> R_altrep_class_t;
+    /// Allocates, registers and returns an ALTREP class with the given names.
+
+    /// In addition to the methods given here, compact representation integer vectors must implement the common “altrep” methods (see R_set_altrep_..._method) and the common “altvec” methods (see R_set_altvec_..._method).
+    pub fn R_make_altlogical_class(
+        cname: *const ::std::os::raw::c_char,
+        pname: *const ::std::os::raw::c_char,
+        info: *mut DllInfo,
+    ) -> R_altrep_class_t;
+    /// Allocates, registers and returns an ALTREP class with the given names.
+
+    /// In addition to the methods given here, compact representation integer vectors must implement the common “altrep” methods (see R_set_altrep_..._method) and the common “altvec” methods (see R_set_altvec_..._method).
+    pub fn R_make_altraw_class(
+        cname: *const ::std::os::raw::c_char,
+        pname: *const ::std::os::raw::c_char,
+        info: *mut DllInfo,
+    ) -> R_altrep_class_t;
+    /// Allocates, registers and returns an ALTREP class with the given names.
+
+    /// In addition to the methods given here, compact representation integer vectors must implement the common “altrep” methods (see R_set_altrep_..._method) and the common “altvec” methods (see R_set_altvec_..._method).
+    pub fn R_make_altcomplex_class(
+        cname: *const ::std::os::raw::c_char,
+        pname: *const ::std::os::raw::c_char,
+        info: *mut DllInfo,
+    ) -> R_altrep_class_t;
+    /// Allocates, registers and returns an ALTREP class with the given names.
+
+    /// In addition to the methods given here, compact representation integer vectors must implement the common “altrep” methods (see R_set_altrep_..._method) and the common “altvec” methods (see R_set_altvec_..._method).
+    pub fn R_make_altlist_class(
+        cname: *const ::std::os::raw::c_char,
+        pname: *const ::std::os::raw::c_char,
+        info: *mut DllInfo,
+    ) -> R_altrep_class_t;
+    /// Returns a nonzero value if x is an ALTREP value whose class pointer is class.
+    pub fn R_altrep_inherits(x: SEXP, arg1: R_altrep_class_t) -> Rboolean;
+    /// Sets the function pointer inside the ALTREP class object. Currently, these methods are common to all ALTREP classes. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altrep_UnserializeEX_method(
+        cls: R_altrep_class_t,
+        fun: R_altrep_UnserializeEX_method_t,
+    );
+    /// Sets the function pointer inside the ALTREP class object. Currently, these methods are common to all ALTREP classes. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altrep_Unserialize_method(
+        cls: R_altrep_class_t,
+        fun: R_altrep_Unserialize_method_t,
+    );
+    /// Sets the function pointer inside the ALTREP class object. Currently, these methods are common to all ALTREP classes. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altrep_Serialized_state_method(
+        cls: R_altrep_class_t,
+        fun: R_altrep_Serialized_state_method_t,
+    );
+    /// Sets the corresponding ALTREP instance variable of ax to v, sharing it without duplication. Behaviour is undefined if ax is not an ALTREP object.
+    /// Versions: Appeared in 3.5.0.
+    pub fn R_set_altrep_data1(x: SEXP, v: SEXP);
+    /// Sets the corresponding ALTREP instance variable of ax to v, sharing it without duplication. Behaviour is undefined if ax is not an ALTREP object.
+    /// Versions: Appeared in 3.5.0.
+    pub fn R_set_altrep_data2(x: SEXP, v: SEXP);
+    ///Sets the function pointer inside the ALTREP class object. Currently, these methods are common to all ALTREP classes. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altrep_DuplicateEX_method(
+        cls: R_altrep_class_t,
+        fun: R_altrep_DuplicateEX_method_t,
+    );
+    /// Sets the function pointer inside the ALTREP class object. Currently, these methods are common to all ALTREP classes. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altrep_Duplicate_method(cls: R_altrep_class_t, fun: R_altrep_Duplicate_method_t);
+    /// Sets the function pointer inside the ALTREP class object. Currently, these methods are common to all ALTREP classes. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altrep_Coerce_method(cls: R_altrep_class_t, fun: R_altrep_Coerce_method_t);
+    /// Sets the function pointer inside the ALTREP class object. Currently, these methods are common to all ALTREP classes. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altrep_Inspect_method(cls: R_altrep_class_t, fun: R_altrep_Inspect_method_t);
+    /// Sets the function pointer inside the ALTREP class object. Currently, these methods are common to all ALTREP classes. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altrep_Length_method(cls: R_altrep_class_t, fun: R_altrep_Length_method_t);
+    /// Sets the function pointer inside the ALTREP class object. Currently, all ALTREP classes are vector classes. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altvec_Dataptr_method(cls: R_altrep_class_t, fun: R_altvec_Dataptr_method_t);
+    /// Sets the function pointer inside the ALTREP class object. Currently, all ALTREP classes are vector classes. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altvec_Dataptr_or_null_method(
+        cls: R_altrep_class_t,
+        fun: R_altvec_Dataptr_or_null_method_t,
+    );
+    /// Sets the function pointer inside the ALTREP class object. Currently, all ALTREP classes are vector classes. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altvec_Extract_subset_method(
+        cls: R_altrep_class_t,
+        fun: R_altvec_Extract_subset_method_t,
+    );
+    /// Sets the function pointer inside the ALTREP class object. Behaviour is undefined if cls is not a compact integer vector class. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altinteger_Elt_method(cls: R_altrep_class_t, fun: R_altinteger_Elt_method_t);
+    /// Sets the function pointer inside the ALTREP class object. Behaviour is undefined if cls is not a compact integer vector class. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altinteger_Get_region_method(
+        cls: R_altrep_class_t,
+        fun: R_altinteger_Get_region_method_t,
+    );
+    /// Sets the function pointer inside the ALTREP class object. Behaviour is undefined if cls is not a compact integer vector class. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altinteger_Is_sorted_method(
+        cls: R_altrep_class_t,
+        fun: R_altinteger_Is_sorted_method_t,
+    );
+    /// Sets the function pointer inside the ALTREP class object. Behaviour is undefined if cls is not a compact integer vector class. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altinteger_No_NA_method(cls: R_altrep_class_t, fun: R_altinteger_No_NA_method_t);
+    /// Sets the function pointer inside the ALTREP class object. Behaviour is undefined if cls is not a compact integer vector class. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altinteger_Sum_method(cls: R_altrep_class_t, fun: R_altinteger_Sum_method_t);
+    /// Sets the function pointer inside the ALTREP class object. Behaviour is undefined if cls is not a compact integer vector class. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altinteger_Min_method(cls: R_altrep_class_t, fun: R_altinteger_Min_method_t);
+    /// Sets the function pointer inside the ALTREP class object. Behaviour is undefined if cls is not a compact integer vector class. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altinteger_Max_method(cls: R_altrep_class_t, fun: R_altinteger_Max_method_t);
+    /// Sets the function pointer inside the ALTREP class object. Behaviour is undefined if cls is not a compact real vector class. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altreal_Elt_method(cls: R_altrep_class_t, fun: R_altreal_Elt_method_t);
+    /// Sets the function pointer inside the ALTREP class object. Behaviour is undefined if cls is not a compact real vector class. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altreal_Get_region_method(
+        cls: R_altrep_class_t,
+        fun: R_altreal_Get_region_method_t,
+    );
+    /// Sets the function pointer inside the ALTREP class object. Behaviour is undefined if cls is not a compact real vector class. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altreal_Is_sorted_method(cls: R_altrep_class_t, fun: R_altreal_Is_sorted_method_t);
+    /// Sets the function pointer inside the ALTREP class object. Behaviour is undefined if cls is not a compact real vector class. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altreal_No_NA_method(cls: R_altrep_class_t, fun: R_altreal_No_NA_method_t);
+    /// Sets the function pointer inside the ALTREP class object. Behaviour is undefined if cls is not a compact real vector class. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altreal_Sum_method(cls: R_altrep_class_t, fun: R_altreal_Sum_method_t);
+    /// Sets the function pointer inside the ALTREP class object. Behaviour is undefined if cls is not a compact real vector class. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altreal_Min_method(cls: R_altrep_class_t, fun: R_altreal_Min_method_t);
+    /// Sets the function pointer inside the ALTREP class object. Behaviour is undefined if cls is not a compact real vector class. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altreal_Max_method(cls: R_altrep_class_t, fun: R_altreal_Max_method_t);
+    /// Sets the function pointer inside the ALTREP class object. Behaviour is undefined if cls is not a compact integer vector class. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altlogical_Elt_method(cls: R_altrep_class_t, fun: R_altlogical_Elt_method_t);
+    /// Sets the function pointer inside the ALTREP class object. Behaviour is undefined if cls is not a compact integer vector class. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altlogical_Get_region_method(
+        cls: R_altrep_class_t,
+        fun: R_altlogical_Get_region_method_t,
+    );
+    /// Sets the function pointer inside the ALTREP class object. Behaviour is undefined if cls is not a compact integer vector class. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altlogical_Is_sorted_method(
+        cls: R_altrep_class_t,
+        fun: R_altlogical_Is_sorted_method_t,
+    );
+    /// Sets the function pointer inside the ALTREP class object. Behaviour is undefined if cls is not a compact integer vector class. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altlogical_No_NA_method(cls: R_altrep_class_t, fun: R_altlogical_No_NA_method_t);
+    /// Sets the function pointer inside the ALTREP class object. Behaviour is undefined if cls is not a compact integer vector class. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altlogical_Sum_method(cls: R_altrep_class_t, fun: R_altlogical_Sum_method_t);
+    /// Sets the function pointer inside the ALTREP class object. Behaviour is undefined if cls is not a compact complex vector class. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altraw_Elt_method(cls: R_altrep_class_t, fun: R_altraw_Elt_method_t);
+    /// Sets the function pointer inside the ALTREP class object. Behaviour is undefined if cls is not a compact complex vector class. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altraw_Get_region_method(cls: R_altrep_class_t, fun: R_altraw_Get_region_method_t);
+    /// Sets the function pointer inside the ALTREP class object. Behaviour is undefined if cls is not a compact complex vector class. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altcomplex_Elt_method(cls: R_altrep_class_t, fun: R_altcomplex_Elt_method_t);
+    /// Sets the function pointer inside the ALTREP class object. Behaviour is undefined if cls is not a compact complex vector class. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altcomplex_Get_region_method(
+        cls: R_altrep_class_t,
+        fun: R_altcomplex_Get_region_method_t,
+    );
+    /// Sets the function pointer inside the ALTREP class object. Behaviour is undefined if cls is not a compact string representation class. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altstring_Elt_method(cls: R_altrep_class_t, fun: R_altstring_Elt_method_t);
+    /// Sets the function pointer inside the ALTREP class object. Behaviour is undefined if cls is not a compact string representation class. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altstring_Set_elt_method(cls: R_altrep_class_t, fun: R_altstring_Set_elt_method_t);
+    /// Sets the function pointer inside the ALTREP class object. Behaviour is undefined if cls is not a compact string representation class. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altstring_Is_sorted_method(
+        cls: R_altrep_class_t,
+        fun: R_altstring_Is_sorted_method_t,
+    );
+    /// Sets the function pointer inside the ALTREP class object. Behaviour is undefined if cls is not a compact string representation class. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altstring_No_NA_method(cls: R_altrep_class_t, fun: R_altstring_No_NA_method_t);
+    /// Sets the function pointer inside the ALTREP class object. Behaviour is undefined if cls is not a compact complex vector class. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altlist_Elt_method(cls: R_altrep_class_t, fun: R_altlist_Elt_method_t);
+    /// Sets the function pointer inside the ALTREP class object. Behaviour is undefined if cls is not a compact complex vector class. There is no facility for unregistering ALTREP classes, so once a function pointer is set from a package shared library, it must not be unloaded.
+    pub fn R_set_altlist_Set_elt_method(cls: R_altrep_class_t, fun: R_altlist_Set_elt_method_t);
+}
diff --git a/extendr-ffi/src/backports.rs b/extendr-ffi/src/backports.rs
new file mode 100644
index 000000000..6b473cf19
--- /dev/null
+++ b/extendr-ffi/src/backports.rs
@@ -0,0 +1,147 @@
+//! 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.
+
+// 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::SEXP;
+// In your extern "C" block:
+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;
+}
+
+/// 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.
+#[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
+#[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 the value of the requested variable in an environment
+#[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
+#[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
+#[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
+#[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
+#[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)
+    }
+}
diff --git a/extendr-ffi/src/graphics.rs b/extendr-ffi/src/graphics.rs
new file mode 100644
index 000000000..75db5c53f
--- /dev/null
+++ b/extendr-ffi/src/graphics.rs
@@ -0,0 +1,518 @@
+//! Graphics Engine support
+use super::*;
+
+pub const R_GE_version: u32 = 16;
+// Types
+pub type DevDesc = _DevDesc;
+pub type pDevDesc = *mut DevDesc;
+pub type pGEcontext = *mut R_GE_gcontext;
+pub type pGEDevDesc = *mut GEDevDesc;
+
+// Constants
+pub const LTY_BLANK: i32 = -1;
+pub const LTY_SOLID: u32 = 0;
+pub const LTY_DASHED: u32 = 68;
+pub const LTY_DOTTED: u32 = 49;
+pub const LTY_DOTDASH: u32 = 13361;
+pub const LTY_LONGDASH: u32 = 55;
+pub const LTY_TWODASH: u32 = 9762;
+pub const R_GE_definitions: u32 = 13;
+
+// Enums
+#[repr(u32)]
+#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
+pub enum GEUnit {
+    GE_DEVICE = 0,
+    GE_NDC = 1,
+    GE_INCHES = 2,
+    GE_CM = 3,
+}
+
+#[repr(u32)]
+#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
+pub enum R_GE_lineend {
+    GE_ROUND_CAP = 1,
+    GE_BUTT_CAP = 2,
+    GE_SQUARE_CAP = 3,
+}
+
+#[repr(u32)]
+#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
+pub enum R_GE_linejoin {
+    GE_ROUND_JOIN = 1,
+    GE_MITRE_JOIN = 2,
+    GE_BEVEL_JOIN = 3,
+}
+
+// Structs
+#[repr(C)]
+#[derive(Debug, Copy, Clone)]
+pub struct R_GE_gcontext {
+    pub col: ::std::os::raw::c_int,
+    pub fill: ::std::os::raw::c_int,
+    pub gamma: f64,
+    pub lwd: f64,
+    pub lty: ::std::os::raw::c_int,
+    pub lend: R_GE_lineend,
+    pub ljoin: R_GE_linejoin,
+    pub lmitre: f64,
+    pub cex: f64,
+    pub ps: f64,
+    pub lineheight: f64,
+    pub fontface: ::std::os::raw::c_int,
+    pub fontfamily: [::std::os::raw::c_char; 201usize],
+    pub patternFill: SEXP,
+}
+
+#[repr(C)]
+#[derive(Debug, Copy, Clone)]
+pub struct _DevDesc {
+    #[doc = "left raster coordinate"]
+    pub left: f64,
+    #[doc = "right raster coordinate"]
+    pub right: f64,
+    #[doc = "bottom raster coordinate"]
+    pub bottom: f64,
+    #[doc = "top raster coordinate"]
+    pub top: f64,
+    #[doc = "R only has the notion of a rectangular clipping region"]
+    pub clipLeft: f64,
+    pub clipRight: f64,
+    pub clipBottom: f64,
+    pub clipTop: f64,
+    #[doc = "x character addressing offset - unused"]
+    pub xCharOffset: f64,
+    #[doc = "y character addressing offset"]
+    pub yCharOffset: f64,
+    #[doc = "1/2 interline space as frac of line height"]
+    pub yLineBias: f64,
+    #[doc = "Inches per raster; \\[0\\]=x, \\[1\\]=y"]
+    pub ipr: [f64; 2usize],
+    #[doc = "Character size in rasters; \\[0\\]=x, \\[1\\]=y"]
+    pub cra: [f64; 2usize],
+    #[doc = "(initial) Device Gamma Correction"]
+    pub gamma: f64,
+    #[doc = "Device-level clipping"]
+    pub canClip: Rboolean,
+    #[doc = "can the gamma factor be modified?"]
+    pub canChangeGamma: Rboolean,
+    #[doc = "Can do at least some horiz adjust of text\n0 = none, 1 = {0,0.5,1}, 2 = \\[0,1\\]"]
+    pub canHAdj: ::std::os::raw::c_int,
+    #[doc = "Device initial settings\n/\n/* These are things that the device must set up when it is created.\n The graphics system can modify them and track current values,"]
+    pub startps: f64,
+    #[doc = "sets par(\"fg\"), par(\"col\") and gpar(\"col\")"]
+    pub startcol: ::std::os::raw::c_int,
+    #[doc = "sets par(\"bg\") and gpar(\"fill\")"]
+    pub startfill: ::std::os::raw::c_int,
+    pub startlty: ::std::os::raw::c_int,
+    pub startfont: ::std::os::raw::c_int,
+    pub startgamma: f64,
+    #[doc = "pointer to device specific parameters"]
+    pub deviceSpecific: *mut ::std::os::raw::c_void,
+    #[doc = "toggle for initial display list status"]
+    pub displayListOn: Rboolean,
+    #[doc = "can the device generate mousedown events"]
+    pub canGenMouseDown: Rboolean,
+    #[doc = "can the device generate mousemove events"]
+    pub canGenMouseMove: Rboolean,
+    #[doc = "can the device generate mouseup events"]
+    pub canGenMouseUp: Rboolean,
+    #[doc = "can the device generate keyboard events"]
+    pub canGenKeybd: Rboolean,
+    #[doc = "can the device generate idle events"]
+    pub canGenIdle: Rboolean,
+    #[doc = "This is set while getGraphicsEvent\nis actively looking for events"]
+    pub gettingEvent: Rboolean,
+    pub activate: ::std::option::Option<unsafe extern "C" fn(arg1: pDevDesc)>,
+    pub circle: ::std::option::Option<
+        unsafe extern "C" fn(x: f64, y: f64, r: f64, gc: pGEcontext, dd: pDevDesc),
+    >,
+    pub clip: ::std::option::Option<
+        unsafe extern "C" fn(x0: f64, x1: f64, y0: f64, y1: f64, dd: pDevDesc),
+    >,
+    pub close: ::std::option::Option<unsafe extern "C" fn(dd: pDevDesc)>,
+    pub deactivate: ::std::option::Option<unsafe extern "C" fn(arg1: pDevDesc)>,
+    pub locator: ::std::option::Option<
+        unsafe extern "C" fn(x: *mut f64, y: *mut f64, dd: pDevDesc) -> Rboolean,
+    >,
+    pub line: ::std::option::Option<
+        unsafe extern "C" fn(x1: f64, y1: f64, x2: f64, y2: f64, gc: pGEcontext, dd: pDevDesc),
+    >,
+    pub metricInfo: ::std::option::Option<
+        unsafe extern "C" fn(
+            c: ::std::os::raw::c_int,
+            gc: pGEcontext,
+            ascent: *mut f64,
+            descent: *mut f64,
+            width: *mut f64,
+            dd: pDevDesc,
+        ),
+    >,
+    pub mode:
+        ::std::option::Option<unsafe extern "C" fn(mode: ::std::os::raw::c_int, dd: pDevDesc)>,
+    pub newPage: ::std::option::Option<unsafe extern "C" fn(gc: pGEcontext, dd: pDevDesc)>,
+    pub polygon: ::std::option::Option<
+        unsafe extern "C" fn(
+            n: ::std::os::raw::c_int,
+            x: *mut f64,
+            y: *mut f64,
+            gc: pGEcontext,
+            dd: pDevDesc,
+        ),
+    >,
+    pub polyline: ::std::option::Option<
+        unsafe extern "C" fn(
+            n: ::std::os::raw::c_int,
+            x: *mut f64,
+            y: *mut f64,
+            gc: pGEcontext,
+            dd: pDevDesc,
+        ),
+    >,
+    pub rect: ::std::option::Option<
+        unsafe extern "C" fn(x0: f64, y0: f64, x1: f64, y1: f64, gc: pGEcontext, dd: pDevDesc),
+    >,
+    pub path: ::std::option::Option<
+        unsafe extern "C" fn(
+            x: *mut f64,
+            y: *mut f64,
+            npoly: ::std::os::raw::c_int,
+            nper: *mut ::std::os::raw::c_int,
+            winding: Rboolean,
+            gc: pGEcontext,
+            dd: pDevDesc,
+        ),
+    >,
+    pub raster: ::std::option::Option<
+        unsafe extern "C" fn(
+            raster: *mut ::std::os::raw::c_uint,
+            w: ::std::os::raw::c_int,
+            h: ::std::os::raw::c_int,
+            x: f64,
+            y: f64,
+            width: f64,
+            height: f64,
+            rot: f64,
+            interpolate: Rboolean,
+            gc: pGEcontext,
+            dd: pDevDesc,
+        ),
+    >,
+    pub cap: ::std::option::Option<unsafe extern "C" fn(dd: pDevDesc) -> SEXP>,
+    pub size: ::std::option::Option<
+        unsafe extern "C" fn(
+            left: *mut f64,
+            right: *mut f64,
+            bottom: *mut f64,
+            top: *mut f64,
+            dd: pDevDesc,
+        ),
+    >,
+    pub strWidth: ::std::option::Option<
+        unsafe extern "C" fn(
+            str_: *const ::std::os::raw::c_char,
+            gc: pGEcontext,
+            dd: pDevDesc,
+        ) -> f64,
+    >,
+    pub text: ::std::option::Option<
+        unsafe extern "C" fn(
+            x: f64,
+            y: f64,
+            str_: *const ::std::os::raw::c_char,
+            rot: f64,
+            hadj: f64,
+            gc: pGEcontext,
+            dd: pDevDesc,
+        ),
+    >,
+    pub onExit: ::std::option::Option<unsafe extern "C" fn(dd: pDevDesc)>,
+    #[doc = "device_getEvent is no longer used, but the slot is kept for back\n compatibility of the structure."]
+    pub getEvent: ::std::option::Option<
+        unsafe extern "C" fn(arg1: SEXP, arg2: *const ::std::os::raw::c_char) -> SEXP,
+    >,
+    pub newFrameConfirm: ::std::option::Option<unsafe extern "C" fn(dd: pDevDesc) -> Rboolean>,
+    #[doc = "and strWidthUTF8"]
+    pub hasTextUTF8: Rboolean,
+    pub textUTF8: ::std::option::Option<
+        unsafe extern "C" fn(
+            x: f64,
+            y: f64,
+            str_: *const ::std::os::raw::c_char,
+            rot: f64,
+            hadj: f64,
+            gc: pGEcontext,
+            dd: pDevDesc,
+        ),
+    >,
+    pub strWidthUTF8: ::std::option::Option<
+        unsafe extern "C" fn(
+            str_: *const ::std::os::raw::c_char,
+            gc: pGEcontext,
+            dd: pDevDesc,
+        ) -> f64,
+    >,
+    pub wantSymbolUTF8: Rboolean,
+    #[doc = "Is rotated text good enough to be preferable to Hershey in\ncontour labels?  Old default was FALSE."]
+    pub useRotatedTextInContour: Rboolean,
+    #[doc = "This is an environment holding event handlers."]
+    pub eventEnv: SEXP,
+    pub eventHelper:
+        ::std::option::Option<unsafe extern "C" fn(dd: pDevDesc, code: ::std::os::raw::c_int)>,
+    pub holdflush: ::std::option::Option<
+        unsafe extern "C" fn(dd: pDevDesc, level: ::std::os::raw::c_int) -> ::std::os::raw::c_int,
+    >,
+    #[doc = "1 = no, 2 = yes"]
+    pub haveTransparency: ::std::os::raw::c_int,
+    #[doc = "1 = no, 2 = fully, 3 = semi"]
+    pub haveTransparentBg: ::std::os::raw::c_int,
+    #[doc = "1 = no, 2 = yes, 3 = except for missing values"]
+    pub haveRaster: ::std::os::raw::c_int,
+    #[doc = "1 = no, 2 = yes"]
+    pub haveCapture: ::std::os::raw::c_int,
+    #[doc = "1 = no, 2 = yes"]
+    pub haveLocator: ::std::os::raw::c_int,
+    pub setPattern:
+        ::std::option::Option<unsafe extern "C" fn(pattern: SEXP, dd: pDevDesc) -> SEXP>,
+    pub releasePattern: ::std::option::Option<unsafe extern "C" fn(ref_: SEXP, dd: pDevDesc)>,
+    pub setClipPath:
+        ::std::option::Option<unsafe extern "C" fn(path: SEXP, ref_: SEXP, dd: pDevDesc) -> SEXP>,
+    pub releaseClipPath: ::std::option::Option<unsafe extern "C" fn(ref_: SEXP, dd: pDevDesc)>,
+    pub setMask:
+        ::std::option::Option<unsafe extern "C" fn(path: SEXP, ref_: SEXP, dd: pDevDesc) -> SEXP>,
+    pub releaseMask: ::std::option::Option<unsafe extern "C" fn(ref_: SEXP, dd: pDevDesc)>,
+    #[doc = "This should match R_GE_version,\n BUT it does not have to.\n It give the graphics engine a chance to work with\n graphics device packages BEFORE they update to\n changes in R_GE_version."]
+    pub deviceVersion: ::std::os::raw::c_int,
+    #[doc = "This can be used to OVERRIDE canClip so that graphics engine\n leaves ALL clipping to the graphics device"]
+    pub deviceClip: Rboolean,
+    pub defineGroup: ::std::option::Option<
+        unsafe extern "C" fn(
+            source: SEXP,
+            op: ::std::os::raw::c_int,
+            destination: SEXP,
+            dd: pDevDesc,
+        ) -> SEXP,
+    >,
+    pub useGroup:
+        ::std::option::Option<unsafe extern "C" fn(ref_: SEXP, trans: SEXP, dd: pDevDesc)>,
+    pub releaseGroup: ::std::option::Option<unsafe extern "C" fn(ref_: SEXP, dd: pDevDesc)>,
+    pub stroke:
+        ::std::option::Option<unsafe extern "C" fn(path: SEXP, gc: pGEcontext, dd: pDevDesc)>,
+    pub fill: ::std::option::Option<
+        unsafe extern "C" fn(path: SEXP, rule: ::std::os::raw::c_int, gc: pGEcontext, dd: pDevDesc),
+    >,
+    pub fillStroke: ::std::option::Option<
+        unsafe extern "C" fn(path: SEXP, rule: ::std::os::raw::c_int, gc: pGEcontext, dd: pDevDesc),
+    >,
+    pub capabilities: ::std::option::Option<unsafe extern "C" fn(cap: SEXP) -> SEXP>,
+    pub glyph: ::std::option::Option<
+        unsafe extern "C" fn(
+            n: ::std::os::raw::c_int,
+            glyphs: *mut ::std::os::raw::c_int,
+            x: *mut f64,
+            y: *mut f64,
+            font: SEXP,
+            size: f64,
+            colour: ::std::os::raw::c_int,
+            rot: f64,
+            dd: pDevDesc,
+        ),
+    >,
+    #[doc = "Area for future expansion.\nBy zeroing this, devices are more likely to work if loaded\ninto a later version of R than that they were compiled under."]
+    pub reserved: [::std::os::raw::c_char; 64usize],
+}
+
+#[repr(C)]
+#[derive(Debug, Copy, Clone)]
+pub struct _GEDevDesc {
+    #[doc = "Stuff that the devices can see (and modify).\n All detailed in GraphicsDevice.h"]
+    pub dev: pDevDesc,
+    #[doc = "toggle for display list status"]
+    pub displayListOn: Rboolean,
+    #[doc = "display list"]
+    pub displayList: SEXP,
+    #[doc = "A pointer to the end of the display list\nto avoid traversing pairlists"]
+    pub DLlastElt: SEXP,
+    #[doc = "The last element of the display list\n just prior to when the display list\n was last initialised"]
+    pub savedSnapshot: SEXP,
+    #[doc = "Has the device received any output?"]
+    pub dirty: Rboolean,
+    #[doc = "Should a graphics call be stored\n on the display list?\n Set to FALSE by do_recordGraphics,\n do_dotcallgr, and do_Externalgr\n so that nested calls are not\n recorded on the display list"]
+    pub recordGraphics: Rboolean,
+    #[doc = "Stuff about the device that only graphics systems see.\n The graphics engine has no idea what is in here.\n Used by graphics systems to store system state per device."]
+    pub gesd: [*mut GESystemDesc; 24usize],
+    #[doc = "per-device setting for 'ask' (use NewFrameConfirm)"]
+    pub ask: Rboolean,
+    #[doc = "Is a device appending a path ?"]
+    pub appending: Rboolean,
+}
+
+pub type GEcallback = ::std::option::Option<
+    unsafe extern "C" fn(arg1: GEevent, arg2: *mut GEDevDesc, arg3: SEXP) -> SEXP,
+>;
+
+#[repr(u32)]
+#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
+pub enum GEevent {
+    #[doc = "In response to this event, the registered graphics system\n should allocate and initialise the systemSpecific structure\n\n Should return R_NilValue on failure so that engine\n can tidy up memory allocation"]
+    GE_InitState = 0,
+    #[doc = "This event gives the registered system a chance to undo\n anything done in the initialisation."]
+    GE_FinaliseState = 1,
+    #[doc = "This is sent by the graphics engine prior to initialising\n the display list.  It give the graphics system the chance\n to squirrel away information it will need for redrawing the\n the display list"]
+    GE_SaveState = 2,
+    #[doc = "This is sent by the graphics engine prior to replaying the\n display list.  It gives the graphics system the chance to\n restore any information it saved on the GE_SaveState event"]
+    GE_RestoreState = 6,
+    #[doc = "Copy system state information to the current device.\n This is used when copying graphics from one device to another\n so all the graphics system needs to do is to copy across\n the bits required for the display list to draw faithfully\n on the new device."]
+    GE_CopyState = 3,
+    #[doc = "Create a snapshot of the system state that is sufficient\n for the current \"image\" to be reproduced"]
+    GE_SaveSnapshotState = 4,
+    #[doc = "Restore the system state that is saved by GE_SaveSnapshotState"]
+    GE_RestoreSnapshotState = 5,
+    #[doc = "When replaying the display list, the graphics engine\n checks, after each replayed action, that the action\n produced valid output.  This is the graphics system's\n chance to say that the output is crap (in which case the\n graphics engine will abort the display list replay)."]
+    GE_CheckPlot = 7,
+    #[doc = "The device wants to scale the current pointsize\n (for scaling an image)\n This is not a nice general solution, but a quick fix for\n the Windows device."]
+    GE_ScalePS = 8,
+}
+
+pub type GEDevDesc = _GEDevDesc;
+
+#[repr(C)]
+#[derive(Debug, Copy, Clone)]
+pub struct GESystemDesc {
+    #[doc = "An array of information about each graphics system that\n has registered with the graphics engine.\n This is used to store graphics state for each graphics\n system on each device."]
+    pub systemSpecific: *mut ::std::os::raw::c_void,
+    #[doc = "An array of function pointers, one per graphics system that\n has registered with the graphics engine.\n\n system_Callback is called when the graphics engine wants\n to give a graphics system the chance to play with its\n device-specific information (stored in systemSpecific)\n There are two parameters:  an \"event\" to tell the graphics\n system why the graphics engine has called this function,\n and the systemSpecific pointer.  The graphics engine\n has to pass the systemSpecific pointer because only\n the graphics engine will know what array index to use."]
+    pub callback: GEcallback,
+}
+
+extern "C" {
+
+    // Functions
+    pub fn GEaddDevice2(arg1: pGEDevDesc, arg2: *const ::std::os::raw::c_char);
+    pub fn GECap(dd: pGEDevDesc) -> SEXP;
+    pub fn GECircle(x: f64, y: f64, radius: f64, gc: pGEcontext, dd: pGEDevDesc);
+    pub fn GEcreateDevDesc(dev: pDevDesc) -> pGEDevDesc;
+    pub fn GEcurrentDevice() -> pGEDevDesc;
+    pub fn GEdeviceNumber(arg1: pGEDevDesc) -> ::std::os::raw::c_int;
+    pub fn GEExpressionHeight(expr: SEXP, gc: pGEcontext, dd: pGEDevDesc) -> f64;
+    pub fn GEExpressionMetric(
+        expr: SEXP,
+        gc: pGEcontext,
+        ascent: *mut f64,
+        descent: *mut f64,
+        width: *mut f64,
+        dd: pGEDevDesc,
+    );
+    pub fn GEExpressionWidth(expr: SEXP, gc: pGEcontext, dd: pGEDevDesc) -> f64;
+    pub fn GEfromDeviceHeight(value: f64, to: GEUnit, dd: pGEDevDesc) -> f64;
+    pub fn GEfromDeviceWidth(value: f64, to: GEUnit, dd: pGEDevDesc) -> f64;
+    pub fn GEfromDeviceX(value: f64, to: GEUnit, dd: pGEDevDesc) -> f64;
+    pub fn GEfromDeviceY(value: f64, to: GEUnit, dd: pGEDevDesc) -> f64;
+    pub fn GEgetDevice(arg1: ::std::os::raw::c_int) -> pGEDevDesc;
+    pub fn GEinitDisplayList(dd: pGEDevDesc);
+    pub fn GELine(x1: f64, y1: f64, x2: f64, y2: f64, gc: pGEcontext, dd: pGEDevDesc);
+    pub fn GEMathText(
+        x: f64,
+        y: f64,
+        expr: SEXP,
+        xc: f64,
+        yc: f64,
+        rot: f64,
+        gc: pGEcontext,
+        dd: pGEDevDesc,
+    );
+    pub fn GEMetricInfo(
+        c: ::std::os::raw::c_int,
+        gc: pGEcontext,
+        ascent: *mut f64,
+        descent: *mut f64,
+        width: *mut f64,
+        dd: pGEDevDesc,
+    );
+    pub fn GEMode(mode: ::std::os::raw::c_int, dd: pGEDevDesc);
+    pub fn GENewPage(gc: pGEcontext, dd: pGEDevDesc);
+    pub fn GEPath(
+        x: *mut f64,
+        y: *mut f64,
+        npoly: ::std::os::raw::c_int,
+        nper: *mut ::std::os::raw::c_int,
+        winding: Rboolean,
+        gc: pGEcontext,
+        dd: pGEDevDesc,
+    );
+    pub fn GEPolygon(
+        n: ::std::os::raw::c_int,
+        x: *mut f64,
+        y: *mut f64,
+        gc: pGEcontext,
+        dd: pGEDevDesc,
+    );
+    pub fn GEPolyline(
+        n: ::std::os::raw::c_int,
+        x: *mut f64,
+        y: *mut f64,
+        gc: pGEcontext,
+        dd: pGEDevDesc,
+    );
+    pub fn GERaster(
+        raster: *mut ::std::os::raw::c_uint,
+        w: ::std::os::raw::c_int,
+        h: ::std::os::raw::c_int,
+        x: f64,
+        y: f64,
+        width: f64,
+        height: f64,
+        rot: f64,
+        interpolate: Rboolean,
+        gc: pGEcontext,
+        dd: pGEDevDesc,
+    );
+    pub fn GERect(x0: f64, y0: f64, x1: f64, y1: f64, gc: pGEcontext, dd: pGEDevDesc);
+    pub fn GESetClip(x1: f64, y1: f64, x2: f64, y2: f64, dd: pGEDevDesc);
+    pub fn GEStrHeight(
+        str_: *const ::std::os::raw::c_char,
+        enc: cetype_t,
+        gc: pGEcontext,
+        dd: pGEDevDesc,
+    ) -> f64;
+    pub fn GEStrMetric(
+        str_: *const ::std::os::raw::c_char,
+        enc: cetype_t,
+        gc: pGEcontext,
+        ascent: *mut f64,
+        descent: *mut f64,
+        width: *mut f64,
+        dd: pGEDevDesc,
+    );
+    pub fn GEStrWidth(
+        str_: *const ::std::os::raw::c_char,
+        enc: cetype_t,
+        gc: pGEcontext,
+        dd: pGEDevDesc,
+    ) -> f64;
+    pub fn GESymbol(
+        x: f64,
+        y: f64,
+        pch: ::std::os::raw::c_int,
+        size: f64,
+        gc: pGEcontext,
+        dd: pGEDevDesc,
+    );
+    pub fn GEText(
+        x: f64,
+        y: f64,
+        str_: *const ::std::os::raw::c_char,
+        enc: cetype_t,
+        xc: f64,
+        yc: f64,
+        rot: f64,
+        gc: pGEcontext,
+        dd: pGEDevDesc,
+    );
+    pub fn GEtoDeviceHeight(value: f64, from: GEUnit, dd: pGEDevDesc) -> f64;
+    pub fn GEtoDeviceWidth(value: f64, from: GEUnit, dd: pGEDevDesc) -> f64;
+    pub fn GEtoDeviceX(value: f64, from: GEUnit, dd: pGEDevDesc) -> f64;
+    pub fn GEtoDeviceY(value: f64, from: GEUnit, dd: pGEDevDesc) -> f64;
+    pub fn R_CheckDeviceAvailable();
+    pub fn R_GE_checkVersionOrDie(version: ::std::os::raw::c_int);
+}
diff --git a/extendr-ffi/src/lib.rs b/extendr-ffi/src/lib.rs
new file mode 100644
index 000000000..cc4e5ec1c
--- /dev/null
+++ b/extendr-ffi/src/lib.rs
@@ -0,0 +1,606 @@
+//! Low level bindings to R's C API
+//!
+//! `extendr-ffi` powers the `extendr-api` library.
+#![allow(non_upper_case_globals)]
+#![allow(non_camel_case_types)]
+#![allow(non_snake_case)]
+#![allow(improper_ctypes)]
+
+pub mod graphics;
+pub use graphics::*;
+pub mod altrep;
+pub use altrep::*;
+mod symbols;
+pub use symbols::*;
+pub mod backports;
+pub use backports::*;
+
+#[cfg(feature = "non-api")]
+mod non_api;
+#[cfg(feature = "non-api")]
+pub use non_api::*;
+
+#[non_exhaustive]
+#[repr(transparent)]
+#[derive(Debug)]
+pub struct SEXPREC(std::ffi::c_void);
+
+extern "C" {
+    /// Returns the [SEXPTYPE] of `x`
+    pub fn TYPEOF(x: SEXP) -> SEXPTYPE;
+}
+
+#[repr(u32)]
+#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
+/// A C type with enumeration constants FALSE and TRUE defined.
+pub enum Rboolean {
+    #[doc = ", MAYBE"]
+    FALSE = 0,
+    #[doc = ", MAYBE"]
+    TRUE = 1,
+}
+
+#[doc = "These are very similar to those in Rdynpriv.h,\nbut we maintain them separately to give us more freedom to do\nsome computations on the internal versions that are derived from\nthese definitions."]
+#[repr(C)]
+#[derive(Debug, Copy, Clone)]
+pub struct R_CMethodDef {
+    pub name: *const ::std::os::raw::c_char,
+    pub fun: DL_FUNC,
+    pub numArgs: ::std::os::raw::c_int,
+    pub types: *mut R_NativePrimitiveArgType,
+}
+pub type R_NativePrimitiveArgType = ::std::os::raw::c_uint;
+pub type R_ExternalMethodDef = R_CallMethodDef;
+pub type R_FortranMethodDef = R_CMethodDef;
+
+pub type Rbyte = ::std::os::raw::c_uchar;
+#[doc = "type for length of (standard, not long) vectors etc"]
+pub type R_len_t = ::std::os::raw::c_int;
+#[repr(u32)]
+/// Enum of possible SEXP types
+#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
+pub enum SEXPTYPE {
+    #[doc = "nil = NULL"]
+    NILSXP = 0,
+    #[doc = "symbols"]
+    SYMSXP = 1,
+    #[doc = "lists of dotted pairs"]
+    LISTSXP = 2,
+    #[doc = "closures"]
+    CLOSXP = 3,
+    #[doc = "environments"]
+    ENVSXP = 4,
+    #[doc = "promises: \\[un\\]evaluated closure arguments"]
+    PROMSXP = 5,
+    #[doc = "language constructs (special lists)"]
+    LANGSXP = 6,
+    #[doc = "special forms"]
+    SPECIALSXP = 7,
+    #[doc = "builtin non-special forms"]
+    BUILTINSXP = 8,
+    #[doc = "\"scalar\" string type (internal only)"]
+    CHARSXP = 9,
+    #[doc = "logical vectors"]
+    LGLSXP = 10,
+    #[doc = "integer vectors"]
+    INTSXP = 13,
+    #[doc = "real variables"]
+    REALSXP = 14,
+    #[doc = "complex variables"]
+    CPLXSXP = 15,
+    #[doc = "string vectors"]
+    STRSXP = 16,
+    #[doc = "dot-dot-dot object"]
+    DOTSXP = 17,
+    #[doc = "make \"any\" args work"]
+    ANYSXP = 18,
+    #[doc = "generic vectors"]
+    VECSXP = 19,
+    #[doc = "expressions vectors"]
+    EXPRSXP = 20,
+    #[doc = "byte code"]
+    BCODESXP = 21,
+    #[doc = "external pointer"]
+    EXTPTRSXP = 22,
+    #[doc = "weak reference"]
+    WEAKREFSXP = 23,
+    #[doc = "raw bytes"]
+    RAWSXP = 24,
+    #[cfg(not(r_4_4))]
+    S4SXP = 25,
+    #[cfg(r_4_4)]
+    #[doc = "S4 non-vector"]
+    OBJSXP = 25,
+    #[doc = "fresh node created in new page"]
+    NEWSXP = 30,
+    #[doc = "node released by GC"]
+    FREESXP = 31,
+    #[doc = "Closure or Builtin"]
+    FUNSXP = 99,
+}
+pub type SEXP = *mut SEXPREC;
+
+#[repr(u32)]
+#[doc = "cetype_t is an identifier reseved by POSIX, but it is\nwell established as public.  Could remap by a #define though"]
+#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
+pub enum cetype_t {
+    CE_NATIVE = 0,
+    CE_UTF8 = 1,
+    CE_LATIN1 = 2,
+    CE_BYTES = 3,
+    CE_SYMBOL = 5,
+    CE_ANY = 99,
+}
+#[doc = "Finalization interface"]
+pub type R_CFinalizer_t = ::std::option::Option<unsafe extern "C" fn(arg1: SEXP)>;
+pub type Int32 = ::std::os::raw::c_uint;
+#[doc = "R 4.3 redefined `Rcomplex` to a union for compatibility with Fortran.\n But the old definition is compatible both the union version\n and the struct version.\n See: <https://github.com/extendr/extendr/issues/524>\n <div rustbindgen replaces=\"Rcomplex\"></div>"]
+#[repr(C)]
+#[derive(Debug, Copy, Clone)]
+pub struct Rcomplex {
+    pub r: f64,
+    pub i: f64,
+}
+
+#[doc = "R_xlen_t is defined as int on 32-bit platforms, and\n that confuses Rust. Keeping it always as ptrdiff_t works\n fine even on 32-bit.\n <div rustbindgen replaces=\"R_xlen_t\"></div>"]
+pub type R_xlen_t = isize;
+
+#[repr(C)]
+#[derive(Debug, Copy, Clone)]
+pub struct _DllInfo {
+    _unused: [u8; 0],
+}
+pub type DllInfo = _DllInfo;
+
+#[repr(u32)]
+#[doc = "PARSE_NULL will not be returned by R_ParseVector"]
+#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
+pub enum ParseStatus {
+    PARSE_NULL = 0,
+    PARSE_OK = 1,
+    PARSE_INCOMPLETE = 2,
+    PARSE_ERROR = 3,
+    PARSE_EOF = 4,
+}
+
+#[doc = "Called with a variable argument set after casting to a compatible\nfunction pointer."]
+pub type DL_FUNC = ::std::option::Option<unsafe extern "C" fn() -> *mut ::std::os::raw::c_void>;
+
+#[repr(C)]
+#[derive(Debug, Copy, Clone)]
+pub struct R_CallMethodDef {
+    pub name: *const ::std::os::raw::c_char,
+    pub fun: DL_FUNC,
+    pub numArgs: ::std::os::raw::c_int,
+}
+
+pub type R_outpstream_t = *mut R_outpstream_st;
+#[repr(C)]
+#[derive(Debug, Copy, Clone)]
+pub struct R_outpstream_st {
+    pub data: R_pstream_data_t,
+    pub type_: R_pstream_format_t,
+    pub version: ::std::os::raw::c_int,
+    pub OutChar: ::std::option::Option<
+        unsafe extern "C" fn(arg1: R_outpstream_t, arg2: ::std::os::raw::c_int),
+    >,
+    pub OutBytes: ::std::option::Option<
+        unsafe extern "C" fn(
+            arg1: R_outpstream_t,
+            arg2: *mut ::std::os::raw::c_void,
+            arg3: ::std::os::raw::c_int,
+        ),
+    >,
+    pub OutPersistHookFunc:
+        ::std::option::Option<unsafe extern "C" fn(arg1: SEXP, arg2: SEXP) -> SEXP>,
+    pub OutPersistHookData: SEXP,
+}
+
+pub type R_pstream_data_t = *mut ::std::os::raw::c_void;
+#[repr(u32)]
+#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
+pub enum R_pstream_format_t {
+    R_pstream_any_format = 0,
+    R_pstream_ascii_format = 1,
+    R_pstream_binary_format = 2,
+    R_pstream_xdr_format = 3,
+    R_pstream_asciihex_format = 4,
+}
+pub type R_inpstream_t = *mut R_inpstream_st;
+#[repr(C)]
+#[derive(Debug, Copy, Clone)]
+pub struct R_inpstream_st {
+    pub data: R_pstream_data_t,
+    pub type_: R_pstream_format_t,
+    pub InChar:
+        ::std::option::Option<unsafe extern "C" fn(arg1: R_inpstream_t) -> ::std::os::raw::c_int>,
+    pub InBytes: ::std::option::Option<
+        unsafe extern "C" fn(
+            arg1: R_inpstream_t,
+            arg2: *mut ::std::os::raw::c_void,
+            arg3: ::std::os::raw::c_int,
+        ),
+    >,
+    pub InPersistHookFunc:
+        ::std::option::Option<unsafe extern "C" fn(arg1: SEXP, arg2: SEXP) -> SEXP>,
+    pub InPersistHookData: SEXP,
+    pub native_encoding: [::std::os::raw::c_char; 64usize],
+    pub nat2nat_obj: *mut ::std::os::raw::c_void,
+    pub nat2utf8_obj: *mut ::std::os::raw::c_void,
+}
+
+extern "C" {
+    #[doc = "IEEE NaN"]
+    pub static mut R_NaN: f64;
+    #[doc = "IEEE Inf"]
+    pub static mut R_PosInf: f64;
+    #[doc = "IEEE -Inf"]
+    pub static mut R_NegInf: f64;
+    #[doc = "NA_REAL: IEEE"]
+    pub static mut R_NaReal: f64;
+    #[doc = "NA_INTEGER:= INT_MIN currently"]
+    pub static mut R_NaInt: ::std::os::raw::c_int;
+    #[doc = "NA_STRING is a SEXP, so defined in Rinternals.h"]
+    pub fn R_IsNA(arg1: f64) -> ::std::os::raw::c_int;
+    pub fn R_IsNaN(arg1: f64) -> ::std::os::raw::c_int;
+    pub fn R_finite(arg1: f64) -> ::std::os::raw::c_int;
+    pub fn Rprintf(arg1: *const ::std::os::raw::c_char, ...);
+    pub fn REprintf(arg1: *const ::std::os::raw::c_char, ...);
+    pub fn CAR(e: SEXP) -> SEXP;
+    pub fn CDR(e: SEXP) -> SEXP;
+    pub fn COMPLEX(x: SEXP) -> *mut Rcomplex;
+    pub fn COMPLEX_GET_REGION(sx: SEXP, i: R_xlen_t, n: R_xlen_t, buf: *mut Rcomplex) -> R_xlen_t;
+    pub fn DATAPTR(x: SEXP) -> *mut ::std::os::raw::c_void;
+    pub fn DATAPTR_RO(x: SEXP) -> *const ::std::os::raw::c_void;
+    pub fn DATAPTR_OR_NULL(x: SEXP) -> *const ::std::os::raw::c_void;
+    pub fn GetRNGstate();
+    pub fn PutRNGstate();
+    pub fn INTEGER(x: SEXP) -> *mut ::std::os::raw::c_int;
+    pub fn LOGICAL(x: SEXP) -> *mut ::std::os::raw::c_int;
+    pub fn LENGTH(x: SEXP) -> ::std::os::raw::c_int;
+    pub fn XLENGTH(x: SEXP) -> R_xlen_t;
+    pub fn RAW(x: SEXP) -> *mut Rbyte;
+    pub fn REAL(x: SEXP) -> *mut f64;
+    pub fn INTEGER_GET_REGION(
+        sx: SEXP,
+        i: R_xlen_t,
+        n: R_xlen_t,
+        buf: *mut ::std::os::raw::c_int,
+    ) -> R_xlen_t;
+    pub fn INTEGER_IS_SORTED(x: SEXP) -> ::std::os::raw::c_int;
+    pub fn INTEGER_NO_NA(x: SEXP) -> ::std::os::raw::c_int;
+    pub fn REAL_IS_SORTED(x: SEXP) -> ::std::os::raw::c_int;
+    pub fn REAL_NO_NA(x: SEXP) -> ::std::os::raw::c_int;
+    pub fn STRING_IS_SORTED(x: SEXP) -> ::std::os::raw::c_int;
+    pub fn STRING_NO_NA(x: SEXP) -> ::std::os::raw::c_int;
+    pub fn LOGICAL_GET_REGION(
+        sx: SEXP,
+        i: R_xlen_t,
+        n: R_xlen_t,
+        buf: *mut ::std::os::raw::c_int,
+    ) -> R_xlen_t;
+    pub fn MARK_NOT_MUTABLE(x: SEXP);
+    pub fn PRINTNAME(x: SEXP) -> SEXP;
+    #[doc = "The nil object"]
+    pub static mut R_NilValue: SEXP;
+    #[doc = "The base environment; formerly R_NilValue"]
+    pub static mut R_BaseEnv: SEXP;
+    #[doc = "The (fake) namespace for base"]
+    pub static mut R_BaseNamespace: SEXP;
+    #[doc = "NA_STRING as a CHARSXP"]
+    pub static mut R_NaString: SEXP;
+    #[doc = "srcref related functions"]
+    pub fn R_CHAR(x: SEXP) -> *const ::std::os::raw::c_char;
+    pub fn R_CleanTempDir();
+    #[doc = "External pointer interface"]
+    pub fn R_MakeExternalPtr(p: *mut ::std::os::raw::c_void, tag: SEXP, prot: SEXP) -> SEXP;
+    pub fn R_ExternalPtrAddr(s: SEXP) -> *mut ::std::os::raw::c_void;
+    pub fn R_ExternalPtrTag(s: SEXP) -> SEXP;
+    pub fn R_ExternalPtrProtected(s: SEXP) -> SEXP;
+    pub fn R_ClearExternalPtr(s: SEXP);
+    pub fn R_SetExternalPtrAddr(s: SEXP, p: *mut ::std::os::raw::c_void);
+    pub fn R_SetExternalPtrTag(s: SEXP, tag: SEXP);
+    pub fn R_SetExternalPtrProtected(s: SEXP, p: SEXP);
+    pub fn R_compute_identical(arg1: SEXP, arg2: SEXP, arg3: ::std::os::raw::c_int) -> Rboolean;
+    #[doc = "C stack limit"]
+    pub static mut R_CStackLimit: usize;
+    pub fn R_do_slot(obj: SEXP, name: SEXP) -> SEXP;
+    pub fn R_do_slot_assign(obj: SEXP, name: SEXP, value: SEXP) -> SEXP;
+    #[doc = "An empty environment at the root of the\nenvironment tree"]
+    pub static mut R_EmptyEnv: SEXP;
+    pub fn R_forceSymbols(info: *mut DllInfo, value: Rboolean) -> Rboolean;
+    pub fn R_GetCurrentEnv() -> SEXP;
+    #[doc = "srcref related functions"]
+    pub fn R_GetCurrentSrcref(arg1: ::std::os::raw::c_int) -> SEXP;
+    pub fn R_GetSrcFilename(arg1: SEXP) -> SEXP;
+    #[doc = "The \"global\" environment"]
+    pub static mut R_GlobalEnv: SEXP;
+    pub fn R_has_slot(obj: SEXP, name: SEXP) -> ::std::os::raw::c_int;
+    pub fn R_IsNamespaceEnv(rho: SEXP) -> Rboolean;
+    pub fn R_IsPackageEnv(rho: SEXP) -> Rboolean;
+    pub fn R_MakeUnwindCont() -> SEXP;
+    #[doc = "Missing argument marker"]
+    pub static mut R_MissingArg: SEXP;
+    pub fn R_NamespaceEnvSpec(rho: SEXP) -> SEXP;
+    #[doc = "Registry for registered namespaces"]
+    pub static mut R_NamespaceRegistry: SEXP;
+    #[doc = "Environment and Binding Features"]
+    pub fn R_NewEnv(arg1: SEXP, arg2: ::std::os::raw::c_int, arg3: ::std::os::raw::c_int) -> SEXP;
+    pub fn R_PackageEnvName(rho: SEXP) -> SEXP;
+    pub fn R_ParseVector(
+        arg1: SEXP,
+        arg2: ::std::os::raw::c_int,
+        arg3: *mut ParseStatus,
+        arg4: SEXP,
+    ) -> SEXP;
+    #[doc = "preserve objects across GCs"]
+    pub fn R_PreserveObject(arg1: SEXP);
+    pub fn R_RegisterCFinalizerEx(s: SEXP, fun: R_CFinalizer_t, onexit: Rboolean);
+    pub fn R_registerRoutines(
+        info: *mut DllInfo,
+        croutines: *const R_CMethodDef,
+        callRoutines: *const R_CallMethodDef,
+        fortranRoutines: *const R_FortranMethodDef,
+        externalRoutines: *const R_ExternalMethodDef,
+    ) -> ::std::os::raw::c_int;
+    pub fn R_ReleaseObject(arg1: SEXP);
+    pub fn R_RunExitFinalizers();
+    pub fn R_Serialize(s: SEXP, ops: R_outpstream_t);
+    #[doc = "Current srcref, for debuggers"]
+    pub static mut R_Srcref: SEXP;
+    pub fn R_tryEval(arg1: SEXP, arg2: SEXP, arg3: *mut ::std::os::raw::c_int) -> SEXP;
+    pub fn R_tryEvalSilent(arg1: SEXP, arg2: SEXP, arg3: *mut ::std::os::raw::c_int) -> SEXP;
+    #[doc = "Unbound marker"]
+    pub static mut R_UnboundValue: SEXP;
+    pub fn R_unif_index(arg1: f64) -> f64;
+    pub fn R_Unserialize(ips: R_inpstream_t) -> SEXP;
+    pub fn R_UnwindProtect(
+        fun: ::std::option::Option<unsafe extern "C" fn(data: *mut ::std::os::raw::c_void) -> SEXP>,
+        data: *mut ::std::os::raw::c_void,
+        cleanfun: ::std::option::Option<
+            unsafe extern "C" fn(data: *mut ::std::os::raw::c_void, jump: Rboolean),
+        >,
+        cleandata: *mut ::std::os::raw::c_void,
+        cont: SEXP,
+    ) -> SEXP;
+    pub fn R_useDynamicSymbols(info: *mut DllInfo, value: Rboolean) -> Rboolean;
+    pub fn RAW_GET_REGION(sx: SEXP, i: R_xlen_t, n: R_xlen_t, buf: *mut Rbyte) -> R_xlen_t;
+    pub fn REAL_GET_REGION(sx: SEXP, i: R_xlen_t, n: R_xlen_t, buf: *mut f64) -> R_xlen_t;
+    pub fn Rf_allocMatrix(
+        arg1: SEXPTYPE,
+        arg2: ::std::os::raw::c_int,
+        arg3: ::std::os::raw::c_int,
+    ) -> SEXP;
+    pub fn Rf_allocVector(arg1: SEXPTYPE, arg2: R_xlen_t) -> SEXP;
+    pub fn Rf_asChar(arg1: SEXP) -> SEXP;
+    pub fn Rf_asCharacterFactor(x: SEXP) -> SEXP;
+    pub fn Rf_coerceVector(arg1: SEXP, arg2: SEXPTYPE) -> SEXP;
+    pub fn Rf_conformable(arg1: SEXP, arg2: SEXP) -> Rboolean;
+    pub fn Rf_cons(arg1: SEXP, arg2: SEXP) -> SEXP;
+    pub fn Rf_defineVar(arg1: SEXP, arg2: SEXP, arg3: SEXP);
+    pub fn Rf_dimgets(arg1: SEXP, arg2: SEXP) -> SEXP;
+    pub fn Rf_dimnamesgets(arg1: SEXP, arg2: SEXP) -> SEXP;
+    pub fn Rf_duplicate(arg1: SEXP) -> SEXP;
+    pub fn Rf_error(arg1: *const ::std::os::raw::c_char, ...) -> !;
+    pub fn Rf_findFun(arg1: SEXP, arg2: SEXP) -> SEXP;
+    pub fn Rf_GetArrayDimnames(arg1: SEXP) -> SEXP;
+    pub fn Rf_getAttrib(arg1: SEXP, arg2: SEXP) -> SEXP;
+    pub fn Rf_GetColNames(arg1: SEXP) -> SEXP;
+    pub fn Rf_GetRowNames(arg1: SEXP) -> SEXP;
+    pub fn Rf_initialize_R(
+        ac: ::std::os::raw::c_int,
+        av: *mut *mut ::std::os::raw::c_char,
+    ) -> ::std::os::raw::c_int;
+    pub fn Rf_install(arg1: *const ::std::os::raw::c_char) -> SEXP;
+    pub fn Rf_isArray(arg1: SEXP) -> Rboolean;
+    pub fn Rf_isComplex(s: SEXP) -> Rboolean;
+    pub fn Rf_isEnvironment(s: SEXP) -> Rboolean;
+    pub fn Rf_isExpression(s: SEXP) -> Rboolean;
+    pub fn Rf_isFactor(arg1: SEXP) -> Rboolean;
+    pub fn Rf_isFrame(arg1: SEXP) -> Rboolean;
+    pub fn Rf_isFunction(arg1: SEXP) -> Rboolean;
+    pub fn Rf_isInteger(arg1: SEXP) -> Rboolean;
+    pub fn Rf_isLanguage(arg1: SEXP) -> Rboolean;
+    pub fn Rf_isList(arg1: SEXP) -> Rboolean;
+    pub fn Rf_isLogical(s: SEXP) -> Rboolean;
+    pub fn Rf_isMatrix(arg1: SEXP) -> Rboolean;
+    pub fn Rf_isNewList(arg1: SEXP) -> Rboolean;
+    pub fn Rf_isNull(s: SEXP) -> Rboolean;
+    pub fn Rf_isNumber(arg1: SEXP) -> Rboolean;
+    pub fn Rf_isObject(s: SEXP) -> Rboolean;
+    pub fn Rf_isPrimitive(arg1: SEXP) -> Rboolean;
+    pub fn Rf_isReal(s: SEXP) -> Rboolean;
+    pub fn Rf_isString(s: SEXP) -> Rboolean;
+    pub fn Rf_isSymbol(s: SEXP) -> Rboolean;
+    pub fn Rf_isTs(arg1: SEXP) -> Rboolean;
+    pub fn Rf_isUserBinop(arg1: SEXP) -> Rboolean;
+    pub fn Rf_isVector(arg1: SEXP) -> Rboolean;
+    pub fn Rf_isVectorAtomic(arg1: SEXP) -> Rboolean;
+    pub fn Rf_isVectorizable(arg1: SEXP) -> Rboolean;
+    pub fn Rf_isVectorList(arg1: SEXP) -> Rboolean;
+    pub fn Rf_lang1(arg1: SEXP) -> SEXP;
+    pub fn Rf_lcons(arg1: SEXP, arg2: SEXP) -> SEXP;
+    pub fn Rf_mkCharLenCE(
+        arg1: *const ::std::os::raw::c_char,
+        arg2: ::std::os::raw::c_int,
+        arg3: cetype_t,
+    ) -> SEXP;
+    pub fn Rf_namesgets(arg1: SEXP, arg2: SEXP) -> SEXP;
+    pub fn Rf_ncols(arg1: SEXP) -> ::std::os::raw::c_int;
+    pub fn Rf_NoDevices() -> ::std::os::raw::c_int;
+    pub fn Rf_nrows(arg1: SEXP) -> ::std::os::raw::c_int;
+    pub fn Rf_NumDevices() -> ::std::os::raw::c_int;
+    pub fn Rf_PairToVectorList(x: SEXP) -> SEXP;
+    pub fn Rf_PrintValue(arg1: SEXP);
+    pub fn Rf_protect(arg1: SEXP) -> SEXP;
+    pub fn Rf_runif(arg1: f64, arg2: f64) -> f64;
+    pub fn Rf_ScalarInteger(arg1: ::std::os::raw::c_int) -> SEXP;
+    pub fn Rf_setAttrib(arg1: SEXP, arg2: SEXP, arg3: SEXP) -> SEXP;
+    pub fn Rf_unprotect(arg1: ::std::os::raw::c_int);
+    pub fn Rf_VectorToPairList(x: SEXP) -> SEXP;
+    pub fn Rf_xlength(arg1: SEXP) -> R_xlen_t;
+    pub fn Rf_xlengthgets(arg1: SEXP, arg2: R_xlen_t) -> SEXP;
+    pub fn SET_ATTRIB(x: SEXP, v: SEXP);
+    pub fn SET_INTEGER_ELT(x: SEXP, i: R_xlen_t, v: ::std::os::raw::c_int);
+    pub fn SET_OBJECT(x: SEXP, v: ::std::os::raw::c_int);
+    pub fn SET_REAL_ELT(x: SEXP, i: R_xlen_t, v: f64);
+    pub fn SET_STRING_ELT(x: SEXP, i: R_xlen_t, v: SEXP);
+    pub fn SET_TAG(x: SEXP, y: SEXP);
+    pub fn SET_VECTOR_ELT(x: SEXP, i: R_xlen_t, v: SEXP) -> SEXP;
+    pub fn SETCDR(x: SEXP, y: SEXP) -> SEXP;
+    pub fn setup_Rmainloop();
+    pub fn REAL_ELT(x: SEXP, i: R_xlen_t) -> f64;
+    pub fn COMPLEX_ELT(x: SEXP, i: R_xlen_t) -> Rcomplex;
+    pub fn INTEGER_ELT(x: SEXP, i: R_xlen_t) -> ::std::os::raw::c_int;
+    pub fn STRING_ELT(x: SEXP, i: R_xlen_t) -> SEXP;
+    pub fn LOGICAL_ELT(x: SEXP, i: R_xlen_t) -> ::std::os::raw::c_int;
+    pub fn STRING_PTR_RO(x: SEXP) -> *const SEXP;
+    pub fn TAG(e: SEXP) -> SEXP;
+    pub fn VECTOR_ELT(x: SEXP, i: R_xlen_t) -> SEXP;
+    pub fn SETLEVELS(x: SEXP, v: ::std::os::raw::c_int) -> ::std::os::raw::c_int;
+    // pub fn Rf_isS4(arg1: SEXP) -> Rboolean;
+
+}
+
+pub unsafe fn Rf_isS4(arg1: SEXP) -> Rboolean {
+    unsafe {
+        if secret::Rf_isS4_original(arg1) == 0 {
+            Rboolean::FALSE
+        } else {
+            Rboolean::TRUE
+        }
+    }
+}
+
+mod secret {
+    use super::*;
+    extern "C" {
+        #[link_name = "Rf_isS4"]
+        pub fn Rf_isS4_original(arg1: SEXP) -> u32;
+    }
+}
+
+impl From<Rboolean> for bool {
+    fn from(value: Rboolean) -> Self {
+        match value {
+            Rboolean::FALSE => false,
+            Rboolean::TRUE => true,
+        }
+    }
+}
+
+impl From<bool> for Rboolean {
+    fn from(value: bool) -> Self {
+        match value {
+            true => Rboolean::TRUE,
+            false => Rboolean::FALSE,
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::os::raw;
+
+    // Generate constant static strings.
+    // Much more efficient than CString.
+    // Generates asciiz.
+    macro_rules! cstr {
+        ($s: expr) => {
+            concat!($s, "\0").as_ptr() as *const raw::c_char
+        };
+    }
+
+    // Generate mutable static strings.
+    // Much more efficient than CString.
+    // Generates asciiz.
+    macro_rules! cstr_mut {
+        ($s: expr) => {
+            concat!($s, "\0").as_ptr() as *mut raw::c_char
+        };
+    }
+
+    // Thanks to @qinwf and @scottmmjackson for showing the way here.
+    fn start_R() {
+        unsafe {
+            if std::env::var("R_HOME").is_err() {
+                // env! gets the build-time R_HOME made in build.rs
+                std::env::set_var("R_HOME", env!("R_HOME"));
+            }
+
+            // Due to Rf_initEmbeddedR using __libc_stack_end
+            // We can't call Rf_initEmbeddedR.
+            // Instead we must follow rustr's example and call the parts.
+
+            //let res = unsafe { Rf_initEmbeddedR(1, args.as_mut_ptr()) };
+            if cfg!(target_os = "windows") && cfg!(target_arch = "x86") {
+                Rf_initialize_R(
+                    4,
+                    [
+                        cstr_mut!("R"),
+                        cstr_mut!("--arch=i386"),
+                        cstr_mut!("--slave"),
+                        cstr_mut!("--no-save"),
+                    ]
+                    .as_mut_ptr(),
+                );
+            } else {
+                Rf_initialize_R(
+                    3,
+                    [cstr_mut!("R"), cstr_mut!("--slave"), cstr_mut!("--no-save")].as_mut_ptr(),
+                );
+            }
+
+            // In case you are curious.
+            // Maybe 8MB is a bit small.
+            // eprintln!("R_CStackLimit={:016x}", R_CStackLimit);
+
+            if cfg!(not(target_os = "windows")) {
+                R_CStackLimit = usize::max_value();
+            }
+
+            setup_Rmainloop();
+        }
+    }
+
+    // Run some R code. Check the result.
+    #[test]
+    fn test_eval() {
+        start_R();
+        use crate::*;
+        extern "C" {
+            pub fn R_ParseEvalString(arg1: *const ::std::os::raw::c_char, arg2: SEXP) -> SEXP;
+        }
+        unsafe {
+            println!("Evaluating 1");
+            let val = Rf_protect(R_ParseEvalString(cstr!("1"), R_NilValue));
+            Rf_PrintValue(val);
+            assert_eq!(TYPEOF(val), SEXPTYPE::REALSXP);
+            assert_eq!(*REAL(val), 1.);
+            Rf_unprotect(1);
+        }
+        // There is one pathological example of `Rf_is*` where `TRUE` is not 1,
+        // but 16. We show here that the casting is done as intended
+        unsafe {
+            let sexp = R_ParseEvalString(cstr!(r#"new("factor")"#), R_GlobalEnv);
+            Rf_protect(sexp);
+            Rf_PrintValue(sexp);
+
+            assert_eq!(Rboolean::TRUE, Rboolean::TRUE);
+            // assert_eq!(Rboolean::from(is_s4), Rboolean::TRUE);
+            // assert_eq!(
+            //     std::mem::discriminant(&Rf_isS4(sexp)),
+            //     std::mem::discriminant(&Rboolean::TRUE),
+            // );
+            assert!(<Rboolean as Into<bool>>::into(Rf_isS4(sexp)));
+            assert!(
+                (Rboolean::FALSE == Rf_isS4(sexp)) || (Rboolean::TRUE == Rf_isS4(sexp)),
+                "PartialEq implementation is broken"
+            );
+            assert!(Rboolean::TRUE == Rf_isS4(sexp));
+            assert_eq!(Rf_isS4(sexp), Rboolean::TRUE);
+            Rf_unprotect(1);
+        }
+    }
+}
diff --git a/extendr-ffi/src/non_api.rs b/extendr-ffi/src/non_api.rs
new file mode 100644
index 000000000..56964f33e
--- /dev/null
+++ b/extendr-ffi/src/non_api.rs
@@ -0,0 +1,27 @@
+// Functions used by the non-api feature of extendr
+use crate::{Rboolean, SEXP, SEXPTYPE};
+extern "C" {
+    #[doc = "Unbound marker"]
+    pub static mut R_UnboundValue: SEXP;
+    pub fn Rf_isValidString(arg1: SEXP) -> Rboolean;
+    pub fn Rf_isValidStringF(arg1: SEXP) -> Rboolean;
+    pub fn SYMVALUE(x: SEXP) -> SEXP;
+    pub fn PRSEEN(x: SEXP) -> ::std::os::raw::c_int;
+    pub fn SET_ENCLOS(x: SEXP, v: SEXP);
+    pub fn ENVFLAGS(x: SEXP) -> ::std::os::raw::c_int;
+    pub fn SET_ENVFLAGS(x: SEXP, v: ::std::os::raw::c_int);
+    pub fn ENCLOS(x: SEXP) -> SEXP;
+    pub fn HASHTAB(x: SEXP) -> SEXP;
+    pub fn FRAME(x: SEXP) -> SEXP;
+    pub fn Rf_allocSExp(arg1: SEXPTYPE) -> SEXP;
+    pub fn SET_FORMALS(x: SEXP, v: SEXP);
+    pub fn SET_BODY(x: SEXP, v: SEXP);
+    pub fn SET_CLOENV(x: SEXP, v: SEXP);
+    pub fn SET_PRCODE(x: SEXP, v: SEXP);
+    pub fn SET_PRENV(x: SEXP, v: SEXP);
+    pub fn SET_PRVALUE(x: SEXP, v: SEXP);
+    #[doc = "Promise Access Functions"]
+    pub fn PRCODE(x: SEXP) -> SEXP;
+    pub fn PRENV(x: SEXP) -> SEXP;
+    pub fn PRVALUE(x: SEXP) -> SEXP;
+}
diff --git a/extendr-ffi/src/symbols.rs b/extendr-ffi/src/symbols.rs
new file mode 100644
index 000000000..a838cc8a1
--- /dev/null
+++ b/extendr-ffi/src/symbols.rs
@@ -0,0 +1,82 @@
+//! Statics representing R Symbols
+use crate::SEXP;
+
+extern "C" {
+    #[doc = "\"base\""]
+    pub static mut R_BaseSymbol: SEXP;
+    #[doc = "\"\" as a STRSXP"]
+    pub static mut R_BlankScalarString: SEXP;
+    #[doc = "\"\" as a CHARSXP"]
+    pub static mut R_BlankString: SEXP;
+    #[doc = "\"{\""]
+    pub static mut R_BraceSymbol: SEXP;
+    #[doc = "\"\\[\\[\""]
+    pub static mut R_Bracket2Symbol: SEXP;
+    #[doc = "\"\\[\""]
+    pub static mut R_BracketSymbol: SEXP;
+    #[doc = "\"class\""]
+    pub static mut R_ClassSymbol: SEXP;
+    #[doc = "\".Device\""]
+    pub static mut R_DeviceSymbol: SEXP;
+    #[doc = "\"dimnames\""]
+    pub static mut R_DimNamesSymbol: SEXP;
+    #[doc = "\"dim\""]
+    pub static mut R_DimSymbol: SEXP;
+    #[doc = "\"$\""]
+    pub static mut R_DollarSymbol: SEXP;
+    #[doc = "\"...\""]
+    pub static mut R_DotsSymbol: SEXP;
+    #[doc = "\"::\""]
+    pub static mut R_DoubleColonSymbol: SEXP;
+    #[doc = "\"drop\""]
+    pub static mut R_DropSymbol: SEXP;
+    #[doc = "\"eval\""]
+    pub static mut R_EvalSymbol: SEXP;
+    #[doc = "\"function\""]
+    pub static mut R_FunctionSymbol: SEXP;
+    #[doc = "\".Last.value\""]
+    pub static mut R_LastvalueSymbol: SEXP;
+    #[doc = "\"levels\""]
+    pub static mut R_LevelsSymbol: SEXP;
+    #[doc = "\"mode\""]
+    pub static mut R_ModeSymbol: SEXP;
+    #[doc = "\"na.rm\""]
+    pub static mut R_NaRmSymbol: SEXP;
+    #[doc = "\"name\""]
+    pub static mut R_NameSymbol: SEXP;
+    #[doc = "\"names\""]
+    pub static mut R_NamesSymbol: SEXP;
+    #[doc = "\".__NAMESPACE__.\""]
+    pub static mut R_NamespaceEnvSymbol: SEXP;
+    #[doc = "\"package\""]
+    pub static mut R_PackageSymbol: SEXP;
+    #[doc = "\"previous\""]
+    pub static mut R_PreviousSymbol: SEXP;
+    #[doc = "\"quote\""]
+    pub static mut R_QuoteSymbol: SEXP;
+    #[doc = "\"row.names\""]
+    pub static mut R_RowNamesSymbol: SEXP;
+    #[doc = "\".Random.seed\""]
+    pub static mut R_SeedsSymbol: SEXP;
+    #[doc = "\"sort.list\""]
+    pub static mut R_SortListSymbol: SEXP;
+    #[doc = "\"source\""]
+    pub static mut R_SourceSymbol: SEXP;
+    #[doc = "\"spec\""]
+    pub static mut R_SpecSymbol: SEXP;
+    #[doc = "\":::\""]
+    pub static mut R_TripleColonSymbol: SEXP;
+    #[doc = "\"tsp\""]
+    pub static mut R_TspSymbol: SEXP;
+    #[doc = "\".defined\""]
+    pub static mut R_dot_defined: SEXP;
+    #[doc = "\".Generic\""]
+    pub static mut R_dot_Generic: SEXP;
+    #[doc = "\".Method\""]
+    pub static mut R_dot_Method: SEXP;
+    #[doc = "\".packageName\""]
+    pub static mut R_dot_packageName: SEXP;
+
+    #[doc = "\".target\""]
+    pub static mut R_dot_target: SEXP;
+}
diff --git a/extendr-macros/Cargo.toml b/extendr-macros/Cargo.toml
index 59dfe9220..2259f8204 100644
--- a/extendr-macros/Cargo.toml
+++ b/extendr-macros/Cargo.toml
@@ -18,5 +18,5 @@ proc-macro2 = { version = "1.0" }
 [dev-dependencies]
 extendr-api = { path = "../extendr-api" }
 extendr-engine = { path = "../extendr-engine" }
-libR-sys = { workspace = true }
+extendr-ffi = { workspace = true }
 trybuild = "1.0"
diff --git a/xtask/README.md b/xtask/README.md
index adfe392a9..fcb5187a3 100644
--- a/xtask/README.md
+++ b/xtask/README.md
@@ -1,7 +1,8 @@
 # `extendr` developer tools
 
-In order to perform integration test (with R), generate documentation, etc.,
-one can use this tool. In the future, `xtask` will be renamed `ci`, when it can
+`cargo extendr` cli is used to perform integration tests with R,
+generate documentation, and other tasks related to maintaining the
+extendr crates. In the future, `xtask` will be renamed `ci`, when it can
 perform all the tasks, that our custom CI scripts can do at the moment.
 
 ```shell
@@ -30,8 +31,6 @@ Options:
   -V, --version  Print version
 ```
 
-Let's describe some of the features listed above.
-
 ## `check-fmt`
 
 This checks if the rust code follows the specified `rustfmt`. It does not
@@ -67,11 +66,6 @@ Use `cargo extendr document` to regenerate the wrappers for the integration-test
 
 In the following are features that could be added to `xtask`.
 
-### Embed local `libR-sys`
-
-Clone `libR-sys` into `extendr/libR-sys`. Change the `extendr/Cargo.toml` to
-use the embedded `extendr/libR-sys`.
-
 ### Copy R-headers
 
 Copy R's C-headers to the current `extendr`-directory.


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index f01dd2550..79ce82f2b 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -16,7 +16,6 @@ on:
 # A special named parameter called `ActionName` can be used to set the name of current ci-cargo action,
 # used as `ci-cargo .... -ActionName "Called for documentation purposes"`.
 jobs:
-  # All tests under this job are run with pre-computed libR-sys bindings.
   tests_no_bindgen:
     runs-on: ${{ matrix.config.os }}
     name: ${{ matrix.config.os }} (R-${{ matrix.config.r }} rust-${{ matrix.config.rust-version }})
@@ -254,46 +253,3 @@ jobs:
         with:
           name: ${{ format('{0}-{1}-r{2}-rust-version{3}-{4}-results', runner.os, runner.arch, matrix.config.r, matrix.config.rust-version, matrix.config.id || strategy.job-index) }}
           path: extendrtests_check
-      # With https://github.com/extendr/rextendr/pull/31
-      # rextendr can be configured using environment variables.
-      # 'patch.crates_io' is used to point libraries to local copies of
-      # extendr-api and extendr-macros, so rextendr tests current version of extendr-*
-      - name: Prepare for R integration tests using {rextendr}
-        id: rextendr_check
-        env:
-          _R_CHECK_CRAN_INCOMING_REMOTE_: false
-        run: |
-          cat("::group::Preparing 'rextendr'\n")
-
-          patch.crates_io <-
-            paste(
-              paste0(
-                "extendr-api = { path = \"",
-                normalizePath(file.path(getwd(), "extendr-api"), winslash = "/"),
-                "\" }"),
-
-              paste0(
-                "extendr-macros = { path = \"",
-                normalizePath(file.path(getwd(), "extendr-macros"), winslash = "/"),
-                "\" }"),
-
-              # uncomment this line when we need to depend on the dev version of libR-sys
-              # 'libR-sys = { git = "https://github.com/extendr/libR-sys" }',
-
-              sep = ";")
-
-          patch.crates_io <- paste0("REXTENDR_PATCH_CRATES_IO=", patch.crates_io)
-
-          github_env_file <- Sys.getenv("GITHUB_ENV")
-          cat(patch.crates_io, file = github_env_file, append = TRUE)
-
-          cat("::endgroup::\n")
-        shell: Rscript {0}
-
-      - name: Run R integration tests using {rextendr}
-        uses: r-lib/actions/check-r-package@v2
-        with:
-          args: 'c("--no-manual", "--as-cran", "--force-multiarch")'
-          working-directory: "tests/rextendr"
-          check-dir: '"rextendr_check"'
-          error-on: '"${{ steps.error-on.outputs.level }}"'
diff --git a/extendr-api/tests/from_iterator_tests.rs b/extendr-api/tests/from_iterator_tests.rs
index 0ba0f42bf..61614504c 100644
--- a/extendr-api/tests/from_iterator_tests.rs
+++ b/extendr-api/tests/from_iterator_tests.rs
@@ -47,7 +47,7 @@ fn test_with_gc_torture_small() {
 #[test]
 fn test_with_gc_torture_large() {
     test!(
-        let x = [0_f64; 150].map(|_|single_threaded(||unsafe {libR_sys::Rf_runif(0., 100.)}));
+        let x = [0_f64; 150].map(|_|single_threaded(||unsafe {extendr_ffi::Rf_runif(0., 100.)}));
         R!("gctorture(on = TRUE)")?;
         let list: List = x.into_iter().collect();
         R!("gctorture(on = FALSE)")?;
diff --git a/extendr-api/tests/rng_tests.rs b/extendr-api/tests/rng_tests.rs
index 543eab0b1..3f26ace22 100644
--- a/extendr-api/tests/rng_tests.rs
+++ b/extendr-api/tests/rng_tests.rs
@@ -19,7 +19,7 @@ fn generate_big_random_vec() -> Vec<f64> {
     let n = 100;
     let param = 10e3;
     (0..n)
-        .map(|_| single_threaded(|| unsafe { libR_sys::R_unif_index(param) }))
+        .map(|_| single_threaded(|| unsafe { extendr_ffi::R_unif_index(param) }))
         .collect()
 }
 
diff --git a/tests/extendrtests/README.md b/tests/extendrtests/README.md
index c1bb60408..04d85529e 100644
--- a/tests/extendrtests/README.md
+++ b/tests/extendrtests/README.md
@@ -24,8 +24,6 @@ options(
             `extendr-api` = list(path = "extendr-api" |> normalizePath()),
             `extendr-macros` = list(path = "extendr-macros" |> normalizePath()),
             `extendr-engine` = list(path = "extendr-engine" |> normalizePath())
-            # uncomment this, if you have locally sourced libR-sys crate..
-            # ,`libR-sys` = list(path = "libR-sys" |> normalizePath())
         )
 )
 ```
diff --git a/tests/extendrtests/src/Makevars b/tests/extendrtests/src/Makevars
index a996ee0a5..3c18febbc 100644
--- a/tests/extendrtests/src/Makevars
+++ b/tests/extendrtests/src/Makevars
@@ -3,15 +3,16 @@ LIBDIR = $(TARGET_DIR)/debug
 STATLIB = $(LIBDIR)/libextendrtests.a
 PKG_LIBS = -L$(LIBDIR) -lextendrtests
 
-all: C_clean
+all: $(SHLIB) rust_clean
 
 $(SHLIB): $(STATLIB)
 
 $(STATLIB):
 	cargo build --quiet --lib --manifest-path=./rust/Cargo.toml --target-dir $(TARGET_DIR) --color=auto
 
-C_clean:
-	rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS)
+
+rust_clean:
+	rm -Rf $(CARGOTMP) $(TARGET_DIR)
 
 clean:
 	rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) rust/target
diff --git a/tests/extendrtests/src/rust/Cargo.toml b/tests/extendrtests/src/rust/Cargo.toml
index c17fffec4..1639643ca 100644
--- a/tests/extendrtests/src/rust/Cargo.toml
+++ b/tests/extendrtests/src/rust/Cargo.toml
@@ -37,7 +37,7 @@ faer = "0.20"
 ## CI overrides this path.
 extendr-api = { path = "../../../../extendr-api" }
 ## This allows to run `rcmdcheck` from `./tests/extendrtests/`
-# extendr-api = { path = "../../../../../../../../../extendr/extendr-api"}
+# extendr-api = { path = "../../../../../../../../../extendr/extendr-api" }
 
 
 ## Build against current extendr version on github. Not recommended
diff --git a/xtask/src/extendrtests/with_absolute_path.rs b/xtask/src/extendrtests/with_absolute_path.rs
index 706dc8c8a..1274e09f6 100644
--- a/xtask/src/extendrtests/with_absolute_path.rs
+++ b/xtask/src/extendrtests/with_absolute_path.rs
@@ -17,7 +17,7 @@ pub(crate) struct DocumentMutHandle<'a> {
     shell: &'a Shell,
 }
 
-impl<'a> Drop for DocumentMutHandle<'a> {
+impl Drop for DocumentMutHandle<'_> {
     fn drop(&mut self) {
         let _rust_folder = self.shell.push_dir(RUST_FOLDER_PATH);
         self.shell
@@ -46,18 +46,6 @@ pub(crate) fn swap_extendr_api_path(shell: &Shell) -> Result<DocumentMutHandle,
     replacement.entry("path").or_insert(item);
     *extendr_api_entry = Value::InlineTable(replacement);
 
-    #[allow(non_snake_case)]
-    let libR_sys_entry = get_libR_sys_entry(&mut cargo_toml);
-    #[allow(non_snake_case)]
-    if let Some(libR_sys_entry) = libR_sys_entry {
-        let mut replacement = InlineTable::new();
-        if let Some(replacement_path) = get_replacement_path_libR_sys(&current_path) {
-            let item = Value::from(replacement_path);
-            replacement.entry("path").or_insert(item);
-            *libR_sys_entry = Value::InlineTable(replacement);
-        }
-    }
-
     // save altered paths
     shell.write_file(CARGO_TOML, cargo_toml.to_string())?;
     Ok(DocumentMutHandle {
@@ -72,15 +60,6 @@ fn get_replacement_path_extendr_api(path: &Path) -> String {
     format!("{path}/extendr-api")
 }
 
-#[allow(non_snake_case)]
-fn get_replacement_path_libR_sys(path: &Path) -> Option<String> {
-    let path = path.adjust_for_r();
-    #[allow(non_snake_case)]
-    let libR_sys_path = format!("{path}/libR-sys");
-    let valid_path = std::path::Path::new(&libR_sys_path);
-    matches!(valid_path.try_exists(), Ok(true)).then_some(libR_sys_path)
-}
-
 fn get_extendr_api_entry(document: &mut DocumentMut) -> Option<&mut Value> {
     document
         .get_mut("patch")?
@@ -89,15 +68,6 @@ fn get_extendr_api_entry(document: &mut DocumentMut) -> Option<&mut Value> {
         .as_value_mut()
 }
 
-#[allow(non_snake_case)]
-fn get_libR_sys_entry(document: &mut DocumentMut) -> Option<&mut Value> {
-    document
-        .get_mut("patch")?
-        .get_mut("crates-io")?
-        .get_mut("libR-sys")?
-        .as_value_mut()
-}
-
 fn read_file_with_line_ending<P: AsRef<Path>>(
     shell: &Shell,
     path: P,