Repository: extendr/extendr
Pull request #393 (2022-03-10 10:49:19)

Derive for creating data frames
The goal of this PR is to make a derive for generating data frames.

Something like

```
#[derive(IntoDataframe)]
struct MyData {
   x: i32,
   y: String,
}

let dataframe : List = (0..100).map(|i| MyData { x: i, y: i.to_string() }).into_dataframe();
```


andy-thomason: Note that we can generate dataframes from structs or tuples.


Ilia-Kosenkov: I also +1 most of @multimeric 's comments as valid concerns.

Patch:
diff --git a/extendr-api/Cargo.toml b/extendr-api/Cargo.toml
index 5bbd32035..657c261bc 100644
--- a/extendr-api/Cargo.toml
+++ b/extendr-api/Cargo.toml
@@ -38,6 +38,8 @@ tests-minimal = ["libR-sys/use-bindgen"]
 # and require --test-threads=1, so we decided to exclude it from here (c.f. #378).
 tests = ["tests-minimal", "ndarray", "serde", "num-complex"]
 
+tests-graphics = ["tests-minimal", "graphics"]
+
 # Literally all features to test
 tests-all = ["tests", "graphics"]
 
diff --git a/extendr-api/src/error.rs b/extendr-api/src/error.rs
index 15ab910db..f464c951b 100644
--- a/extendr-api/src/error.rs
+++ b/extendr-api/src/error.rs
@@ -68,6 +68,7 @@ pub enum Error {
     ExpectedMatrix3D(Robj),
     ExpectedNumeric(Robj),
     ExpectedAltrep(Robj),
+    ExpectedDataframe(Robj),
 
     OutOfRange(Robj),
     MustNotBeNA(Robj),
@@ -135,6 +136,10 @@ impl std::fmt::Display for Error {
             Error::ExpectedMatrix3D(robj) => write!(f, "Expected Matrix3D, got {:?}", robj.rtype()),
             Error::ExpectedNumeric(robj) => write!(f, "Expected Numeric, got {:?}", robj.rtype()),
             Error::ExpectedAltrep(robj) => write!(f, "Expected Altrep, got {:?}", robj.rtype()),
+            Error::ExpectedDataframe(robj) => {
+                write!(f, "Expected Dataframe, got {:?}", robj.rtype())
+            }
+
             Error::OutOfRange(_robj) => write!(f, "Out of range."),
             Error::MustNotBeNA(_robj) => write!(f, "Must not be NA."),
             Error::ExpectedNonZeroLength(_robj) => write!(f, "Expected non zero length"),
diff --git a/extendr-api/src/graphics/device_driver.rs b/extendr-api/src/graphics/device_driver.rs
index 9da89eb23..6bd4e7c61 100644
--- a/extendr-api/src/graphics/device_driver.rs
+++ b/extendr-api/src/graphics/device_driver.rs
@@ -61,6 +61,11 @@ pub trait DeviceDriver: std::marker::Sized {
     /// operation, this can be set `false`.
     const USE_CAPTURE: bool = true;
 
+    /// Whether the device has a locator capability, i.e.,
+    /// reading the position of the graphics cursor when the mouse button is pressed.
+    /// It works with X11, windows and quartz devices.
+    const USE_LOCATOR: bool = true;
+
     /// Whether the device maintains a plot history. This corresponds to
     /// `displayListOn` in the underlying [DevDesc].
     const USE_PLOT_HISTORY: bool = false;
@@ -265,6 +270,17 @@ pub trait DeviceDriver: std::marker::Sized {
         0
     }
 
+    /// A callback function that returns the coords of the event
+    fn locator(&mut self, x: *mut f64, y: *mut f64, dd: DevDesc) -> bool {
+        true
+    }
+
+    /// A callback function for X11_eventHelper.
+    /// Argument `code` should, ideally, be of type c_int,
+    /// but compiler throws erors. It should be ok to use
+    /// i32 here.
+    fn eventHelper(&mut self, dd: DevDesc, code: i32) {}
+
     /// Create a [Device].
     fn create_device<T: DeviceDriver>(
         self,
@@ -587,6 +603,24 @@ pub trait DeviceDriver: std::marker::Sized {
             data.holdflush(*dd, level as _)
         }
 
+        unsafe extern "C" fn device_driver_locator<T: DeviceDriver>(
+            x: *mut f64,
+            y: *mut f64,
+            dd: pDevDesc,
+        ) -> Rboolean {
+            let data = ((*dd).deviceSpecific as *mut T).as_mut().unwrap();
+            if let Ok(success) = data.locator(x, y, *dd).try_into() {
+                success
+            } else {
+                false.into()
+            }
+        }
+
+        unsafe extern "C" fn device_driver_eventHelper<T: DeviceDriver>(dd: pDevDesc, code: c_int) {
+            let mut data = ((*dd).deviceSpecific as *mut T).read();
+            data.eventHelper(*dd, code);
+        }
+
         #[cfg(use_r_ge_version_14)]
         unsafe extern "C" fn device_driver_setPattern<T: DeviceDriver>(
             pattern: SEXP,
@@ -763,7 +797,7 @@ pub trait DeviceDriver: std::marker::Sized {
             };
             (*p_dev_desc).close = Some(device_driver_close::<T>);
             (*p_dev_desc).deactivate = Some(device_driver_deactivate::<T>);
-            (*p_dev_desc).locator = None; // TOD;
+            (*p_dev_desc).locator = Some(device_driver_locator::<T>); // TOD;
             (*p_dev_desc).line = Some(device_driver_line::<T>);
             (*p_dev_desc).metricInfo = Some(device_driver_char_metric::<T>);
             (*p_dev_desc).mode = Some(device_driver_mode::<T>);
@@ -818,7 +852,7 @@ pub trait DeviceDriver: std::marker::Sized {
             (*p_dev_desc).useRotatedTextInContour = 0;
 
             (*p_dev_desc).eventEnv = empty_env().get();
-            (*p_dev_desc).eventHelper = None;
+            (*p_dev_desc).eventHelper = Some(device_driver_eventHelper::<T>);
 
             (*p_dev_desc).holdflush = Some(device_driver_holdflush::<T>);
 
@@ -841,7 +875,11 @@ pub trait DeviceDriver: std::marker::Sized {
                 DevCapCapture::No as _
             };
 
-            (*p_dev_desc).haveLocator = DevCapLocator::Unset as _;
+            (*p_dev_desc).haveLocator = if <T>::USE_LOCATOR {
+                DevCapLocator::Yes as _
+            } else {
+                DevCapLocator::No as _
+            };
 
             // NOTE: Unlike the features that will be added in  Graphics API
             // version 15 (i.e. R 4.2), the features in API v13 & v14 (i.e. R
diff --git a/extendr-api/src/ownership.rs b/extendr-api/src/ownership.rs
index b51a16afd..759303cc5 100644
--- a/extendr-api/src/ownership.rs
+++ b/extendr-api/src/ownership.rs
@@ -13,8 +13,8 @@ use std::collections::hash_map::{Entry, HashMap};
 use std::sync::Mutex;
 
 use libR_sys::{
-    R_NilValue, R_PreserveObject, R_ReleaseObject, R_xlen_t, Rf_allocVector, LENGTH,
-    SET_VECTOR_ELT, SEXP, VECSXP, VECTOR_ELT,
+    R_NilValue, R_PreserveObject, R_ReleaseObject, R_xlen_t, Rf_allocVector, Rf_protect,
+    Rf_unprotect, LENGTH, SET_VECTOR_ELT, SEXP, VECSXP, VECTOR_ELT,
 };
 
 lazy_static! {
@@ -31,8 +31,8 @@ pub(crate) unsafe fn unprotect(sexp: SEXP) {
     own.unprotect(sexp);
 }
 
-pub const INITIAL_PRESERVATION_SIZE: usize = 25000;
-pub const EXTRA_PRESERVATION_SIZE: usize = 25000;
+pub const INITIAL_PRESERVATION_SIZE: usize = 100000;
+pub const EXTRA_PRESERVATION_SIZE: usize = 100000;
 
 struct Object {
     refcount: usize,
@@ -69,6 +69,8 @@ impl Ownership {
     }
 
     unsafe fn protect(&mut self, sexp: SEXP) {
+        Rf_protect(sexp);
+
         if self.cur_index == self.max_index {
             self.garbage_collect();
         }
@@ -100,6 +102,8 @@ impl Ownership {
                 vacant.insert(Object { refcount, index });
             }
         }
+
+        Rf_unprotect(1);
     }
 
     pub unsafe fn unprotect(&mut self, sexp: SEXP) {
@@ -154,7 +158,7 @@ impl Ownership {
     // Garbage collect the tracking structures.
     unsafe fn garbage_collect(&mut self) {
         // println!("garbage_collect {} {}", self.cur_index, self.max_index);
-        let new_size = self.cur_index + EXTRA_PRESERVATION_SIZE;
+        let new_size = self.cur_index * 2 + EXTRA_PRESERVATION_SIZE;
         let new_sexp = Rf_allocVector(VECSXP, new_size as R_xlen_t);
         R_PreserveObject(new_sexp);
         let old_sexp = self.preservation as SEXP;
diff --git a/extendr-api/src/prelude.rs b/extendr-api/src/prelude.rs
index d086205fa..858d96278 100644
--- a/extendr-api/src/prelude.rs
+++ b/extendr-api/src/prelude.rs
@@ -58,9 +58,9 @@ pub use super::thread_safety::{
 };
 
 pub use super::wrapper::{
-    Complexes, Doubles, EnvIter, Environment, Expressions, ExternalPtr, FromList, Function,
-    Integers, Language, List, ListIter, Logicals, Nullable, Pairlist, Primitive, Promise, Raw,
-    Rstr, Strings, Symbol,
+    Complexes, Dataframe, Doubles, EnvIter, Environment, Expressions, ExternalPtr, FromList,
+    Function, Integers, IntoDataFrame, Language, List, ListIter, Logicals, Nullable, Pairlist,
+    Primitive, Promise, Raw, Rstr, Strings, Symbol,
 };
 
 #[cfg(feature = "ndarray")]
@@ -69,7 +69,7 @@ pub use super::robj_ndarray::*;
 #[cfg(feature = "ndarray")]
 pub use ndarray::*;
 
-pub use extendr_macros::{call, extendr, extendr_module, pairlist, Rraw, R};
+pub use extendr_macros::{call, extendr, extendr_module, pairlist, IntoDataFrame, Rraw, R};
 
 pub use super::iter::StrIter;
 
diff --git a/extendr-api/src/robj/into_robj.rs b/extendr-api/src/robj/into_robj.rs
index ebd6f5d06..b18cdab9e 100644
--- a/extendr-api/src/robj/into_robj.rs
+++ b/extendr-api/src/robj/into_robj.rs
@@ -578,3 +578,24 @@ impl<'a> From<Vec<Robj>> for Robj {
         List::from_values(val.iter()).into()
     }
 }
+
+impl<'a> From<Vec<Rstr>> for Robj {
+    /// Convert a vector of Rstr into strings.
+    fn from(val: Vec<Rstr>) -> Self {
+        Strings::from_values(val.into_iter()).into()
+    }
+}
+
+impl<'a> From<Vec<Rint>> for Robj {
+    /// Convert a vector of Rint into integers.
+    fn from(val: Vec<Rint>) -> Self {
+        Integers::from_values(val.into_iter()).into()
+    }
+}
+
+impl<'a> From<Vec<Rfloat>> for Robj {
+    /// Convert a vector of Rfloat into doubles.
+    fn from(val: Vec<Rfloat>) -> Self {
+        Doubles::from_values(val.into_iter()).into()
+    }
+}
diff --git a/extendr-api/src/scalar/macros.rs b/extendr-api/src/scalar/macros.rs
index 812152de5..1a6707235 100644
--- a/extendr-api/src/scalar/macros.rs
+++ b/extendr-api/src/scalar/macros.rs
@@ -503,6 +503,13 @@ macro_rules! gen_from_primitive {
             }
         }
 
+        // Same but for references
+        impl From<&$type_prim> for $type {
+            fn from(v: &$type_prim) -> Self {
+                Self(*v)
+            }
+        }
+
         // The 'example usage' expands to...
         //
         // impl From<Option<i32>> for Rint {
@@ -523,6 +530,17 @@ macro_rules! gen_from_primitive {
                 }
             }
         }
+
+        // Same but for references
+        impl From<Option<&$type_prim>> for $type {
+            fn from(v: Option<&$type_prim>) -> Self {
+                if let Some(v) = v {
+                    v.into()
+                } else {
+                    $type::na()
+                }
+            }
+        }
     };
 }
 
