Repository: extendr/extendr
Pull request #170 (2021-02-02 21:50:33)

Consider always exporting the `$` method
I think we should consider always exporting the `$` method for implementations.

In my use case I have a class that is only used as output for a function, so I don't want to expose it's constructors. However I want the `$` methods to be accessible to users can do `$method()` on the output objects.

It's also considered a good practice in the R packages book:

> A method for an internal generic: technically, you don’t need to export these methods. However, I recommend exporting every S3 method you write because it’s simpler and makes it less likely that you’ll introduce hard to find bugs. Use devtools::missing_s3() to list all S3 methods that you’ve forgotten to export.

[Link](https://r-pkgs.org/namespace.html?q=internal#export-s3) 




Patch:
diff --git a/ci-cargo.ps1 b/ci-cargo.ps1
new file mode 100644
index 000000000..0e5adfd3c
--- /dev/null
+++ b/ci-cargo.ps1
@@ -0,0 +1,71 @@
+function ci-cargo {
+
+    param(
+        [Parameter(Position = 0, ValueFromRemainingArguments)]
+        [String[]]
+        $CargoArgs,
+        [String]
+        $ActionName
+    )
+
+
+    try {    
+        echo "::group::$ActionName"
+        echo "Running cargo $CargoArgs"
+        cargo $CargoArgs
+        if($LASTEXITCODE -ne  0) {
+            throw $LASTEXITCODE
+        }
+    }
+    catch {
+        if ($ActionName -ne $null -and $ActionName -ne "") {
+            $ActionName = "'$ActionName': "
+        }
+        $err_msg = "$($ActionName)cargo failed with code $LASTEXITCODE (args: $CargoArgs)"
+        echo "::error::$err_msg"
+        Write-Error -Message "$err_msg" -ErrorAction Stop 
+    }
+    finally {
+        echo "::endgroup::"
+    }
+
+    <#
+        .SYNOPSIS
+        Runs cargo with specified args, adapting error handling and output to CI.
+
+        .DESCRIPTION
+        Runs cargo in a `try` block, catches exceptions and non-zero exit codes.
+        Explicitly logs the beginning and the end of cargo execution, as well as the error message.
+
+        .PARAMETER CargoArgs
+        Arguments passed to cargo, as-is.
+        Note that `--` separator is handled by powershell itself,
+        so it should be wrapped in quotes `'--'` and passed as string.
+
+        .PARAMETER ActionName
+        Optional string that is used to format logs and error messages.
+
+        .INPUTS
+        None. You cannot pipe objects.
+
+        .OUTPUTS
+        No explicit output.
+
+        .EXAMPLE
+        PS> ci-cargo --version
+            ::group::
+            Running cargo --version
+            cargo 1.49.0 (d00d64df9 2020-12-05)
+            ::endgroup::
+
+        .EXAMPLE
+        PS> ci-cargo -ActioName "Build"  build
+
+        .EXAMPLE
+        PS> ci-cargo +stable-x86_64-pc-windows-gnu test --features tests-all --target i686-pc-windows-gnu '--' --nocapture -ActionName "Called from documentation"
+
+        .LINK
+        Used by: https://github.com/extendr/extendr
+    #>
+    
+}
diff --git a/extendr-api/Cargo.toml b/extendr-api/Cargo.toml
index 453eccdf5..0feb19808 100644
--- a/extendr-api/Cargo.toml
+++ b/extendr-api/Cargo.toml
@@ -18,6 +18,7 @@ libR-sys = "0.2.0"
 extendr-macros = { path = "../extendr-macros", version="0.1.11" }
 extendr-engine = { path = "../extendr-engine", version="0.1.11" }
 ndarray = { version = "0.13.1", optional = true }
+lazy_static = "1.4"
 
 [features]
 default = []
diff --git a/extendr-api/src/functions.rs b/extendr-api/src/functions.rs
index 8363a0570..8ed122986 100644
--- a/extendr-api/src/functions.rs
+++ b/extendr-api/src/functions.rs
@@ -34,8 +34,7 @@ pub fn new_env_with_capacity(capacity: usize) -> Robj {
         // Unhashed envirnment
         call!("new.env", FALSE, global_env(), 0).unwrap()
     } else {
-        // Hashed environment, the pidgeon hole principle
-        // ensures there are empty slots in the hash table.
+        // Hashed environment for larger hashmaps.
         call!("new.env", TRUE, global_env(), capacity as i32 * 2 + 1).unwrap()
     }
 }
diff --git a/extendr-api/src/iter.rs b/extendr-api/src/iter.rs
index 3606c6153..e9f16db11 100644
--- a/extendr-api/src/iter.rs
+++ b/extendr-api/src/iter.rs
@@ -1,6 +1,24 @@
 use crate::*;
 
-// Iterator over the objects in a VECSXP, EXPRSXP or WEAKREFSXP.
+/// Iterator over the objects in a VECSXP, EXPRSXP or WEAKREFSXP.
+///
+/// ```
+/// use extendr_api::prelude::*;
+/// test! {
+///     let my_list = list!(a = 1, b = 2);
+///     let mut total = 0;
+///     for robj in my_list.as_list_iter().unwrap() {
+///       if let Some(val) = robj.as_integer() {
+///         total += val;
+///       }
+///     }
+///     assert_eq!(total, 3);
+///    
+///     for name in my_list.names().unwrap() {
+///        assert!(name == "a" || name == "b")
+///     }
+/// }
+/// ```
 #[derive(Clone)]
 pub struct ListIter {
     vector: Robj,
@@ -42,37 +60,95 @@ impl Iterator for ListIter {
     }
 }
 
+/// Generalised iterator of numbers and logical. See Int, Real and Logical.
+pub struct SliceIter<T> {
+    // Control lifetime of vector to make sure the memory is not freed.
+    #[allow(dead_code)]
+    vector: Robj,
+    i: usize,
+    len: usize,
+    ptr: *const T,
+}
+
+impl<T> SliceIter<T> {
+    // A new, empty list iterator.
+    pub fn new() -> Self {
+        SliceIter {
+            vector: ().into(),
+            i: 0,
+            len: 0,
+            ptr: std::ptr::null(),
+        }
+    }
+
+    pub fn from_slice(vector: Robj, slice: &[T]) -> Self {
+        SliceIter {
+            vector,
+            i: 0,
+            len: slice.len(),
+            ptr: slice.as_ptr(),
+        }
+    }
+}
+
+/// Basis of Int, Real and Logical.
+impl<T: Copy> Iterator for SliceIter<T> {
+    type Item = T;
+
+    fn size_hint(&self) -> (usize, Option<usize>) {
+        (self.len, Some(self.len))
+    }
+
+    fn next(&mut self) -> Option<Self::Item> {
+        let i = self.i;
+        self.i += 1;
+        if i >= self.len {
+            self.i = self.len;
+            None
+        } else {
+            unsafe { Some(*self.ptr.offset(i as isize)) }
+        }
+    }
+
+    fn nth(&mut self, n: usize) -> Option<Self::Item> {
+        self.i += n;
+        self.next()
+    }
+}
+
 /// Iterator over name-value pairs in lists.
 pub type NamedListIter = std::iter::Zip<StrIter, ListIter>;
 
 /// Iterator over primitives in integer objects.
-pub type IntegerIter<'a> = std::iter::Cloned<std::slice::Iter<'a, i32>>;
+/// ```
+/// use extendr_api::prelude::*;
+///
+/// fn add(a: Int, b: Int) -> Robj {
+///     a.zip(b).map(|(a, b)| a+b).collect_robj()
+/// }
+/// ```
+pub type Int = SliceIter<i32>;
 
 /// Iterator over primitives in real objects.
-pub type RealIter<'a> = std::iter::Cloned<std::slice::Iter<'a, f64>>;
+/// ```
+/// use extendr_api::prelude::*;
+///
+/// fn add1(a: Real) -> Robj {
+///     a.map(|a| a + 1.0).collect_robj()
+/// }
+/// ```
+pub type Real = SliceIter<f64>;
 
 /// Iterator over primitives in logical objects.
-pub type LogicalIter<'a> = std::iter::Cloned<std::slice::Iter<'a, Bool>>;
-
-/// Iterator over the objects in a vector or string.
-///
 /// ```
 /// use extendr_api::prelude::*;
-/// test! {
-///     let my_list = list!(a = 1, b = 2);
-///     let mut total = 0;
-///     for robj in my_list.as_list_iter().unwrap() {
-///       if let Some(val) = robj.as_integer() {
-///         total += val;
-///       }
-///     }
-///     assert_eq!(total, 3);
-///    
-///     for name in my_list.names().unwrap() {
-///        assert!(name == "a" || name == "b")
-///     }
+///
+/// fn all_true(mut a: Logical) -> bool {
+///     a.all(|a| a.is_true())
 /// }
 /// ```
+pub type Logical = SliceIter<Bool>;
+
 #[derive(Clone)]
 pub struct PairlistIter {
     root_obj: Robj,
diff --git a/extendr-api/src/lib.rs b/extendr-api/src/lib.rs
index 8f42ef53c..6ef44c40e 100644
--- a/extendr-api/src/lib.rs
+++ b/extendr-api/src/lib.rs
@@ -41,6 +41,7 @@
 //! test! {
 //!     // An R object with a single string "hello"
 //!     let character = r!("hello");
+//!     let character = r!(["hello", "goodbye"]);
 //!    
 //!     // An R integer object with a single number 1L.
 //!     // Note that in Rust, 1 is an integer and 1.0 is a real.
@@ -52,6 +53,8 @@
 //!    
 //!     // An R real vector.
 //!     let real_vector = r!([1.0, 2.0]);
+//!     let real_vector = &[1.0, 2.0].iter().collect_robj();
+//!     let real_vector = r!(vec![1.0, 2.0]);
 //!    
 //!     // An R function object.
 //!     let function = R!(function(x, y) { x + y })?;
@@ -59,8 +62,16 @@
 //!     // A named list using the list! macro.
 //!     let list = list!(a = 1, b = 2);
 //!    
+//!     // An unnamed list (of R objects) using the List wrapper.
+//!     let list = r!(List(vec![1, 2, 3]));
+//!     let list = r!(List(vec!["a", "b", "c"]));
+//!     let list = r!(List(&[r!("a"), r!(1), r!(2.0)]));
+//!
 //!     // A symbol
 //!     let sym = sym!(wombat);
+//!
+//!     // A R vector using collect_robj()
+//!     let vector = (0..3).map(|x| x * 10).collect_robj();
 //! }
 //! ```
 //!
@@ -197,6 +208,7 @@ pub mod lang_macros;
 pub mod logical;
 pub mod matrix;
 pub mod metadata;
+pub mod ownership;
 pub mod prelude;
 pub mod rmacros;
 
@@ -484,17 +496,17 @@ mod tests {
     }
 
     #[extendr]
-    pub fn f64_iter(x: RealIter) -> RealIter {
+    pub fn f64_iter(x: Real) -> Real {
         x
     }
 
     #[extendr]
-    pub fn i32_iter(x: IntegerIter) -> IntegerIter {
+    pub fn i32_iter(x: Int) -> Int {
         x
     }
 
     #[extendr]
-    pub fn bool_iter(x: LogicalIter) -> LogicalIter {
+    pub fn bool_iter(x: Logical) -> Logical {
         x
     }
 
@@ -641,19 +653,19 @@ mod tests {
                 assert_eq!(new_owned(wrap__bool_slice(robj.get())), robj);
 
                 // #[extendr]
-                // pub fn f64_iter(x: RealIter) -> RealIter { x }
+                // pub fn f64_iter(x: Real) -> Real { x }
 
                 let robj = r!([1., 2., 3.]);
                 assert_eq!(new_owned(wrap__f64_iter(robj.get())), robj);
 
                 // #[extendr]
-                // pub fn i32_iter(x: IntegerIter) -> IntegerIter { x }
+                // pub fn i32_iter(x: Int) -> Int { x }
 
                 let robj = r!([1, 2, 3]);
                 assert_eq!(new_owned(wrap__i32_iter(robj.get())), robj);
 
                 // #[extendr]
-                // pub fn bool_iter(x: LogicalIter) -> LogicalIter { x }
+                // pub fn bool_iter(x: Logical) -> Logical { x }
 
                 let robj = r!([TRUE, FALSE, TRUE]);
                 assert_eq!(new_owned(wrap__bool_iter(robj.get())), robj);
diff --git a/extendr-api/src/logical.rs b/extendr-api/src/logical.rs
index c8660331d..a87ae26f7 100644
--- a/extendr-api/src/logical.rs
+++ b/extendr-api/src/logical.rs
@@ -16,7 +16,7 @@ impl Bool {
 
     /// Test if TRUE
     pub fn is_true(&self) -> bool {
-        self.0 != 0
+        self.0 == 1
     }
 
     /// Test if FALSE
diff --git a/extendr-api/src/metadata.rs b/extendr-api/src/metadata.rs
index 69797660d..52a4fdcc7 100644
--- a/extendr-api/src/metadata.rs
+++ b/extendr-api/src/metadata.rs
@@ -266,6 +266,11 @@ fn write_impl_wrapper(
         writeln!(w, "#' @rdname {}", imp.name)?;
         writeln!(w, "#' @usage NULL")?;
         writeln!(w, "#' @export")?;
+    } else {
+        // we always export the `$` method so the method is correctly
+        // added to the NAMESPACE even if we don't export the class
+        // itself and its initializers.
+        writeln!(w, "#' @export")?;
     }
     // LHS with dollar operator is wrapped in ``, so pass name as is,
     // but in the body `imp_name_fixed` is called as valid R function,
diff --git a/extendr-api/src/ownership.rs b/extendr-api/src/ownership.rs
new file mode 100644
index 000000000..6459867bf
--- /dev/null
+++ b/extendr-api/src/ownership.rs
@@ -0,0 +1,348 @@
+//! Maintain ownership of R objects.
+//!
+//! This provides the functions protect() and unprotect()
+//! A single preserved vector holds ownership of all protected objects.
+//!
+//! Objects are refernce counted, so multiple calls are possible,
+//! unlike R_PreserveObject.
+//!
+//! This module exports two functions, protect(sexp) and unprotect(sexp).
+
+use lazy_static::lazy_static;
+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,
+};
+
+lazy_static! {
+    static ref OWNERSHIP: Mutex<Ownership> = Mutex::new(Ownership::new());
+}
+
+pub unsafe fn protect(sexp: SEXP) {
+    let mut own = OWNERSHIP.lock().expect("protect failed");
+    own.protect(sexp);
+}
+
+pub unsafe fn unprotect(sexp: SEXP) {
+    let mut own = OWNERSHIP.lock().expect("unprotect failed");
+    own.unprotect(sexp);
+}
+
+pub const INITIAL_PRESERVATION_SIZE: usize = 25000;
+pub const EXTRA_PRESERVATION_SIZE: usize = 25000;
+
+struct Object {
+    refcount: usize,
+    index: usize,
+}
+
+// A reference counted object with an index in the preservation vector.
+struct Ownership {
+    // A growable vector containing all owned objects.
+    preservation: usize,
+
+    // An incrementing count of objects through the vector.
+    cur_index: usize,
+
+    // The size of the vector.
+    max_index: usize,
+
+    // A hash map from SEXP address to object.
+    objects: HashMap<usize, Object>,
+}
+
+impl Ownership {
+    fn new() -> Self {
+        unsafe {
+            let preservation = Rf_allocVector(VECSXP, INITIAL_PRESERVATION_SIZE as R_xlen_t);
+            R_PreserveObject(preservation);
+            Ownership {
+                preservation: preservation as usize,
+                cur_index: 0,
+                max_index: INITIAL_PRESERVATION_SIZE,
+                objects: HashMap::with_capacity(INITIAL_PRESERVATION_SIZE),
+            }
+        }
+    }
+
+    unsafe fn protect(&mut self, sexp: SEXP) {
+        if self.cur_index == self.max_index {
+            self.garbage_collect();
+        }
+
+        let sexp_usize = sexp as usize;
+        match *self {
+            Ownership {
+                ref mut preservation,
+                ref mut cur_index,
+                ref mut max_index,
+                ref mut objects,
+            } => {
+                let mut entry = objects.entry(sexp_usize);
+                let preservation_sexp = *preservation as SEXP;
+                match entry {
+                    Entry::Occupied(ref mut occupied) => {
+                        if occupied.get().refcount == 0 {
+                            // Address re-used - re-set the sexp.
+                            SET_VECTOR_ELT(
+                                preservation_sexp,
+                                occupied.get().index as R_xlen_t,
+                                sexp,
+                            );
+                        }
+                        occupied.get_mut().refcount += 1;
+                    }
+                    Entry::Vacant(vacant) => {
+                        let index = *cur_index;
+                        SET_VECTOR_ELT(preservation_sexp, index as R_xlen_t, sexp);
+                        *cur_index += 1;
+                        assert!(index != *max_index);
+                        let refcount = 1;
+                        vacant.insert(Object { refcount, index });
+                    }
+                }
+            }
+        }
+    }
+
+    pub unsafe fn unprotect(&mut self, sexp: SEXP) {
+        let sexp_usize = sexp as usize;
+        match *self {
+            Ownership {
+                preservation,
+                cur_index: _,
+                max_index: _,
+                ref mut objects,
+            } => {
+                let mut entry = objects.entry(sexp_usize);
+                match entry {
+                    Entry::Occupied(ref mut occupied) => {
+                        let object = occupied.get_mut();
+                        if object.refcount == 0 {
+                            panic!("Attempt to unprotect an already unprotected object.")
+                        } else {
+                            object.refcount -= 1;
+                            if object.refcount == 0 {
+                                // Clear the preservation vector, but keep the hash table entry.
+                                // It is hard to clear the hash table entry here because we don't
+                                // have a ref to objects anymore and it is faster to clear them up en-masse.
+                                let preservation_sexp = preservation as SEXP;
+                                SET_VECTOR_ELT(
+                                    preservation_sexp,
+                                    object.index as R_xlen_t,
+                                    R_NilValue,
+                                );
+                            }
+                        }
+                    }
+                    Entry::Vacant(_) => {
+                        panic!("Attempt to unprotect a never protected object.")
+                    }
+                }
+            }
+        }
+    }
+
+    #[allow(dead_code)]
+    unsafe fn ref_count(&mut self, sexp: SEXP) -> usize {
+        match *self {
+            Ownership {
+                preservation: _,
+                cur_index: _,
+                max_index: _,
+                ref mut objects,
+            } => {
+                let sexp_usize = sexp as usize;
+                let mut entry = objects.entry(sexp_usize);
+                match entry {
+                    Entry::Occupied(ref mut occupied) => occupied.get().refcount,
+                    Entry::Vacant(_) => 0,
+                }
+            }
+        }
+    }
+
+    // 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_sexp = Rf_allocVector(VECSXP, new_size as R_xlen_t);
+        R_PreserveObject(new_sexp);
+        let old_sexp = self.preservation as SEXP;
+
+        let mut new_objects = HashMap::with_capacity(new_size);
+
+        // copy non-null elements to new vector and hashmap.
+        let mut j = 0;
+        for (addr, object) in self.objects.iter() {
+            if object.refcount != 0 {
+                SET_VECTOR_ELT(new_sexp, j as R_xlen_t, *addr as SEXP);
+                new_objects.insert(
+                    *addr,
+                    Object {
+                        refcount: object.refcount,
+                        index: j,
+                    },
+                );
+                j += 1;
+            }
+        }
+        // println!("j={}", j);
+
+        R_ReleaseObject(old_sexp);
+        self.preservation = new_sexp as usize;
+        self.cur_index = j;
+        self.max_index = new_size;
+        self.objects = new_objects;
+    }
+
+    // Check the consistency of the model.
+    #[allow(dead_code)]
+    unsafe fn check_objects(&mut self) {
+        let preservation_sexp = self.preservation as SEXP;
+        assert_eq!(self.max_index, LENGTH(preservation_sexp) as usize);
+
+        // println!("\ncheck");
+
+        for (addr, object) in self.objects.iter() {
+            assert!(object.index < self.max_index);
+            let elt = VECTOR_ELT(preservation_sexp, object.index as R_xlen_t);
+            // println!(
+            //     "refcount={:?} index={:?} elt={:?}",
+            //     object.refcount, object.index, elt
+            // );
+            if object.refcount != 0 {
+                // A non-zero refcount implies the object is in the vector.
+                assert_eq!(elt, *addr as SEXP);
+            } else {
+                // A zero refcount implies the object is NULL in the vector.
+                assert_eq!(elt, R_NilValue);
+            }
+        }
+
+        // println!("check 2");
+        for i in 0..self.max_index {
+            let elt = VECTOR_ELT(preservation_sexp, i as R_xlen_t);
+            if elt == R_NilValue {
+                assert_eq!(self.ref_count(elt), 0);
+            } else {
+                assert!(self.ref_count(elt) != 0);
+            }
+        }
+        // println!("/check");
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use crate::*;
+    use libR_sys::{Rf_ScalarInteger, Rf_protect, Rf_unprotect};
+
+    #[test]
+    fn basic_test() {
+        test! {
+            single_threaded(|| unsafe {
+                {
+                    let mut own = OWNERSHIP.lock().expect("lock failed");
+                    own.check_objects();
+                }
+
+                let sexp1 = Rf_protect(Rf_ScalarInteger(1));
+                let sexp2 = Rf_protect(Rf_ScalarInteger(2));
+                protect(sexp1);
+                {
+                    let mut own = OWNERSHIP.lock().expect("lock failed");
+                    own.check_objects();
+                    assert_eq!(own.ref_count(sexp1), 1);
+                    assert_eq!(own.ref_count(sexp2), 0);
+                }
+
+                protect(sexp1);
+                {
+                    let mut own = OWNERSHIP.lock().expect("lock failed");
+                    own.check_objects();
+                    assert_eq!(own.ref_count(sexp1), 2);
+                    assert_eq!(own.ref_count(sexp2), 0);
+                }
+
+                unprotect(sexp1);
+                {
+                    let mut own = OWNERSHIP.lock().expect("lock failed");
+                    own.check_objects();
+                    assert_eq!(own.ref_count(sexp1), 1);
+                    assert_eq!(own.ref_count(sexp2), 0);
+                }
+
+                unprotect(sexp1);
+                {
+                    let mut own = OWNERSHIP.lock().expect("lock failed");
+                    own.check_objects();
+                    assert_eq!(own.ref_count(sexp1), 0);
+                    assert_eq!(own.ref_count(sexp2), 0);
+                }
+
+                protect(sexp2);
+                {
+                    let mut own = OWNERSHIP.lock().expect("lock failed");
+                    own.check_objects();
+                    assert_eq!(own.ref_count(sexp1), 0);
+                    assert_eq!(own.ref_count(sexp2), 1);
+                }
+
+                protect(sexp1);
+                {
+                    let mut own = OWNERSHIP.lock().expect("lock failed");
+                    own.check_objects();
+                    assert_eq!(own.ref_count(sexp1), 1);
+                    assert_eq!(own.ref_count(sexp2), 1);
+                }
+                Rf_unprotect(2);
+            });
+        }
+    }
+
+    #[test]
+    fn collection_test() {
+        test! {
+            single_threaded(|| unsafe {
+                {
+                    let mut own = OWNERSHIP.lock().expect("protect failed");
+                    own.check_objects();
+                }
+
+                // Force a garbage collect.
+                let test_size = INITIAL_PRESERVATION_SIZE + EXTRA_PRESERVATION_SIZE * 5;
+
+                // Make some test objects.
+                let sexp_pres = Rf_allocVector(VECSXP, test_size as R_xlen_t);
+                Rf_protect(sexp_pres);
+
+                let sexps = (0..test_size).map(|i| {
+                    let sexp = Rf_ScalarInteger(1);
+                    SET_VECTOR_ELT(sexp_pres, i as R_xlen_t, sexp);
+                    sexp
+                }).collect::<Vec<_>>();
+
+                for (i, sexp) in sexps.iter().enumerate() {
+                    protect(*sexp);
+                    if i % 2 == 0 {
+                        unprotect(*sexp);
+                    }
+                }
+
+                {
+                    let mut own = OWNERSHIP.lock().expect("protect failed");
+                    own.check_objects();
+                    own.garbage_collect();
+                    own.check_objects();
+                }
+
+                Rf_unprotect(1);
+            });
+        }
+    }
+}
diff --git a/extendr-api/src/prelude.rs b/extendr-api/src/prelude.rs
index a2b2c4291..e4bc2e65c 100644
--- a/extendr-api/src/prelude.rs
+++ b/extendr-api/src/prelude.rs
@@ -51,5 +51,5 @@ pub use ndarray::*;
 pub use extendr_macros::{extendr, extendr_module};
 
 pub use super::iter::{
-    EnvIter, IntegerIter, ListIter, LogicalIter, PairlistIter, PairlistTagIter, RealIter, StrIter,
+    EnvIter, Int, ListIter, Logical, PairlistIter, PairlistTagIter, Real, StrIter,
 };
diff --git a/extendr-api/src/robj/from_robj.rs b/extendr-api/src/robj/from_robj.rs
index 9983eb1b1..f00898144 100644
--- a/extendr-api/src/robj/from_robj.rs
+++ b/extendr-api/src/robj/from_robj.rs
@@ -153,9 +153,9 @@ macro_rules! impl_iter_from_robj {
 
 impl_iter_from_robj!(StrIter, as_str_iter, "Not a character vector.");
 impl_iter_from_robj!(ListIter, as_list_iter, "Not a list.");
-impl_iter_from_robj!(IntegerIter<'a>, as_integer_iter, "Not an integer vector.");
-impl_iter_from_robj!(RealIter<'a>, as_real_iter, "Not a real vector.");
-impl_iter_from_robj!(LogicalIter<'a>, as_logical_iter, "Not a logical vector.");
+impl_iter_from_robj!(Int, as_integer_iter, "Not an integer vector.");
+impl_iter_from_robj!(Real, as_real_iter, "Not a real vector.");
+impl_iter_from_robj!(Logical, as_logical_iter, "Not a logical vector.");
 
 /// Pass-through Robj conversion.
 impl<'a> FromRobj<'a> for Robj {
diff --git a/extendr-api/src/robj/into_robj.rs b/extendr-api/src/robj/into_robj.rs
index 4791a2d52..c2fdc341d 100644
--- a/extendr-api/src/robj/into_robj.rs
+++ b/extendr-api/src/robj/into_robj.rs
@@ -321,7 +321,7 @@ where
         let sexptype = I::Item::sexptype();
         if sexptype != 0 {
             let sexp = Rf_allocVector(sexptype, len as R_xlen_t);
-            R_PreserveObject(sexp);
+            ownership::protect(sexp);
             match sexptype {
                 REALSXP => {
                     let ptr = REAL(sexp);
@@ -497,23 +497,23 @@ impl_from_into_iter! {&'a [T]}
 impl_from_as_iterator! {Range<T>}
 impl_from_as_iterator! {RangeInclusive<T>}
 
-impl<'a> From<RealIter<'a>> for Robj {
+impl From<Real> for Robj {
     /// Convert a real iterator into a vector.
-    fn from(val: RealIter) -> Self {
+    fn from(val: Real) -> Self {
         val.collect_robj()
     }
 }
 
-impl<'a> From<IntegerIter<'a>> for Robj {
+impl From<Int> for Robj {
     /// Convert an integer iterator into a vector.
-    fn from(val: IntegerIter) -> Self {
+    fn from(val: Int) -> Self {
         val.collect_robj()
     }
 }
 
-impl<'a> From<LogicalIter<'a>> for Robj {
+impl From<Logical> for Robj {
     /// Convert a logical iterator into a vector.
-    fn from(val: LogicalIter) -> Self {
+    fn from(val: Logical) -> Self {
         val.collect_robj()
     }
 }
@@ -527,3 +527,10 @@ impl<'a> From<HashMap<&'a str, Robj>> for Robj {
         res
     }
 }
+
+impl<'a> From<Vec<Robj>> for Robj {
+    /// Convert a vector of Robj into a list.
+    fn from(val: Vec<Robj>) -> Self {
+        List(val.iter()).into()
+    }
+}
diff --git a/extendr-api/src/robj/mod.rs b/extendr-api/src/robj/mod.rs
index 7b4602ff9..2378ddba9 100644
--- a/extendr-api/src/robj/mod.rs
+++ b/extendr-api/src/robj/mod.rs
@@ -129,7 +129,13 @@ pub enum Robj {
 
 impl Clone for Robj {
     fn clone(&self) -> Self {
-        self.duplicate()
+        unsafe {
+            match *self {
+                Robj::Owned(sexp) => new_owned(sexp),
+                Robj::Borrowed(sexp) => new_borrowed(sexp),
+                Robj::Sys(sexp) => new_sys(sexp),
+            }
+        }
     }
 }
 
@@ -299,12 +305,9 @@ impl Robj {
     /// assert_eq!(tot, 6);
     /// }
     /// ```
-    pub fn as_integer_iter<'a>(&self) -> Option<IntegerIter<'a>>
-    where
-        Self: 'a,
-    {
+    pub fn as_integer_iter(&self) -> Option<Int> {
         if let Some(slice) = self.as_integer_slice() {
-            Some(slice.iter().cloned())
+            Some(Int::from_slice(self.to_owned(), slice))
         } else {
             None
         }
@@ -375,9 +378,9 @@ impl Robj {
     ///     assert_eq!((nt, nf, nna), (1, 1, 1));
     /// }
     /// ```
-    pub fn as_logical_iter(&self) -> Option<LogicalIter> {
+    pub fn as_logical_iter(&self) -> Option<Logical> {
         if let Some(slice) = self.as_logical_slice() {
-            Some(slice.iter().cloned())
+            Some(Logical::from_slice(self.to_owned(), slice))
         } else {
             None
         }
@@ -417,9 +420,9 @@ impl Robj {
     ///     assert_eq!(tot, 6.);
     /// }
     /// ```
-    pub fn as_real_iter(&self) -> Option<RealIter> {
+    pub fn as_real_iter(&self) -> Option<Real> {
         if let Some(slice) = self.as_real_slice() {
-            Some(slice.iter().cloned())
+            Some(Real::from_slice(self.to_owned(), slice))
         } else {
             None
         }
@@ -731,7 +734,7 @@ impl Robj {
     pub unsafe fn unprotected(self) -> Robj {
         match self {
             Robj::Owned(sexp) => {
-                single_threaded(|| R_ReleaseObject(sexp));
+                single_threaded(|| ownership::unprotect(sexp));
                 Robj::Borrowed(sexp)
             }
             _ => self,
@@ -904,11 +907,7 @@ impl Robj {
     ///    assert_eq!(dim, vec![2, 2]);
     /// }
     /// ```
-    pub fn dim<'a>(&self) -> Option<IntegerIter<'a>>
-    where
-        Self: 'a,
-        Robj: 'a,
-    {
+    pub fn dim(&self) -> Option<Int> {
         if let Some(dim) = self.get_attrib(dim_symbol()) {
             dim.as_integer_iter()
         } else {
@@ -1018,7 +1017,7 @@ impl Robj {
 
 #[doc(hidden)]
 pub unsafe fn new_owned(sexp: SEXP) -> Robj {
-    single_threaded(|| R_PreserveObject(sexp));
+    single_threaded(|| ownership::protect(sexp));
     Robj::Owned(sexp)
 }
 
@@ -1223,7 +1222,7 @@ impl Drop for Robj {
     fn drop(&mut self) {
         unsafe {
             match self {
-                Robj::Owned(sexp) => single_threaded(|| R_ReleaseObject(*sexp)),
+                Robj::Owned(sexp) => single_threaded(|| ownership::unprotect(*sexp)),
                 Robj::Borrowed(_) => (),
                 Robj::Sys(_) => (),
             }
diff --git a/extendr-api/src/robj/rinternals.rs b/extendr-api/src/robj/rinternals.rs
index 0c0a7bf08..ba7476608 100644
--- a/extendr-api/src/robj/rinternals.rs
+++ b/extendr-api/src/robj/rinternals.rs
@@ -111,7 +111,9 @@ impl Robj {
     /// test! {
     ///    let my_fun = base_env().find_function(sym!(ls)).unwrap();
     ///    assert_eq!(my_fun.is_function(), true);
-    ///    assert!(base_env().find_function(sym!(qwertyuiop)).is_none());
+    ///
+    ///    // Note: this may crash on some versions of windows which don't support unwinding.
+    ///    // assert!(base_env().find_function(sym!(qwertyuiop)).is_none());
     /// }
     /// ```
     pub fn find_function<K: Into<Robj>>(&self, key: K) -> Option<Robj> {
@@ -157,7 +159,9 @@ impl Robj {
     ///        .find_var(sym!(iris)).unwrap().eval_promise().unwrap();
     ///    assert_eq!(iris_dataframe.is_frame(), true);
     ///    assert_eq!(iris_dataframe.len(), 5);
-    ///    assert_eq!(global_env().find_var(sym!(imnotasymbol)), None);
+    ///
+    ///    // Note: this may crash on some versions of windows which don't support unwinding.
+    ///    //assert_eq!(global_env().find_var(sym!(imnotasymbol)), None);
     /// }
     /// ```
     pub fn find_var<K: Into<Robj>>(&self, key: K) -> Option<Robj> {
diff --git a/extendr-api/src/thread_safety.rs b/extendr-api/src/thread_safety.rs
index 65a0080b3..ded9a44df 100644
--- a/extendr-api/src/thread_safety.rs
+++ b/extendr-api/src/thread_safety.rs
@@ -96,7 +96,7 @@ pub fn throw_r_error<S: AsRef<str>>(s: S) {
 }
 
 /// Wrap an R function such as Rf_findFunction and convert errors and panics into results.
-/// ```
+/// ```ignore
 /// use extendr_api::prelude::*;
 /// test! {
 ///    let res = catch_r_error(|| unsafe {
@@ -138,6 +138,8 @@ where
         let cleandata = std::mem::transmute(&x);
         let cont = R_MakeUnwindCont();
         Rf_protect(cont);
+
+        // Note that catch_unwind does not work for 32 bit windows targets.
         let res = match std::panic::catch_unwind(|| {
             R_UnwindProtect(fun, data, cleanfun, cleandata, cont)
         }) {
diff --git a/extendr-api/src/wrapper.rs b/extendr-api/src/wrapper.rs
index 8c7ff3cbe..c0b1955ab 100644
--- a/extendr-api/src/wrapper.rs
+++ b/extendr-api/src/wrapper.rs
@@ -1,10 +1,10 @@
 //! Wrappers are lightweight proxies for references to R datatypes.
 //! They do not contain an Robj (see array.rs for an example of this).
 
+#[doc(hidden)]
 use crate::*;
 #[doc(hidden)]
 use libR_sys::*;
-#[doc(hidden)]
 
 /// Wrapper for creating symbols.
 ///
@@ -195,7 +195,7 @@ where
     T::IntoIter: ExactSizeIterator,
     T::Item: Into<Robj>,
 {
-    /// Make a list object from an array of Robjs.
+    /// Make a list object from an iterator of Robjs.
     /// ```
     /// use extendr_api::prelude::*;
     /// test! {
@@ -214,7 +214,7 @@ where
     T::IntoIter: ExactSizeIterator,
     T::Item: Into<Robj>,
 {
-    /// Make an expression object from an array of Robjs.
+    /// Make an expression object from an iterator of Robjs.
     /// ```
     /// use extendr_api::prelude::*;
     /// test! {
@@ -233,7 +233,7 @@ impl<'a> From<Raw<'a>> for Robj {
         single_threaded(|| unsafe {
             let val = val.0;
             let sexp = Rf_allocVector(RAWSXP, val.len() as R_xlen_t);
-            R_PreserveObject(sexp);
+            ownership::protect(sexp);
             let ptr = RAW(sexp);
             for (i, &v) in val.iter().enumerate() {
                 *ptr.offset(i as isize) = v;
@@ -377,7 +377,7 @@ where
     single_threaded(|| unsafe {
         let values = values.into_iter();
         let sexp = Rf_allocVector(sexptype, values.len() as R_xlen_t);
-        R_PreserveObject(sexp);
+        ownership::protect(sexp);
         for (i, val) in values.enumerate() {
             SET_VECTOR_ELT(sexp, i as R_xlen_t, val.into().get());
         }


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 82d0000ee..68242a98b 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -10,6 +10,11 @@ on:
       - main
       - master
 
+# cargo error handling is wrapped by ci-cargo, for simplicity and better logging.
+# To use ci-cargo in a step, source ci-cargo.ps1 (using `. ./ci-cargo.ps`, note the extra dot in front).
+# ci-cargo does not handle correctly -- separator, so wrap it in quotes ('--')
+# 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:
@@ -113,7 +118,6 @@ jobs:
         run: |
           Del alias:R
           echo "LD_LIBRARY_PATH=$(R -s -e 'cat(normalizePath(R.home()))')/lib" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
-
       # Check code formatting. As this doesn't depend on the platform, do this only on one platform.
       - name: Check code formatting
         if: matrix.config.check_fmt
@@ -123,38 +127,36 @@ jobs:
       # Required by Windows builds, does not affect other platforms
       - name: Build
         run: |
+          . ./ci-cargo.ps1
           foreach($target in ($env:BUILD_TARGETS).Split(',')) {
-            echo "::group::Build for $target target"
-
-            cargo build $(if($target -ne 'default') {"--target=$target"} ) 
-            if (!$?) { 
-              echo "::error::Build for $target target" ;
-              throw "Last exit code $LASTEXITCODE"
-            }
-
-            echo "::endgroup::"
+            ci-cargo build $(if($target -ne 'default') {"--target=$target"} ) -ActionName "Building for $target target"
           }
-
       # For each target in the BUILD_TARGETS comma-separated list, run cargo test with appropriate target
       # Required by Windows builds, does not affect other platforms
+      # ! ci-cargo requires '--' to be wrapped in quotes (passed as an explicit string)
       - name: Run tests
         run: |
+          . ./ci-cargo.ps1
           foreach($target in ($env:BUILD_TARGETS).Split(',')) {
-            echo "::group::Test for $target target"
-            
-            cargo test $(if($target -ne 'default') {"--target=$target"} ) -- --nocapture
-            if (!$?) { 
-              echo "::error::Test for $target target" ;
-              throw "Last exit code $LASTEXITCODE"
-            }
-
-            echo "::endgroup::"
+            ci-cargo test $(if($target -ne 'default') {"--target=$target"} ) '--' --nocapture -ActionName "Testing for $target target"
           }
 
+      - name: Obtain 'rextendr'
+        run : |
+          git clone https://github.com/extendr/rextendr ./tests/rextendr
+
+      # Dependencies from both test packages are combined
       - name: Query dependencies for integration testing
         run: |
           install.packages('remotes')
-          saveRDS(remotes::dev_package_deps(pkgdir = "tests/extendrtests", dependencies = TRUE), ".github/depends.Rds", version = 2)
+          saveRDS(
+            unique(
+              rbind(
+                remotes::dev_package_deps(pkgdir = "tests/extendrtests", dependencies = TRUE), 
+                remotes::dev_package_deps(pkgdir = "tests/rextendr", dependencies = TRUE))),
+            ".github/depends.Rds", 
+            version = 2
+          )
           writeLines(sprintf("R-%i.%i", getRversion()$major, getRversion()$minor), ".github/R-version")
         shell: Rscript {0}
 
@@ -165,29 +167,97 @@ jobs:
           key: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1-${{ hashFiles('.github/depends.Rds') }}
           restore-keys: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1-
 
-      - name: Install R dependencies for integration testing
+      # Currently rextendr requires R > 4, so if R version is older (e.g. 3.6.3), rextendr tests are omitted
+      # Regex is used to inject absolute path into Cargo.toml
+      - name: Install dependencies & configure R for integration testing
         run: |
           remotes::install_deps(pkgdir = "tests/extendrtests", dependencies = TRUE)
+          if (as.integer(R.version$major) >= 4) { 
+            remotes::install_deps(pkgdir = "tests/rextendr", dependencies = TRUE)
+          }
           remotes::install_cran("rcmdcheck")
+
+          api_path <- normalizePath(file.path(getwd(), "extendr-api"), winslash = "/")
+          toml_path <- file.path(getwd(), "tests", "extendrtests", "src", "rust", "Cargo.toml")
+          lines <- readLines(toml_path)
+          lines <- gsub(
+            "(^\\s*extendr-api\\s*=\\s*\\{\\s*path\\s*=\\s*\")(.*?)(\"\\s*\\})",
+            paste0("\\1", api_path, "\\3"),
+            lines
+          )
+          writeLines(lines, toml_path)
         shell: Rscript {0}
         
       # Windows: modified PATH will launch 32-bit Rscript if the build targets only i686/i386, so no conflict will arise
       # Rust pseudo multi-targeting: R CMD CHECK receives an extra argument '--no-multiarch' if BUILD_TARGETS is not 'default' and does not contain any commas
-      - name: Run R integration tests
-        id: r_integration_tests
+      # To enable RStudio support, extendrtests should be installable from 'extendr/tests/extendrtests',
+      # for this work directory should be modified and then reverted back
+      - name: Run R integration tests using {extendrtests}
+        id: extendrtests_check
         env:
           _R_CHECK_CRAN_INCOMING_REMOTE_: false
         run: |
-          setwd("tests/extendrtests")
-          rcmdcheck::rcmdcheck(path = ".", args = c("--no-manual", "--as-cran"), error_on = "warning", check_dir = "../../../")
+          cat("::group::Checking 'extendrtests'\n")
+          rcmdcheck::rcmdcheck(
+            path = "tests/extendrtests", 
+            args = c("--no-manual", "--as-cran", "--force-multiarch"),
+            error_on = "warning", 
+            check_dir = "extendrtests_check"
+          )
+          cat("::endgroup::\n")
         shell: Rscript {0}
         
-      - name: Upload check results from R integration tests
-        if: failure()
+      # If R is older than 4.0, rextendr is not tested and a warning is emitted. 
+      # 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: Run R integration tests using {rextendr}
+        id: rextendr_check
+        env:
+          _R_CHECK_CRAN_INCOMING_REMOTE_: false
+        run: |
+          if (as.integer(R.version$major) < 4) {
+            cat("::warning:: R version is not compatible with 'rextendr'\n")
+          } else {
+            cat("::group::Checking '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 = "/"),
+                  "\" }"),
+                sep = ";")
+
+            Sys.setenv(REXTENDR_PATCH_CRATES_IO = patch.crates_io)
+            rcmdcheck::rcmdcheck(
+              path = "tests/rextendr", 
+              args = c("--no-manual", "--as-cran", "--force-multiarch"), 
+              error_on = "warning", 
+              check_dir = "rextendr_check")
+            cat("::endgroup::\n")
+          }
+        shell: Rscript {0}
+
+
+      - name: Upload extendrtests check results from R integration tests
+        if: steps.extendrtests_check.outcome != 'success'
+        uses: actions/upload-artifact@main
+        with:
+          name:  extendrtests-${{ matrix.config.os }}-R-${{ matrix.config.r }}-rust-${{ matrix.config.rust-version }}
+          path: extendrtests_check
+
+      - name: Upload rextendr check results from R integration tests
+        if: steps.rextendr_check.outcome != 'success'
         uses: actions/upload-artifact@main
         with:
-          name:  ${{ matrix.config.os }}-R-${{ matrix.config.r }}-rust-${{ matrix.config.rust-version }}
-          path: check
+          name:  rextendr-${{ matrix.config.os }}-R-${{ matrix.config.r }}-rust-${{ matrix.config.rust-version }}
+          path: rextendr_check
 
 
   # All tests under this job are run with R devel and freshly generated bindings.
@@ -199,7 +269,7 @@ jobs:
   bindgen_no_cross_compile:
     runs-on: ${{ matrix.config.os }}
 
-    name: ${{ matrix.config.os }} w/ bindgen no cross-compile (R-${{ matrix.config.r }} rust-${{ matrix.config.rust-version }})
+    name: ${{ matrix.config.os }} w/ bindgen (R-${{ matrix.config.r }} rust-${{ matrix.config.rust-version }})
             
     strategy:
       fail-fast: false
@@ -223,8 +293,6 @@ jobs:
     steps:
       - uses: actions/checkout@v2
       
-      # Default action allows to install only one toolchain,
-      # so Rust is set up manually.
       # 1. Update rustup
       # 2. For each target add respective toolchain &
       #    update target for that toolchain
@@ -269,7 +337,6 @@ jobs:
           r-version: ${{ matrix.config.r }}
           windows-path-include-mingw: false
       
-
       # 1. Inspect targets. If empty, assume 'x86_64' arch
       # 1.5 Make symlink to please i686 toolchain (https://github.com/rust-lang/cargo/issues/8990)
       # 2. Install msys2 packages (--needed skips already installed)
@@ -318,6 +385,7 @@ jobs:
       # 3.x Run build commands, checking for errors, providing explicit toolchain & target
       - name: Build
         run: |
+          . ./ci-cargo.ps1
           $targets = $env:BUILD_TARGETS.Split(',')
           $libclang_paths = $env:LIBCLANG_PATHS.Split([System.IO.Path]::PathSeparator)
           $itt = 0..($targets.Length - 1)
@@ -337,33 +405,21 @@ jobs:
               $env:LIBCLANG_PATH = $null
             }
 
-            echo "::group::Build extendr-api for $toolchain"
-
-            cargo +$toolchain build --manifest-path extendr-api/Cargo.toml --features tests-all $(if($target -ne 'default') {"--target=$target"} )
-            if (!$?) { 
-              echo "::error::Build extendr-api for $toolchain" 
-              throw "Last exit code $LASTEXITCODE"
-            }
-
-            echo "::endgroup::"
-          
-            echo "::group::Build extendr-engine for $toolchain"
-
-            cargo +$toolchain build --manifest-path extendr-engine/Cargo.toml --features tests-all $(if($target -ne 'default') {"--target=$target"} ) 
-            if (!$?) { 
-              echo "::error::Build extendr-engine for $toolchain"
-              throw "Last exit code $LASTEXITCODE"
-            }
 
-            echo "::endgroup::"
+            ci-cargo +$toolchain build --manifest-path extendr-api/Cargo.toml --features tests-all $(if($target -ne 'default') {"--target=$target"} ) -ActionName "Build extendr-api for $toolchain"
+         
+            ci-cargo +$toolchain build --manifest-path extendr-engine/Cargo.toml --features tests-all $(if($target -ne 'default') {"--target=$target"} ) -ActionName "Build extendr-engine for $toolchain"
+            
           }
         env: 
           BUILD_TARGETS: ${{ join(matrix.config.rust-targets, ',') }}
           RUST_TOOLCHAIN: ${{ matrix.config.rust-version }}
 
       # Identical to the build process, but running more test commands
+      # ! ci-cargo requires '--' to be wrapped in quotes (passed as an explicit string)
       - name: Run tests
         run: |
+          . ./ci-cargo.ps1
           $targets = $env:BUILD_TARGETS.Split(',')
           $libclang_paths = $env:LIBCLANG_PATHS.Split([System.IO.Path]::PathSeparator)
           $itt = 0..($targets.Length - 1)
@@ -383,45 +439,14 @@ jobs:
               $env:LIBCLANG_PATH = $null
             }
 
-            echo "::group::Test extendr-engine \w tests-all for $target target"
-
-            cargo +$toolchain test --manifest-path extendr-engine/Cargo.toml --features tests-all $(if($target -ne 'default') {"--target=$target"} ) -- --nocapture 
-            if (!$?) { 
-              echo "::error::Test extendr-engine \w tests-all for $target target" ;
-              throw "Last exit code $LASTEXITCODE"
-            }
-
-            echo "::endgroup::"
-          
-            echo "::group::Test extendr-api \w tests-all for $target target"
-
-            cargo +$toolchain test --manifest-path extendr-api/Cargo.toml --features tests-all $(if($target -ne 'default') {"--target=$target"} ) -- --nocapture 
-            if (!$?) { 
-              echo "::error::Test extendr-api \w tests-all for $target target" ;
-              throw "Last exit code $LASTEXITCODE"
-            }
-
-            echo "::endgroup::"
-                    
-            echo "::group::Test extendr-api \w tests-minimal for $target target"
-
-            cargo +$toolchain test --manifest-path extendr-api/Cargo.toml --features tests-minimal $(if($target -ne 'default') {"--target=$target"} ) -- --nocapture
-            if (!$?) { 
-              echo "::error::Test extendr-api \w tests-minimal for $target target" ;
-              throw "Last exit code $LASTEXITCODE"
-            }
-
-            echo "::endgroup::"
-          
-            echo "::group::Test extendr-macros for $target target"
+            ci-cargo +$toolchain test --manifest-path extendr-engine/Cargo.toml --features tests-all $(if($target -ne 'default') {"--target=$target"} ) '--' --nocapture -ActionName "Test extendr-engine \w tests-all for $target target"
+            
+            ci-cargo +$toolchain test --manifest-path extendr-api/Cargo.toml --features tests-all $(if($target -ne 'default') {"--target=$target"} ) '--' --nocapture -ActionName "Test extendr-api \w tests-all for $target target"
 
-            cargo +$toolchain test --manifest-path extendr-macros/Cargo.toml  $(if($target -ne 'default') {"--target=$target"} ) -- --nocapture
-            if (!$?) { 
-              echo "::error::Test extendr-macros for $target target" ;
-              throw "Last exit code $LASTEXITCODE"
-            }
+            ci-cargo +$toolchain test --manifest-path extendr-api/Cargo.toml --features tests-minimal $(if($target -ne 'default') {"--target=$target"} ) '--' --nocapture -ActionName "Test extendr-api \w tests-minimal for $target target"
+                      
+            ci-cargo +$toolchain test --manifest-path extendr-macros/Cargo.toml  $(if($target -ne 'default') {"--target=$target"} ) '--' --nocapture -ActionName "Test extendr-macros for $target target"
             
-            echo "::endgroup::"
           }
         env: 
           BUILD_TARGETS: ${{ join(matrix.config.rust-targets, ',') }}
diff --git a/extendr-api/src/robj/tests.rs b/extendr-api/src/robj/tests.rs
index b2872283d..de75734ab 100644
--- a/extendr-api/src/robj/tests.rs
+++ b/extendr-api/src/robj/tests.rs
@@ -227,7 +227,7 @@ fn output_iterator_test() {
 // Test that we can use Iterators as the input to functions.
 // eg.
 // #[extendr]
-// fn fred(a: RealIter, b: RealIter) -> RealIter {
+// fn fred(a: Real, b: Real) -> Robj {
 // }
 #[test]
 fn input_iterator_test() {
@@ -244,18 +244,18 @@ fn input_iterator_test() {
 
         let src: &[i32] = &[1, 2, 3];
         let robj = Robj::from(src);
-        let iter = <IntegerIter>::from_robj(&robj).unwrap();
+        let iter = <Int>::from_robj(&robj).unwrap();
         assert_eq!(iter.collect::<Vec<_>>(), src);
 
         let src: &[f64] = &[1., 2., 3.];
         let robj = Robj::from(src);
-        let iter = <RealIter>::from_robj(&robj).unwrap();
+        let iter = <Real>::from_robj(&robj).unwrap();
         assert_eq!(iter.collect::<Vec<_>>(), src);
 
         /*
         let src: &[Bool] = &[TRUE, FALSE, TRUE];
         let robj = Robj::from(src);
-        let iter = <LogicalIter>::from_robj(&robj).unwrap();
+        let iter = <Logical>::from_robj(&robj).unwrap();
         assert_eq!(iter.collect::<Vec<_>>(), src);
         */
     }
diff --git a/tests/extendrtests/NAMESPACE b/tests/extendrtests/NAMESPACE
index 3c3c9bb94..f3d6dc289 100644
--- a/tests/extendrtests/NAMESPACE
+++ b/tests/extendrtests/NAMESPACE
@@ -1,6 +1,8 @@
 # Generated by roxygen2: do not edit by hand
 
+S3method("$","__MyClass")
 S3method("$",MyClass)
+S3method("$",MyClassUnexported)
 S3method("$",MySubmoduleClass)
 export("__00__special_function_name")
 export(MyClass)
diff --git a/tests/extendrtests/R/extendr-wrappers.R b/tests/extendrtests/R/extendr-wrappers.R
index 3321bfe76..9063bccae 100644
--- a/tests/extendrtests/R/extendr-wrappers.R
+++ b/tests/extendrtests/R/extendr-wrappers.R
@@ -85,6 +85,7 @@ MyClass$me <- function() .Call(wrap__MyClass__me, self)
 
 `__MyClass`$`__name_test` <- function() invisible(.Call(wrap____MyClass____name_test, self))
 
+#' @export
 `$.__MyClass` <- function (self, name) { func <- `__MyClass`[[name]]; environment(func) <- environment(); func }
 
 #' Class for testing (unexported)
@@ -94,6 +95,7 @@ MyClassUnexported$new <- function() .Call(wrap__MyClassUnexported__new)
 
 MyClassUnexported$a <- function() .Call(wrap__MyClassUnexported__a, self)
 
+#' @export
 `$.MyClassUnexported` <- function (self, name) { func <- MyClassUnexported[[name]]; environment(func) <- environment(); func }
 
 #' Class for testing (exported)
diff --git a/tests/extendrtests/src/rust/Cargo.toml b/tests/extendrtests/src/rust/Cargo.toml
index 035e26cb3..562b44c59 100644
--- a/tests/extendrtests/src/rust/Cargo.toml
+++ b/tests/extendrtests/src/rust/Cargo.toml
@@ -16,10 +16,10 @@ crate-type = ["staticlib"]
 extendr-api = "*"
 
 [patch.crates-io]
-## Build against local extendr sources. Check locally by 
-## running rcmdcheck::rcmdcheck(check_dir = "../../../") from
-## the root of extendrtests.
-extendr-api = { path = "../../../../../extendr/extendr-api/"}
+## This is configured to work with RStudio features.
+## Replace by absolute path to simplify testing.
+## CI overrides this path.
+extendr-api = { path = "../../../../../extendr/extendr-api"}
 
 ## Build against current extendr version on github. Not recommended
 ## for development work.