diff --git a/extendr-api/src/wrapper/dataframe.rs b/extendr-api/src/wrapper/dataframe.rs
new file mode 100644
index 000000000..21125926b
--- /dev/null
+++ b/extendr-api/src/wrapper/dataframe.rs
@@ -0,0 +1,64 @@
+use super::*;
+
+pub trait IntoDataFrame<T> {
+    fn into_dataframe(self) -> Result<Dataframe<T>>;
+}
+
+#[derive(PartialEq, Clone)]
+pub struct Dataframe<T> {
+    pub(crate) robj: Robj,
+    marker: std::marker::PhantomData<T>,
+}
+
+impl<T> std::convert::TryFrom<&Robj> for Dataframe<T> {
+    type Error = Error;
+    fn try_from(robj: &Robj) -> Result<Self> {
+        // TODO: check type using derived trait.
+        if robj.is_list() && robj.inherits("data.frame") {
+            Ok(Dataframe {
+                robj: robj.clone(),
+                marker: std::marker::PhantomData,
+            })
+        } else {
+            Err(Error::ExpectedDataframe(robj.clone()))
+        }
+    }
+}
+
+impl<T> std::convert::TryFrom<Robj> for Dataframe<T> {
+    type Error = Error;
+    fn try_from(robj: Robj) -> Result<Self> {
+        (&robj).try_into()
+    }
+}
+
+impl<T> Dataframe<T> {
+    /// Use `#[derive(IntoDataFrame)]` to use this.
+    pub fn try_from_values<I: IntoDataFrame<T>>(iter: I) -> Result<Self> {
+        iter.into_dataframe()
+    }
+}
+
+impl<T> Attributes for Dataframe<T> {}
+
+impl<T> std::fmt::Debug for Dataframe<T>
+where
+    T: std::fmt::Debug,
+{
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "dataframe!({})",
+            self.as_list()
+                .unwrap()
+                .iter()
+                .map(|(k, v)| if !k.is_empty() {
+                    format!("{}={:?}", k, v)
+                } else {
+                    format!("{:?}", v)
+                })
+                .collect::<Vec<_>>()
+                .join(", ")
+        )
+    }
+}
diff --git a/extendr-api/src/wrapper/mod.rs b/extendr-api/src/wrapper/mod.rs
index 37663963e..9c99408ee 100644
--- a/extendr-api/src/wrapper/mod.rs
+++ b/extendr-api/src/wrapper/mod.rs
@@ -7,6 +7,7 @@ use libR_sys::*;
 
 pub mod altrep;
 pub mod complexes;
+pub mod dataframe;
 pub mod doubles;
 pub mod environment;
 pub mod expr;
@@ -34,6 +35,7 @@ pub use altrep::{
     AltrepImpl,
 };
 pub use complexes::Complexes;
+pub use dataframe::{Dataframe, IntoDataFrame};
 pub use doubles::Doubles;
 pub use environment::{EnvIter, Environment};
 pub use expr::Expressions;
@@ -126,7 +128,13 @@ macro_rules! make_conversions {
             }
         }
 
-        impl GetSexp for $typename {
+        make_getsexp!($typename, impl);
+    };
+}
+
+macro_rules! make_getsexp {
+    ($typename: ty, $($impl : tt)*) => {
+        $($impl)* GetSexp for $typename {
             unsafe fn get(&self) -> SEXP {
                 self.robj.get()
             }
@@ -143,22 +151,22 @@ macro_rules! make_conversions {
         // These traits all derive from GetSexp
 
         /// len() and is_empty()
-        impl Length for $typename {}
+        $($impl)* Length for $typename {}
 
         /// rtype() and rany()
-        impl Types for $typename {}
+        $($impl)* Types for $typename {}
 
         /// as_*()
-        impl Conversions for $typename {}
+        $($impl)* Conversions for $typename {}
 
         /// find_var() etc.
-        impl Rinternals for $typename {}
+        $($impl)* Rinternals for $typename {}
 
         /// as_typed_slice_raw() etc.
-        impl Slices for $typename {}
+        $($impl)* Slices for $typename {}
 
         /// dollar() etc.
-        impl Operators for $typename {}
+        $($impl)* Operators for $typename {}
     };
 }
 
@@ -226,6 +234,8 @@ make_conversions!(
 
 make_conversions!(Strings, ExpectedString, is_string, "Not a string vector");
 
+make_getsexp!(Dataframe<T>, impl<T>);
+
 // impl Deref for Integers {
 //     type Target = [Rint];
 
diff --git a/extendr-api/src/wrapper/strings.rs b/extendr-api/src/wrapper/strings.rs
index d08e2136b..6d54c9872 100644
--- a/extendr-api/src/wrapper/strings.rs
+++ b/extendr-api/src/wrapper/strings.rs
@@ -68,25 +68,22 @@ impl Strings {
         }
     }
 
-    /// This is a relatively expensive operation, so use a variable if using this in a loop.
+    /// Get an element in a string vector.
     pub fn elt(&self, i: usize) -> Rstr {
-        unsafe {
-            let sexp = if i >= self.len() {
-                R_NaString
-            } else {
-                STRING_ELT(self.get(), i as R_xlen_t)
-            };
-            Rstr {
-                robj: Robj::from_sexp(sexp),
-            }
+        if i >= self.len() {
+            Rstr::na()
+        } else {
+            Robj::from_sexp(unsafe { STRING_ELT(self.get(), i as R_xlen_t) })
+                .try_into()
+                .unwrap()
         }
     }
 
     /// Set a single element of this string vector.
-    pub fn set_elt<T: AsRef<str>>(&mut self, i: usize, e: T) {
+    pub fn set_elt(&mut self, i: usize, e: Rstr) {
         single_threaded(|| unsafe {
             if i < self.len() {
-                SET_STRING_ELT(self.robj.get(), i as isize, str_to_character(e.as_ref()));
+                SET_STRING_ELT(self.robj.get(), i as isize, e.get());
             }
         });
     }
diff --git a/extendr-macros/src/dataframe.rs b/extendr-macros/src/dataframe.rs
new file mode 100644
index 000000000..04e4f0839
--- /dev/null
+++ b/extendr-macros/src/dataframe.rs
@@ -0,0 +1,55 @@
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::{parse_macro_input, Data, DataStruct, DeriveInput};
+
+fn parse_struct(input: &DeriveInput, datastruct: &DataStruct) -> TokenStream {
+    let structname = &input.ident;
+    let mut a = Vec::new();
+    for f in &datastruct.fields {
+        a.push(f.ident.clone());
+    }
+    quote! {
+        impl IntoDataFrame<#structname> for Vec<#structname>
+        {
+            fn into_dataframe(self) -> Result<Dataframe<#structname>> {
+                #(let mut #a = Vec::with_capacity(self.len());)*
+                for val in self {
+                    #(#a.push(val.#a);)*
+                }
+                let caller = eval_string("data.frame")?;
+                let res = caller.call(Pairlist::from_pairs(&[
+                    #((stringify!(#a), extendr_api::robj::Robj::from(#a))),*
+                ]))?;
+                res.try_into()
+            }
+        }
+
+        impl<I> IntoDataFrame<#structname> for (I,)
+        where
+            I : ExactSizeIterator<Item=#structname>,
+        {
+            /// Thanks to RFC 2451, we need to wrap a generic iterator in a tuple!
+            fn into_dataframe(self) -> Result<Dataframe<#structname>> {
+                #(let mut #a = Vec::with_capacity(self.0.len());)*
+                for val in self.0 {
+                    #(#a.push(val.#a);)*
+                }
+                let caller = eval_string("data.frame")?;
+                let res = caller.call(Pairlist::from_pairs(&[
+                    #((stringify!(#a), extendr_api::robj::Robj::from(#a))),*
+                ]))?;
+                res.try_into()
+            }
+        }
+    }
+    .into()
+}
+
+pub fn derive_into_dataframe(item: TokenStream) -> TokenStream {
+    let input: DeriveInput = parse_macro_input!(item as DeriveInput);
+
+    match &input.data {
+        Data::Struct(datastruct) => parse_struct(&input, datastruct),
+        _ => quote!(compile_error("IntoDataFrame expected a struct.")).into(),
+    }
+}
diff --git a/extendr-macros/src/lib.rs b/extendr-macros/src/lib.rs
index 41ee11bfb..e57313b88 100644
--- a/extendr-macros/src/lib.rs
+++ b/extendr-macros/src/lib.rs
@@ -57,6 +57,7 @@
 #[allow(non_snake_case)]
 mod R;
 mod call;
+mod dataframe;
 mod extendr_function;
 mod extendr_impl;
 mod extendr_module;
@@ -238,3 +239,29 @@ pub fn derive_try_from_robj(item: TokenStream) -> TokenStream {
 pub fn derive_into_robj(item: TokenStream) -> TokenStream {
     list_struct::derive_into_robj(item)
 }
+
+/// Enable the construction of dataframes from arrays of structures.
+///
+/// # Example
+///
+/// ```ignore
+/// use extendr_api::prelude::*;
+///
+/// #[derive(Debug, IntoDataFrame)]
+/// struct MyStruct {
+///     x: i32,
+///     y: String,
+/// }
+///
+/// let v = vec![MyStruct { x: 0, y: "abc".into() }, MyStruct { x: 1, y: "xyz".into() }];
+/// let df = v.into_dataframe()?;
+///
+/// assert!(df.inherits("data.frame"));
+/// assert_eq!(df[0], r!([0, 1]));
+/// assert_eq!(df[1], r!(["abc", "xyz"]));
+/// ```
+
+#[proc_macro_derive(IntoDataFrame)]
+pub fn derive_into_dataframe(item: TokenStream) -> TokenStream {
+    dataframe::derive_into_dataframe(item)
+}


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 7c5206426..724364560 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -28,10 +28,11 @@ jobs:
         config:
           # Windows jobs with unspecific Rust architecture build for both i686 and x86_64 
           # R integration tests are also executed for both architectures
-          - {os: windows-latest, r: 'release', rust-version: 'stable-msvc'}
-          # TODO: remove this when we drop suppoort for R 4.1. Since R 4.2 uses
-          # a different toolchain than R 4.1, we need to test both.
-          - {os: windows-latest, r: 'devel',   rust-version: 'stable-msvc'}
+          - {os: windows-latest, r: 'release', rust-version: 'stable-gnu',  rtools-version: '42'}
+          - {os: windows-latest, r: 'devel',   rust-version: 'stable-gnu',  rtools-version: '42'}
+          # For R < 4.2, the MSVC toolchain is used to support cross-compilation for the 32-bit.
+          # TODO: Remove this runner when we drop the support for R < 4.2
+          - {os: windows-latest, r: '4.1',     rust-version: 'stable-msvc'}
           # - {os: windows-latest, r: 'release', rust-version: 'nightly-msvc'}
           # - {os: windows-latest, r: 'devel', rust-version: 'stable-msvc'}
           # - {os: windows-latest, r: 'oldrel', rust-version: 'stable-msvc'}
@@ -80,18 +81,66 @@ jobs:
         uses: r-lib/actions/setup-r@v2
         with:
           r-version: ${{ matrix.config.r }}
-          use-public-rspm: true
+          rtools-version: ${{ matrix.config.rtools-version }}
+          # TODO: enable RSPM when all the packages are available
+          use-public-rspm: false
       
       - name: Set up Pandoc
         uses: r-lib/actions/setup-pandoc@v2
-      
-      # All configurations for Windows go here
-      # If 'rust-version' has no architecture id, both if conditions are executed
-      # Otherwise, only one condition is met
-      # $targets variable is used to build targets list used later for cargo build/test
-      # The order of if blocks favors x86_64 over i686 paths, which is optimal for multi-targeting
-      - name: Configure Windows (R-release)
-        if: startsWith(runner.os, 'Windows') && matrix.config.r != 'devel'
+
+      - name: Configure Windows (R >= 4.2)
+        if: startsWith(runner.os, 'Windows') && matrix.config.r != '4.1'
+        run: |
+          $rtools42_home = "C:\rtools42"
+
+          # c.f. https://github.com/wch/r-source/blob/f1501504df8df1668a57d3a1b6f80167f24441d3/src/library/profile/Rprofile.windows#L70-L71
+          echo "${rtools42_home}\x86_64-w64-mingw32.static.posix\bin"      | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append ;
+          echo "${rtools42_home}\usr\bin"                                  | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append ;
+          echo "$(Rscript.exe -e 'cat(normalizePath(R.home()))')\bin\x64"  | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append ;
+
+          # Add target
+          rustup target add x86_64-pc-windows-gnu
+          echo "BUILD_TARGETS=x86_64-pc-windows-gnu" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append ;
+
+          # The following lines add two tweaks:
+          #
+          #  1. Change the linker name to "x86_64-w64-mingw32.static.posix-gcc.exe".
+          #  2. Add empty libgcc_s.a and libgcc_eh.a, and add them to the compiler's
+          #     library search paths via `LIBRARY_PATH` envvar.
+          # 
+          # The first tweak is needed because Rtools42 doesn't contain 
+          # "x86_64-w64-mingw32-gcc," which `rustc` uses as the default linker 
+          # for the `x86_64-pc-windows-gnu` target.
+          #  
+          # If we use the Rtools' toolchain, the second tweak is also required.
+          # `rustc` adds `-lgcc_eh` and `-lgcc_s` flags to the compiler, but
+          # Rtools' GCC doesn't have `libgcc_eh` or `libgcc_a` due to the 
+          # compilation settings. So, in order to please the compiler, we need
+          # to add empty `libgcc_eh` or `libgcc_a` to the library search paths.
+          # 
+          # For more details, please refer to https://github.com/r-windows/rtools-packages/blob/2407b23f1e0925bbb20a4162c963600105236318/mingw-w64-gcc/PKGBUILD#L313-L316
+
+          New-Item -Path libgcc_mock -Type Directory
+          New-Item -Path libgcc_mock\gcc.c -Type File
+          x86_64-w64-mingw32.static.posix-gcc.exe -c libgcc_mock\gcc.c -o libgcc_mock\gcc.o
+          x86_64-w64-mingw32.static.posix-ar.exe -r libgcc_mock\libgcc_eh.a libgcc_mock\gcc.o
+          x86_64-w64-mingw32.static.posix-ar.exe -r libgcc_mock\libgcc_s.a libgcc_mock\gcc.o
+
+          New-Item -Path .cargo -ItemType Directory -Force
+          $pwd_slash = echo "${PWD}" | % {$_ -replace '\\','/'}
+          @"
+          [target.x86_64-pc-windows-gnu]
+          linker = "x86_64-w64-mingw32.static.posix-gcc.exe"
+
+          [env]
+          LIBRARY_PATH = "${pwd_slash}/libgcc_mock"
+          "@ | Out-File -FilePath .cargo/config.toml -Encoding utf8 -Append ;
+        env: 
+          RUST_TOOLCHAIN: ${{ matrix.config.rust-version }}
+
+      # TODO: Remove this runner when we drop the support for R < 4.2
+      - name: Configure Windows (R < 4.2)
+        if: startsWith(runner.os, 'Windows') && matrix.config.r == '4.1'
         # 1. Add rust target
         # 2. Add target name to the $targets variable
         # 3. Add mingw32/mingw64 bin folders to PATH
@@ -114,18 +163,6 @@ jobs:
         env: 
           RUST_TOOLCHAIN: ${{ matrix.config.rust-version }}
 
-      - name: Configure Windows (R-devel)
-        if: startsWith(runner.os, 'Windows') && matrix.config.r == 'devel'
-        run: |
-          rustup target add x86_64-pc-windows-gnu ;
-          # Use Rtools's UCRT toolchain rather than MSYS2. Note that the setting needs to be modified when Rtools42 becomes official.
-          echo "${env:RTOOLS40_HOME}\ucrt64\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append ;
-          echo "$(Rscript.exe -e 'cat(normalizePath(R.home()))')\bin\x64"  | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append ;
-          echo "BUILD_TARGETS=x86_64-pc-windows-gnu" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append ;
-        env: 
-          RUST_TOOLCHAIN: ${{ matrix.config.rust-version }}
-
-
       # This is required for ubuntu r-devel
       # 'Del alias:R' removes 'R' alias which prevents running R 
       - name: Configure Linux
@@ -168,14 +205,25 @@ jobs:
       - name: Install dependencies for extendrtests and rcmdcheck
         uses: r-lib/actions/setup-r-dependencies@v2
         with:
+          cache-version: 2
           working-directory: tests/extendrtests
           extra-packages: any::rcmdcheck
 
       - name: Install dependencies for rextendr
         uses: r-lib/actions/setup-r-dependencies@v2
         with:
+          cache-version: 2
           working-directory: tests/rextendr
 
+      # TODO: remove this when we decide which to use stable-gnu or stable-msvc for R >= 4.2
+      - name: Tweak Makevars.ucrt
+        if: startsWith(runner.os, 'Windows') && matrix.config.r == 'devel' && matrix.config.rust-version == 'stable-msvc'
+        run: |
+          sed -i 's/stable-gnu/stable-msvc/g' tests/extendrtests/src/Makevars.ucrt
+          sed -i 's/stable-gnu/stable-msvc/g' tests/rextendr/inst/templates/Makevars.ucrt
+          sed -i 's/stable-gnu/stable-msvc/g' tests/rextendr/tests/testthat/_snaps/use_extendr.md
+        shell: bash
+
       # Regex is used to inject absolute path into Cargo.toml
       - name: Configure R for integration testing
         run: |
@@ -280,11 +328,14 @@ jobs:
         config:
           - {os: ubuntu-20.04,   r: 'devel', rust-version: 'stable'}
           - {os: macOS-latest,   r: 'devel', rust-version: 'stable'}
-          # This will be changed back to `R-devel` as soon as R drops 32-bit support and we no longer need cross-compilation
-          - {os: windows-latest, r: 'release', rust-version: 'nightly-msvc', rust-targets: ['x86_64-pc-windows-gnu'], extra-args: ['-Zdoctest-xcompile']}
-          - {os: windows-latest, r: 'release', rust-version: 'nightly-msvc', rust-targets: ['i686-pc-windows-gnu'], extra-args: ['-Zdoctest-xcompile']}
-
-
+          - {os: windows-latest, r: 'release', rust-version: 'stable-gnu',   rust-targets: ['x86_64-pc-windows-gnu'], rtools-version: '42'}
+          - {os: windows-latest, r: 'devel',   rust-version: 'stable-gnu',   rust-targets: ['x86_64-pc-windows-gnu'], rtools-version: '42'}
+          # For R < 4.2, the MSVC toolchain is used to support cross-compilation for the 32-bit.
+          # Since it does cross-compilation from MSVC to GNU, we need `-Zdoctest-xcompile` to
+          # let the doctests run, which accordingly require the nightly toolchain.
+          # TODO: Remove these runners when we drop the support for R < 4.2.
+          - {os: windows-latest, r: '4.1',     rust-version: 'nightly-msvc', rust-targets: ['x86_64-pc-windows-gnu'], extra-args: ['-Zdoctest-xcompile']}
+          - {os: windows-latest, r: '4.1',     rust-version: 'nightly-msvc', rust-targets: ['i686-pc-windows-gnu'],   extra-args: ['-Zdoctest-xcompile']}
 
     env:
       R_REMOTES_NO_ERRORS_FROM_WARNINGS: true
@@ -327,6 +378,7 @@ jobs:
         uses: r-lib/actions/setup-r@v2
         with:
           r-version: ${{ matrix.config.r }}
+          rtools-version: ${{ matrix.config.rtools-version }}
           use-public-rspm: true
           windows-path-include-mingw: false
 
@@ -346,13 +398,62 @@ jobs:
           install: mingw32/mingw-w64-i686-clang mingw-w64-i686-toolchain
           release: false
 
+      - name: Configure Windows (R >= 4.2)
+        if: startsWith(runner.os, 'Windows') && matrix.config.r != '4.1'
+        run: |
+          $rtools42_home = "C:\rtools42"
+
+          echo "::group::Setting up x86_64"
+          echo "C:\msys64\mingw64\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
+          # c.f. https://github.com/wch/r-source/blob/f1501504df8df1668a57d3a1b6f80167f24441d3/src/library/profile/Rprofile.windows#L70-L71
+          echo "${rtools42_home}\x86_64-w64-mingw32.static.posix\bin"      | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append ;
+          echo "${rtools42_home}\usr\bin"                                  | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append ;
+          echo "$(Rscript.exe -e 'cat(normalizePath(R.home()))')\bin\x64"  | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
+          echo "::endgroup::"
+          
+          # The following lines add two tweaks:
+          #
+          #  1. Change the linker name to "x86_64-w64-mingw32.static.posix-gcc.exe".
+          #  2. Add empty libgcc_s.a and libgcc_eh.a, and add them to the compiler's
+          #     library search paths via `LIBRARY_PATH` envvar.
+          # 
+          # The first tweak is needed because Rtools42 doesn't contain 
+          # "x86_64-w64-mingw32-gcc," which `rustc` uses as the default linker 
+          # for the `x86_64-pc-windows-gnu` target.
+          #  
+          # If we use the Rtools' toolchain, the second tweak is also required.
+          # `rustc` adds `-lgcc_eh` and `-lgcc_s` flags to the compiler, but
+          # Rtools' GCC doesn't have `libgcc_eh` or `libgcc_a` due to the 
+          # compilation settings. So, in order to please the compiler, we need
+          # to add empty `libgcc_eh` or `libgcc_a` to the library search paths.
+          # 
+          # For more details, please refer to https://github.com/r-windows/rtools-packages/blob/2407b23f1e0925bbb20a4162c963600105236318/mingw-w64-gcc/PKGBUILD#L313-L316
+
+          New-Item -Path libgcc_mock -Type Directory
+          New-Item -Path libgcc_mock\gcc.c -Type File
+          x86_64-w64-mingw32.static.posix-gcc.exe -c libgcc_mock\gcc.c -o libgcc_mock\gcc.o
+          x86_64-w64-mingw32.static.posix-ar.exe -r libgcc_mock\libgcc_eh.a libgcc_mock\gcc.o
+          x86_64-w64-mingw32.static.posix-ar.exe -r libgcc_mock\libgcc_s.a libgcc_mock\gcc.o
+
+          New-Item -Path .cargo -ItemType Directory -Force
+          $pwd_slash = echo "${PWD}" | % {$_ -replace '\\','/'}
+          @"
+          [target.x86_64-pc-windows-gnu]
+          linker = "x86_64-w64-mingw32.static.posix-gcc.exe"
+
+          [env]
+          LIBRARY_PATH = "${pwd_slash}/libgcc_mock"
+          "@ | Out-File -FilePath .cargo/config.toml -Encoding utf8 -Append ;
+        env:
+          RUST_TARGETS: ${{ join(matrix.config.rust-targets, ',') }}
+
       # 1. Inspect targets. If empty, assume 'x86_64' arch
       # 2. Install msys2 packages (--needed skips already installed)
       # 3. Add msys2/mingw{bits}/bin to path
       # 4. Add R/{arch}/bin to path
       # 5. Create array of correct (arch-dependent) paths to 'libclang.dll', export as env variable
-      - name: Configure Windows
-        if: startsWith(runner.os, 'Windows')
+      - name: Configure Windows (R < 4.2)
+        if: startsWith(runner.os, 'Windows') && matrix.config.r == '4.1'
         run: |
 
           # mingw64 is required for both 32 and 64 bit targets
@@ -426,7 +527,7 @@ jobs:
             ci-cargo +$toolchain test --manifest-path extendr-api/Cargo.toml --features tests $(if($target -ne 'default') {"--target=$target"} ) $env:EXTRA_ARGS '--' --nocapture -ActionName "Test extendr-api \w tests for $target target"
 
             # graphics tests requires --test-threads=1
-            ci-cargo +$toolchain test graphics_tests:: --manifest-path extendr-api/Cargo.toml --features graphics $(if($target -ne 'default') {"--target=$target"} ) $env:EXTRA_ARGS '--' --nocapture --test-threads=1 -ActionName "Test extendr-api \w graphics for $target target"
+            ci-cargo +$toolchain test graphics_tests:: --manifest-path extendr-api/Cargo.toml --features tests-graphics $(if($target -ne 'default') {"--target=$target"} ) $env:EXTRA_ARGS '--' --nocapture --test-threads=1 -ActionName "Test extendr-api \w graphics for $target target"
 
             ci-cargo +$toolchain test --manifest-path extendr-api/Cargo.toml --features tests-minimal $(if($target -ne 'default') {"--target=$target"} ) $env:EXTRA_ARGS '--' --nocapture -ActionName "Test extendr-api \w tests-minimal for $target target"
                       
diff --git a/extendr-api/tests/dataframe_tests.rs b/extendr-api/tests/dataframe_tests.rs
new file mode 100644
index 000000000..b69bfbdf9
--- /dev/null
+++ b/extendr-api/tests/dataframe_tests.rs
@@ -0,0 +1,31 @@
+use extendr_api::prelude::*;
+
+#[test]
+fn test_derive_into_dataframe() {
+    test! {
+        use extendr_api::prelude::*;
+
+        #[derive(Debug, IntoDataFrame)]
+        struct MyStruct {
+            x: Rint,
+            y: Rstr,
+        }
+
+        let v = vec![MyStruct { x: 0.into(), y: "abc".into() }, MyStruct { x: 1.into(), y: "xyz".into() }];
+        let df = v.into_dataframe()?;
+
+        assert!(df.inherits("data.frame"));
+
+        let list : List = df.as_list().unwrap();
+        assert_eq!(list[0], r!([0, 1]));
+        assert_eq!(list[1], r!(["abc", "xyz"]));
+
+        // Note the odd RFC 2451 (I,). If you can fix this, I'll be a happy bunny.
+        let df3 = ((0..2).map(|i| MyStruct { x: i.into(), y: i.to_string().into() }),).into_dataframe()?;
+        assert!(df3.inherits("data.frame"));
+
+        let list : List = df3.as_list().unwrap();
+        assert_eq!(list[0], r!([0, 1]));
+        assert_eq!(list[1], r!(["0", "1"]));
+    }
+}
diff --git a/extendr-macros/tests/test_derive_into_list.rs b/extendr-api/tests/derive_into_list.rs
similarity index 100%
rename from extendr-macros/tests/test_derive_into_list.rs
rename to extendr-api/tests/derive_into_list.rs
diff --git a/extendr-api/tests/issue-397-memory-allocation.rs b/extendr-api/tests/issue-397-memory-allocation.rs
new file mode 100644
index 000000000..44fee6415
--- /dev/null
+++ b/extendr-api/tests/issue-397-memory-allocation.rs
@@ -0,0 +1,24 @@
+use extendr_api::prelude::*;
+
+#[test]
+fn test_allocation() {
+    test! {
+        const COUNT: u64 = 2_000_000u64;
+        let mut data: Vec<Robj> = vec![];
+        for i in 1u64..COUNT {
+            if i % 1000 == 0 { println!("{}", i); }
+            data.push(i.into());
+        }
+
+        for (ix, v) in data.iter().enumerate() {
+            assert_eq!(Some((ix + 1) as f64), v.as_real())
+        }
+
+        let obj: Robj = List::from_names_and_values(&["A"], vec![data]).into();
+        obj.set_attrib(
+            row_names_symbol(),
+            (1i32..=COUNT as i32).collect_robj(),
+        )?;
+        obj.set_class(&["data.frame"])?;
+    }
+}
diff --git a/extendr-api/tests/vector_tests.rs b/extendr-api/tests/vector_tests.rs
index 60db96cdc..5c42bedd6 100644
--- a/extendr-api/tests/vector_tests.rs
+++ b/extendr-api/tests/vector_tests.rs
@@ -18,7 +18,7 @@ fn test_strings() {
         let v = s.as_slice().iter().map(|c| c.as_str()).collect::<String>();
         assert_eq!(v, "xyz");
 
-        s.set_elt(1, "q");
+        s.set_elt(1, Rstr::from("q"));
         assert_eq!(s.elt(1), "q");
 
         let s : Strings = ["x", "y", "z"].iter().collect();
diff --git a/tests/extendrtests/src/Makevars.ucrt b/tests/extendrtests/src/Makevars.ucrt
new file mode 100644
index 000000000..4a804d86a
--- /dev/null
+++ b/tests/extendrtests/src/Makevars.ucrt
@@ -0,0 +1,8 @@
+# Use GNU toolchain for R >= 4.2
+TOOLCHAIN = stable-gnu
+
+# Rtools42 doesn't have the linker in the location that cargo expects, so we
+# need to overwrite it via configuration.
+CARGO_LINKER = x86_64-w64-mingw32.static.posix-gcc.exe
+
+include Makevars.win
diff --git a/tests/extendrtests/src/Makevars.win b/tests/extendrtests/src/Makevars.win
index 0f6f5d787..e4bb9c5bc 100644
--- a/tests/extendrtests/src/Makevars.win
+++ b/tests/extendrtests/src/Makevars.win
@@ -1,5 +1,8 @@
 TARGET = $(subst 64,x86_64,$(subst 32,i686,$(WIN)))-pc-windows-gnu
-TOOLCHAIN = stable-msvc
+
+# This is provided in Makevars.ucrt for R >= 4.2
+TOOLCHAIN ?= stable-msvc
+
 TARGET_DIR = ./rust/target
 LIBDIR = $(TARGET_DIR)/$(TARGET)/debug
 STATLIB = $(LIBDIR)/lextendrtest.a
@@ -10,7 +13,19 @@ all: C_clean
 $(SHLIB): $(STATLIB)
 
 $(STATLIB):
-	cargo +$(TOOLCHAIN) build --target=$(TARGET) --lib --manifest-path=./rust/Cargo.toml --target-dir $(TARGET_DIR)
+	# Note: on the GitHub Actions CI, the tests pass without this tweak because
+	#       the same setup is already done in the CI.
+	mkdir -p $(TARGET_DIR)/libgcc_mock
+	cd $(TARGET_DIR)/libgcc_mock && \
+		touch gcc_mock.c && \
+		gcc -c gcc_mock.c -o gcc_mock.o && \
+		ar -r libgcc_eh.a gcc_mock.o && \
+		cp libgcc_eh.a libgcc_s.a
+
+	# CARGO_LINKER is provided in Makevars.ucrt for R >= 4.2
+	export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER="$(CARGO_LINKER)" && \
+		export LIBRARY_PATH="$${LIBRARY_PATH};$(CURDIR)/$(TARGET_DIR)/libgcc_mock" && \
+		cargo +$(TOOLCHAIN) build --target=$(TARGET) --lib --manifest-path=./rust/Cargo.toml --target-dir $(TARGET_DIR)
 
 C_clean:
 	rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS)