Repository: extendr/extendr
Pull request #121 (2021-01-08 17:56:53)

Implement unwind protection by converting R errors to panics.
Wrap an R function such as Rf_findFunction and convert errors and panics into results.
```
use extendr_api::*;
test! {
   let res = catch_r_error(|| unsafe {
       throw_r_error("bad things!")
       std::ptr::null_mut()
   });
   assert_eq!(res.is_ok(), true);
}
```



Patch:
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index 45dda971e..7f6fd6aaf 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -6,7 +6,7 @@
         {
             "label": "test",
             "type": "shell",
-            "command": "cd extendr-api; clear; cargo test -- --nocapture && cargo doc --target-dir /tmp/doc/",
+            "command": "cd extendr-api; clear; cargo test --all-features -- --nocapture && cargo doc --target-dir /tmp/doc/",
             "problemMatcher": [
                 "$rustc"
             ],
diff --git a/extendr-api/src/error.rs b/extendr-api/src/error.rs
index e8d12c08a..72b136d0b 100644
--- a/extendr-api/src/error.rs
+++ b/extendr-api/src/error.rs
@@ -2,6 +2,7 @@
 
 use libR_sys::*;
 use std::os::raw;
+use crate::*;
 
 static mut R_ERROR_BUF: Vec<u8> = Vec::new();
 
@@ -24,9 +25,11 @@ pub fn unwrap_or_throw<T>(r: std::result::Result<T, &'static str>) -> T {
 
 #[derive(Debug, PartialEq)]
 pub enum Error {
+    Panic{},
     NotFound,
     NotAVectorType,
-    ParseError,
+    EvalError{ code: Robj, error: i32 },
+    ParseError{ code: String, status: u32 },
     Other(String),
 }
 
@@ -56,6 +59,12 @@ impl From<&str> for Error {
     }
 }
 
+impl From<String> for Error {
+    fn from(err: String) -> Error {
+        Error::Other(err)
+    }
+}
+
 // NoneError is unstable.
 //
 // impl From<std::option::NoneError> for Error {
diff --git a/extendr-api/src/functions.rs b/extendr-api/src/functions.rs
index d24f3d327..b8962609c 100644
--- a/extendr-api/src/functions.rs
+++ b/extendr-api/src/functions.rs
@@ -364,6 +364,20 @@ pub fn blank_scalar_string() -> Robj {
     unsafe { new_sys(R_BlankScalarString) }
 }
 
+/// Special "NA" string that represents null strings.
+/// ```
+/// use extendr_api::*;
+/// test! {
+///     assert!(na_str().as_ptr() != "NA".as_ptr());
+///     assert_eq!(na_str(), "NA");
+///     assert_eq!("NA".is_na(), false);
+///     assert_eq!(na_str().is_na(), true);
+/// }
+/// ```
+pub fn na_str() -> &'static str {
+    unsafe { std::str::from_utf8_unchecked(&[b'N', b'A']) }
+}
+
 /// Extendr test harness.
 ///
 /// See also [test!]
diff --git a/extendr-api/src/lib.rs b/extendr-api/src/lib.rs
index 1b399ce55..47ae3ee32 100644
--- a/extendr-api/src/lib.rs
+++ b/extendr-api/src/lib.rs
@@ -209,7 +209,7 @@ pub use lang::*;
 pub use logical::*;
 pub use rmacros::*;
 pub use robj::*;
-pub use thread_safety::{handle_panic, single_threaded};
+pub use thread_safety::{handle_panic, single_threaded, throw_r_error, catch_r_error};
 pub use wrapper::*;
 pub use matrix::*;
 
@@ -218,6 +218,9 @@ pub use robj_ndarray::*;
 
 pub use extendr_macros::*;
 
+#[doc(hidden)]
+pub use std::collections::HashMap;
+
 #[doc(hidden)]
 pub use libR_sys::DllInfo;
 
@@ -290,6 +293,13 @@ impl IsNA for Bool {
     }
 }
 
+impl IsNA for &str {
+    /// Check for NA in a string by address.
+    fn is_na(&self) -> bool {
+        self.as_ptr() == na_str().as_ptr()
+    }
+}
+
 #[doc(hidden)]
 pub fn print_r_output<T: Into<Vec<u8>>>(s: T) {
     let cs = CString::new(s).expect("NulError");
@@ -399,6 +409,33 @@ mod tests {
         123.
     }
 
+    #[extendr]
+    pub fn f64_slice(x: &[f64]) -> &[f64] { x }
+
+    #[extendr]
+    pub fn i32_slice(x: &[i32]) -> &[i32] { x }
+
+    #[extendr]
+    pub fn bool_slice(x: &[Bool]) -> &[Bool] { x }
+
+    #[extendr]
+    pub fn f64_iter(x: RealIter) -> RealIter { x }
+
+    #[extendr]
+    pub fn i32_iter(x: IntegerIter) -> IntegerIter { x }
+
+    #[extendr]
+    pub fn bool_iter(x: LogicalIter) -> LogicalIter { x }
+
+    #[extendr]
+    pub fn symbol(x: Symbol) -> Symbol { x }
+
+    #[extendr]
+    pub fn matrix(x: RMatrix<&[f64]>) -> RMatrix<&[f64]> { x }
+
+    #[extendr]
+    pub fn hash_map(x: HashMap<&str, Robj>) -> HashMap<&str, Robj> { x }
+
     struct Person {
         pub name: String,
     }
@@ -481,6 +518,82 @@ mod tests {
         }
     }
 
+    #[test]
+    fn class_wrapper_test() {
+        test! {
+            let mut person = Person::new();
+            person.set_name("fred");
+            let robj = r!(person);
+            assert_eq!(robj.check_external_ptr("Person"), true);
+            let person2 = <&Person>::from_robj(&robj).unwrap();
+            assert_eq!(person2.name(), "fred");
+        }
+    }
+
+    #[test]
+    fn slice_test() {
+        test! {
+            unsafe {
+                // #[extendr]
+                // pub fn f64_slice(x: &[f64]) -> &[f64] { x }
+
+                let robj = r!([1., 2., 3.]);
+                assert_eq!(new_owned(wrap__f64_slice(robj.get())), robj);
+            
+                // #[extendr]
+                // pub fn i32_slice(x: &[i32]) -> &[i32] { x }
+            
+                let robj = r!([1, 2, 3]);
+                assert_eq!(new_owned(wrap__i32_slice(robj.get())), robj);
+            
+                // #[extendr]
+                // pub fn bool_slice(x: &[Bool]) -> &[Bool] { x }
+            
+                let robj = r!([TRUE, FALSE, TRUE]);
+                assert_eq!(new_owned(wrap__bool_slice(robj.get())), robj);
+            
+                // #[extendr]
+                // pub fn f64_iter(x: RealIter) -> RealIter { 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 }
+            
+                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 }
+            
+                let robj = r!([TRUE, FALSE, TRUE]);
+                assert_eq!(new_owned(wrap__bool_iter(robj.get())), robj);
+            
+                // #[extendr]
+                // pub fn symbol(x: Symbol) -> Symbol { x }
+            
+                let robj = sym!(fred);
+                assert_eq!(new_owned(wrap__symbol(robj.get())), robj);
+            
+                // #[extendr]
+                // pub fn matrix(x: Matrix<&[f64]>) -> Matrix<&[f64]> { x }
+            
+                let m = RMatrix::new([1., 2.], 1, 2);
+                let robj = r!(m);
+                assert_eq!(new_owned(wrap__matrix(robj.get())), robj);
+            
+                // #[extendr]
+                // pub fn hash_map(x: HashMap<&str, Robj>) -> HashMap<&str, Robj> { x }
+                let robj = r!(List(&[1, 2]));
+                robj.set_attrib(names_symbol(), r!(["a", "b"]));
+                let res = new_owned(wrap__hash_map(robj.get()));
+                assert_eq!(res.len(), 2);
+            }
+        }
+    
+    }
+
     #[test]
     fn r_output_test() {
         // R equivalent
@@ -501,4 +614,12 @@ mod tests {
         let result = R!(test_con).unwrap();
         assert_eq!(result, r!("Hello world"));
     }
+
+    #[test]
+    fn test_na_str() {
+        assert!(na_str().as_ptr() != "NA".as_ptr());
+        assert_eq!(na_str(), "NA");
+        assert_eq!("NA".is_na(), false);
+        assert_eq!(na_str().is_na(), true);
+    }
 }
diff --git a/extendr-api/src/matrix.rs b/extendr-api/src/matrix.rs
index ceff8c137..f66ac5926 100644
--- a/extendr-api/src/matrix.rs
+++ b/extendr-api/src/matrix.rs
@@ -8,7 +8,7 @@ use std::ops::{Index, IndexMut};
 /// ```
 /// use extendr_api::*;
 /// test! {
-///     let matrix = Matrix::new([
+///     let matrix = RMatrix::new([
 ///          1., 2., 3.,
 ///          4., 5., 6.], 3, 2);
 ///     let robj = r!(matrix);
@@ -16,21 +16,21 @@ use std::ops::{Index, IndexMut};
 ///     assert_eq!(robj.nrows(), 3);
 ///     assert_eq!(robj.ncols(), 2);
 ///
-///     let matrix2 : Matrix<&[f64]> = robj.as_matrix().ok_or("error")?;
+///     let matrix2 : RMatrix<&[f64]> = robj.as_matrix().ok_or("error")?;
 ///     assert_eq!(matrix2.data().len(), 6);
 ///     assert_eq!(matrix2.nrows(), 3);
 ///     assert_eq!(matrix2.ncols(), 2);
 /// }
 /// ```
 #[derive(Debug, PartialEq)]
-pub struct Array<T, D> {
+pub struct RArray<T, D> {
     data: T,
     dim: D,
 }
 
-pub type Vector<T> = Array<T, [usize; 1]>;
-pub type Matrix<T> = Array<T, [usize; 2]>;
-pub type Matrix3D<T> = Array<T, [usize; 3]>;
+pub type RColumn<T> = RArray<T, [usize; 1]>;
+pub type RMatrix<T> = RArray<T, [usize; 2]>;
+pub type RMatrix3D<T> = RArray<T, [usize; 3]>;
 
 const BASE: usize = 0;
 
@@ -39,7 +39,7 @@ trait Offset<D> {
     fn offset(&self, idx: D) -> usize;
 }
 
-impl<T> Offset<[usize; 1]> for Array<T, [usize; 1]> {
+impl<T> Offset<[usize; 1]> for RArray<T, [usize; 1]> {
     /// Get the offset into the array for a given index.
     fn offset(&self, index: [usize; 1]) -> usize {
         if index[0] - BASE > self.dim[0] {
@@ -49,7 +49,7 @@ impl<T> Offset<[usize; 1]> for Array<T, [usize; 1]> {
     }
 }
 
-impl<T> Offset<[usize; 2]> for Array<T, [usize; 2]> {
+impl<T> Offset<[usize; 2]> for RArray<T, [usize; 2]> {
     /// Get the offset into the array for a given index.
     fn offset(&self, index: [usize; 2]) -> usize {
         if index[0] - BASE > self.dim[0] {
@@ -62,23 +62,23 @@ impl<T> Offset<[usize; 2]> for Array<T, [usize; 2]> {
     }
 }
 
-impl<T> Offset<[usize; 3]> for Array<T, [usize; 3]> {
+impl<T> Offset<[usize; 3]> for RArray<T, [usize; 3]> {
     /// Get the offset into the array for a given index.
     fn offset(&self, index: [usize; 3]) -> usize {
         if index[0] - BASE > self.dim[0] {
-            panic!("Matrix3D index: row overflow");
+            panic!("RMatrix3D index: row overflow");
         }
         if index[1] - BASE > self.dim[1] {
-            panic!("Matrix3D index: column overflow");
+            panic!("RMatrix3D index: column overflow");
         }
         if index[2] - BASE > self.dim[2] {
-            panic!("Matrix3D index: submatrix overflow");
+            panic!("RMatrix3D index: submatrix overflow");
         }
         (index[0] - BASE) + self.dim[0] * (index[1] - BASE + self.dim[1] * (index[2] - BASE))
     }
 }
 
-impl<T, D> Array<T, D> {
+impl<T, D> RArray<T, D> {
     /// Get the underlying data fro this array.
     pub fn data(&self) -> &T {
         &self.data
@@ -90,7 +90,7 @@ impl<T, D> Array<T, D> {
     }
 }
 
-impl<T> Vector<T> {
+impl<T> RColumn<T> {
     /// Make a new vector type.
     pub fn new(data: T, nrows: usize) -> Self {
         let dim = [nrows];
@@ -103,7 +103,7 @@ impl<T> Vector<T> {
     }
 }
 
-impl<T> Matrix<T> {
+impl<T> RMatrix<T> {
     /// Create a new matrix wrapper.
     pub fn new(data: T, nrows: usize, ncols: usize) -> Self {
         let dim = [nrows, ncols];
@@ -121,7 +121,7 @@ impl<T> Matrix<T> {
     }
 }
 
-impl<T> Matrix3D<T> {
+impl<T> RMatrix3D<T> {
     /// Create a new matrix wrapper.
     pub fn new(data: T, nrows: usize, ncols: usize, nsub: usize) -> Self {
         let dim = [nrows, ncols, nsub];
@@ -144,21 +144,21 @@ impl<T> Matrix3D<T> {
     }
 }
 
-impl<T> From<Vector<T>> for Robj
+impl<T> From<RColumn<T>> for Robj
 where
     T: Into<Robj>,
 {
-    fn from(array: Vector<T>) -> Self {
+    fn from(array: RColumn<T>) -> Self {
         let res = array.data.into();
         res
     }
 }
 
-impl<T> From<Array<T, [usize; 2]>> for Robj
+impl<T> From<RArray<T, [usize; 2]>> for Robj
 where
     T: Into<Robj>,
 {
-    fn from(array: Array<T, [usize; 2]>) -> Self {
+    fn from(array: RArray<T, [usize; 2]>) -> Self {
         let res = array.data.into();
         let dim = [array.dim[0] as i32, array.dim[1] as i32];
         res.set_attrib(Symbol("dim"), dim);
@@ -167,11 +167,11 @@ where
     }
 }
 
-impl<T> From<Array<T, [usize; 3]>> for Robj
+impl<T> From<RArray<T, [usize; 3]>> for Robj
 where
     T: Into<Robj>,
 {
-    fn from(array: Array<T, [usize; 3]>) -> Self {
+    fn from(array: RArray<T, [usize; 3]>) -> Self {
         let res = array.data.into();
         let dim = [
             array.dim[0] as i32,
@@ -185,31 +185,31 @@ where
 }
 
 impl Robj {
-    pub fn as_vector<'a, E>(&self) -> Option<Vector<&'a [E]>>
+    pub fn as_vector<'a, E>(&self) -> Option<RColumn<&'a [E]>>
     where
         Self: AsTypedSlice<'a, E>,
     {
         if let Some(data) = self.as_typed_slice() {
             let dim = [self.nrows() as usize];
-            return Some(Array { data, dim });
+            return Some(RArray { data, dim });
         }
         None
     }
 
-    pub fn as_matrix<'a, E>(&self) -> Option<Matrix<&'a [E]>>
+    pub fn as_matrix<'a, E>(&self) -> Option<RMatrix<&'a [E]>>
     where
         Self: AsTypedSlice<'a, E>,
     {
         if self.is_matrix() {
             if let Some(data) = self.as_typed_slice() {
                 let dim = [self.nrows() as usize, self.ncols() as usize];
-                return Some(Array { data, dim });
+                return Some(RArray { data, dim });
             }
         }
         None
     }
 
-    pub fn as_matrix3d<'a, E>(&self) -> Option<Matrix3D<&'a [E]>>
+    pub fn as_matrix3d<'a, E>(&self) -> Option<RMatrix3D<&'a [E]>>
     where
         Self: AsTypedSlice<'a, E>,
     {
@@ -218,7 +218,7 @@ impl Robj {
                 if let Some(dim) = self.dim() {
                     let dim: Vec<_> = dim.cloned().collect();
                     let dim = [dim[0] as usize, dim[1] as usize, dim[2] as usize];
-                    return Some(Array { data, dim });
+                    return Some(RArray { data, dim });
                 }
             }
         }
@@ -226,7 +226,7 @@ impl Robj {
     }
 }
 
-impl<T> Index<[usize; 2]> for Array<T, [usize; 2]>
+impl<T> Index<[usize; 2]> for RArray<T, [usize; 2]>
 where
     T: Index<usize>,
 {
@@ -238,7 +238,7 @@ where
     /// ```
     /// use extendr_api::*;
     /// test! {
-    ///     let matrix = Matrix::new(vec![
+    ///     let matrix = RMatrix::new(vec![
     ///          1., 2., 3.,
     ///          4., 5., 6.], 3, 2);
     ///     assert_eq!(matrix[[0, 0]], 1.);
@@ -251,7 +251,7 @@ where
     }
 }
 
-impl<T> IndexMut<[usize; 2]> for Array<T, [usize; 2]>
+impl<T> IndexMut<[usize; 2]> for RArray<T, [usize; 2]>
 where
     T: IndexMut<usize>,
 {
@@ -261,11 +261,11 @@ where
     /// ```
     /// use extendr_api::*;
     /// test! {
-    ///     let mut matrix = Matrix::new(vec![0.; 6], 3, 2);
+    ///     let mut matrix = RMatrix::new(vec![0.; 6], 3, 2);
     ///     matrix[[0, 0]] = 1.;
     ///     matrix[[1, 0]] = 2.;
     ///     matrix[[2, 0]] = 3.;
-    ///     assert_eq!(matrix, Matrix::new(vec![1., 2., 3., 0., 0., 0.], 3, 2));
+    ///     assert_eq!(matrix, RMatrix::new(vec![1., 2., 3., 0., 0., 0.], 3, 2));
     /// }
     /// ```
     fn index_mut(&mut self, index: [usize; 2]) -> &mut Self::Output {
@@ -277,35 +277,35 @@ where
 #[test]
 fn matrix_ops() {
     test! {
-        let vector = Vector::new([1., 2., 3.], 3);
+        let vector = RColumn::new([1., 2., 3.], 3);
         let robj = r!(vector);
         assert_eq!(robj.is_vector(), true);
         assert_eq!(robj.nrows(), 3);
 
-        let vector2 : Vector<&[f64]> = robj.as_vector().ok_or("expected array")?;
+        let vector2 : RColumn<&[f64]> = robj.as_vector().ok_or("expected array")?;
         assert_eq!(vector2.data().len(), 3);
         assert_eq!(vector2.nrows(), 3);
 
-        let matrix = Matrix::new([
+        let matrix = RMatrix::new([
             1., 2., 3.,
             4., 5., 6.], 3, 2);
         let robj = r!(matrix);
         assert_eq!(robj.is_matrix(), true);
         assert_eq!(robj.nrows(), 3);
         assert_eq!(robj.ncols(), 2);
-        let matrix2 : Matrix<&[f64]> = robj.as_matrix().ok_or("expected matrix")?;
+        let matrix2 : RMatrix<&[f64]> = robj.as_matrix().ok_or("expected matrix")?;
         assert_eq!(matrix2.data().len(), 6);
         assert_eq!(matrix2.nrows(), 3);
         assert_eq!(matrix2.ncols(), 2);
 
-        let array = Matrix3D::new([
+        let array = RMatrix3D::new([
             1., 2.,  3., 4.,
             5.,  6., 7., 8.], 2, 2, 2);
         let robj = r!(array);
         assert_eq!(robj.is_array(), true);
         assert_eq!(robj.nrows(), 2);
         assert_eq!(robj.ncols(), 2);
-        let array2 : Matrix3D<&[f64]> = robj.as_matrix3d().ok_or("expected matrix3d")?;
+        let array2 : RMatrix3D<&[f64]> = robj.as_matrix3d().ok_or("expected matrix3d")?;
         assert_eq!(array2.data().len(), 8);
         assert_eq!(array2.nrows(), 2);
         assert_eq!(array2.ncols(), 2);
diff --git a/extendr-api/src/robj/from_robj.rs b/extendr-api/src/robj/from_robj.rs
index e6ea191d4..a2c64f1cd 100644
--- a/extendr-api/src/robj/from_robj.rs
+++ b/extendr-api/src/robj/from_robj.rs
@@ -234,3 +234,58 @@ impl<'a> FromRobj<'a> for Option<String> {
         }
     }
 }
+
+impl<'a, T> FromRobj<'a> for &'a [T]
+where
+    Robj : AsTypedSlice<'a, T>
+{
+    fn from_robj(robj: &'a Robj) -> std::result::Result<Self, &'static str> {
+        if let Some(slice) = robj.as_typed_slice() {
+            Ok(slice)
+        } else {
+            Err("Expected a vector type.")
+        }
+    }
+}
+
+// Symbol input parameters.
+impl<'a> FromRobj<'a> for Symbol<'a> {
+    fn from_robj(robj: &'a Robj) -> std::result::Result<Self, &'static str> {
+        if let Some(x) = robj.as_symbol() {
+            Ok(x)
+        } else {
+            Err("Expected a symbol.")
+        }
+    }
+}
+
+// Matrix input parameters.
+impl<'a, T> FromRobj<'a> for RArray<&'a [T], [usize; 2]>
+where
+    Robj: AsTypedSlice<'a, T>,
+{
+    fn from_robj(robj: &'a Robj) -> std::result::Result<Self, &'static str> {
+        if let Some(x) = robj.as_matrix() {
+            Ok(x)
+        } else if !robj.as_typed_slice().is_some() {
+            Err("Matrix type mismatch.")
+        } else {
+            Err("Expected a matrix.")
+        }
+    }
+}
+
+// Matrix input parameters.
+impl<'a, T> FromRobj<'a> for RMatrix3D<&'a [T]>
+where
+    Robj: AsTypedSlice<'a, T>,
+{
+    fn from_robj(robj: &'a Robj) -> std::result::Result<Self, &'static str> {
+        if let Some(x) = robj.as_matrix3d() {
+            Ok(x)
+        } else {
+            Err("Expected a 3d matrix.")
+        }
+    }
+}
+
diff --git a/extendr-api/src/robj/into_robj.rs b/extendr-api/src/robj/into_robj.rs
index 7c864d65d..910858bce 100644
--- a/extendr-api/src/robj/into_robj.rs
+++ b/extendr-api/src/robj/into_robj.rs
@@ -1,8 +1,15 @@
 use super::*;
 use crate::single_threaded;
+use std::collections::HashMap;
 
 fn str_to_character(s: &str) -> SEXP {
-    unsafe { Rf_mkCharLen(s.as_ptr() as *const raw::c_char, s.len() as i32) }
+    unsafe {
+        if s.is_na() {
+            R_NaString
+        } else {
+            Rf_mkCharLen(s.as_ptr() as *const raw::c_char, s.len() as i32)
+        }
+    }
 }
 
 /// Convert a null to an Robj.
@@ -303,49 +310,51 @@ impl ToVectorValue for Option<bool> {
 }
 
 // Not thread safe.
-unsafe fn fixed_size_collect<I>(iter: I, len: usize) -> Robj
+fn fixed_size_collect<I>(iter: I, len: usize) -> Robj
 where
     I: Iterator,
     I: Sized,
     I::Item: ToVectorValue,
 {
-    // Length of the vector is known in advance.
-    let sexptype = I::Item::sexptype();
-    if sexptype != 0 {
-        let sexp = Rf_allocVector(sexptype, len as R_xlen_t);
-        R_PreserveObject(sexp);
-        match sexptype {
-            REALSXP => {
-                let ptr = REAL(sexp);
-                for (i, v) in iter.enumerate() {
-                    *ptr.offset(i as isize) = v.to_real();
+    single_threaded(|| unsafe { 
+        // Length of the vector is known in advance.
+        let sexptype = I::Item::sexptype();
+        if sexptype != 0 {
+            let sexp = Rf_allocVector(sexptype, len as R_xlen_t);
+            R_PreserveObject(sexp);
+            match sexptype {
+                REALSXP => {
+                    let ptr = REAL(sexp);
+                    for (i, v) in iter.enumerate() {
+                        *ptr.offset(i as isize) = v.to_real();
+                    }
                 }
-            }
-            INTSXP => {
-                let ptr = INTEGER(sexp);
-                for (i, v) in iter.enumerate() {
-                    *ptr.offset(i as isize) = v.to_integer();
+                INTSXP => {
+                    let ptr = INTEGER(sexp);
+                    for (i, v) in iter.enumerate() {
+                        *ptr.offset(i as isize) = v.to_integer();
+                    }
                 }
-            }
-            LGLSXP => {
-                let ptr = LOGICAL(sexp);
-                for (i, v) in iter.enumerate() {
-                    *ptr.offset(i as isize) = v.to_logical();
+                LGLSXP => {
+                    let ptr = LOGICAL(sexp);
+                    for (i, v) in iter.enumerate() {
+                        *ptr.offset(i as isize) = v.to_logical();
+                    }
                 }
-            }
-            STRSXP => {
-                for (i, v) in iter.enumerate() {
-                    SET_STRING_ELT(sexp, i as isize, v.to_sexp());
+                STRSXP => {
+                    for (i, v) in iter.enumerate() {
+                        SET_STRING_ELT(sexp, i as isize, v.to_sexp());
+                    }
+                }
+                _ => {
+                    panic!("unexpected SEXPTYPE in collect_robj");
                 }
             }
-            _ => {
-                panic!("unexpected SEXPTYPE in collect_robj");
-            }
+            return Robj::Owned(sexp);
+        } else {
+            return Robj::from(());
         }
-        return Robj::Owned(sexp);
-    } else {
-        return Robj::from(());
-    }
+    })
 }
 
 /// Extensions to iterators for R objects including [RobjItertools::collect_robj()].
@@ -380,7 +389,7 @@ pub trait RobjItertools: Iterator {
     {
         if let (len, Some(max)) = self.size_hint().clone() {
             if len == max {
-                return single_threaded(|| unsafe { fixed_size_collect(self, len) });
+                return fixed_size_collect(self, len);
             }
         }
         // If the size is indeterminate, create a vector and call recursively.
@@ -487,3 +496,41 @@ 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 {
+    /// Convert a real iterator into a vector.
+    fn from(val: RealIter) -> Self {
+        val.collect_robj()
+    }
+}
+
+impl<'a> From<IntegerIter<'a>> for Robj {
+    /// Convert an integer iterator into a vector.
+    fn from(val: IntegerIter) -> Self {
+        val.collect_robj()
+    }
+}
+
+impl<'a> From<LogicalIter<'a>> for Robj {
+    /// Convert a logical iterator into a vector.
+    fn from(val: LogicalIter) -> Self {
+        val.collect_robj()
+    }
+}
+
+impl<'a> From<HashMap<&'a str, Robj>> for Robj {
+    /// Convert a hashmap into a list.
+    fn from(val: HashMap<&'a str, Robj>) -> Self {
+        let res: Robj = List(
+            val
+            .iter()
+            .map(|(_, v)| v)
+        ).into();
+        let names = val
+            .into_iter()
+            .map(|(k, _)| k)
+            .collect_robj();
+        res.set_attrib(names_symbol(), names);
+        res
+    }
+}
diff --git a/extendr-api/src/robj/iter.rs b/extendr-api/src/robj/iter.rs
index bd95f0a5f..46b575974 100644
--- a/extendr-api/src/robj/iter.rs
+++ b/extendr-api/src/robj/iter.rs
@@ -215,15 +215,17 @@ impl StrIter {
 fn str_from_strsxp<'a>(sexp: SEXP, index: isize) -> &'a str {
     unsafe {
         if index < 0 || index >= Rf_xlength(sexp) {
-            ""
+            na_str()
         } else {
             let charsxp = STRING_ELT(sexp, index);
-            if TYPEOF(charsxp) == CHARSXP as i32 {
+            if charsxp == R_NaString {
+                na_str()
+            } else if TYPEOF(charsxp) == CHARSXP as i32 {
                 let ptr = R_CHAR(charsxp) as *const u8;
                 let slice = std::slice::from_raw_parts(ptr, Rf_xlength(charsxp) as usize);
                 std::str::from_utf8_unchecked(slice)
             } else {
-                "error: not a char"
+                na_str()
             }
         }
     }
@@ -353,6 +355,15 @@ impl Robj {
     /// assert_eq!(factor.as_integer_vector().unwrap(), vec![1, 2, 3, 3]);
     /// assert_eq!(factor.as_str_iter().unwrap().collect::<Vec<_>>(), vec!["abcd", "def", "fg", "fg"]);
     /// assert_eq!(factor.as_str_iter().unwrap().collect::<Vec<_>>(), vec!["abcd", "def", "fg", "fg"]);
+    ///
+    /// let obj = Robj::from(vec![Some("a"), Some("b"), None]);
+    /// assert_eq!(obj.as_str_iter().unwrap().map(|s| s.is_na()).collect::<Vec<_>>(), vec![false, false, true]);
+    ///
+    /// let obj = Robj::from(vec!["a", "b", na_str()]);
+    /// assert_eq!(obj.as_str_iter().unwrap().map(|s| s.is_na()).collect::<Vec<_>>(), vec![false, false, true]);
+    ///
+    /// let obj = Robj::from(vec!["a", "b", "NA"]);
+    /// assert_eq!(obj.as_str_iter().unwrap().map(|s| s.is_na()).collect::<Vec<_>>(), vec![false, false, false]);
     /// ```
     pub fn as_str_iter(&self) -> Option<StrIter> {
         let i = 0;
diff --git a/extendr-api/src/robj/mod.rs b/extendr-api/src/robj/mod.rs
index 0669c5235..c213db618 100644
--- a/extendr-api/src/robj/mod.rs
+++ b/extendr-api/src/robj/mod.rs
@@ -658,7 +658,7 @@ impl Robj {
             let mut error: raw::c_int = 0;
             let res = R_tryEval(self.get(), R_GlobalEnv, &mut error as *mut raw::c_int);
             if error != 0 {
-                Err(Error::from("R eval error"))
+                Err(Error::EvalError{ code: r!(self), error })
             } else {
                 Ok(new_owned(res))
             }
@@ -693,11 +693,11 @@ impl Robj {
             use libR_sys::*;
             let mut status = 0_u32;
             let status_ptr = &mut status as *mut u32;
-            let code: Robj = code.into();
-            let parsed = new_owned(R_ParseVector(code.get(), -1, status_ptr, R_NilValue));
+            let codeobj: Robj = code.into();
+            let parsed = new_owned(R_ParseVector(codeobj.get(), -1, status_ptr, R_NilValue));
             match status {
                 1 => Ok(parsed),
-                _ => Err(Error::ParseError),
+                _ => Err(Error::ParseError{code: code.into(), status }),
             }
         })
     }
@@ -715,7 +715,7 @@ impl Robj {
             let mut res = Robj::from(());
             if let Some(iter) = expr.as_list_iter() {
                 for lang in iter {
-                    res = lang.eval()?;
+                    res = lang.eval()?
                 }
             }
             Ok(res)
@@ -1196,8 +1196,12 @@ impl std::fmt::Debug for Robj {
             STRSXP => {
                 write!(f, "r!([")?;
                 let mut sep = "";
-                for obj in self.as_str_iter().unwrap() {
-                    write!(f, "{}{:?}", sep, obj)?;
+                for s in self.as_str_iter().unwrap() {
+                    // if s.is_na() {
+                    //     write!(f, "{}na_str()", sep)?;
+                    // } else {
+                        write!(f, "{}{:?}", sep, s)?;
+                    // }
                     sep = ", ";
                 }
                 write!(f, "])")
diff --git a/extendr-api/src/robj/rinternals.rs b/extendr-api/src/robj/rinternals.rs
index d5dd4ccf2..d51f0a632 100644
--- a/extendr-api/src/robj/rinternals.rs
+++ b/extendr-api/src/robj/rinternals.rs
@@ -112,13 +112,14 @@ impl Robj {
     ///
     ///    let my_fun = base_env().find_function(sym!(ls)).unwrap();
     ///    assert_eq!(my_fun.is_function(), true);
-    ///    assert_eq!(base_env().find_var(sym!(ls)).unwrap().is_promise(), true);
+    ///    assert!(base_env().find_function(sym!(qwertyuiop)).is_none());
     /// ```
     pub fn find_function<K: Into<Robj>>(&self, key: K) -> Option<Robj> {
         let key = key.into();
         if !self.is_environment() || !key.is_symbol() {
             return None;
         }
+        // This may be better:
         // let mut env: Robj = self.into();
         // loop {
         //     if let Some(var) = env.local(&key) {
@@ -135,8 +136,7 @@ impl Robj {
         //     }
         // }
         unsafe {
-            let var = Rf_findFun(key.get(), self.get());
-            if var != R_UnboundValue {
+            if let Ok(var) = catch_r_error(|| Rf_findFun(key.get(), self.get())) {
                 Some(new_borrowed(var))
             } else {
                 None
diff --git a/extendr-api/src/thread_safety.rs b/extendr-api/src/thread_safety.rs
index 64780f2ff..ecfce2758 100644
--- a/extendr-api/src/thread_safety.rs
+++ b/extendr-api/src/thread_safety.rs
@@ -1,23 +1,28 @@
 //! Provide limited protection for multithreaded access to the R API.
 
+use crate::*;
+
 use std::cell::RefCell;
-use std::sync::atomic::{AtomicUsize, Ordering};
+use std::sync::atomic::{AtomicU32, Ordering};
 
-static OWNER_THREAD: AtomicUsize = AtomicUsize::new(0);
-static NEXT_THREAD_ID: AtomicUsize = AtomicUsize::new(1);
+static OWNER_THREAD: AtomicU32 = AtomicU32::new(0);
+static NEXT_THREAD_ID: AtomicU32 = AtomicU32::new(1);
 
 thread_local! {
-    static THREAD_ID: RefCell<usize> = RefCell::new(0);
+    static THREAD_ID: RefCell<u32> = RefCell::new(0);
 }
 
 // Get an integer 1.. for each thread that calls this.
-fn this_thread_id() -> usize {
+fn this_thread_id() -> u32 {
     THREAD_ID.with(|f| {
-        if *f.borrow() == 0 {
+        let mut id = *f.borrow();
+        if id == 0 {
             // Initialise with next value.
-            *f.borrow_mut() = NEXT_THREAD_ID.fetch_add(1, Ordering::SeqCst);
+            let mut ptr = f.borrow_mut();
+            id = NEXT_THREAD_ID.fetch_add(1, Ordering::SeqCst);
+            *ptr = id;
         }
-        *f.borrow()
+        id
     })
 }
 
@@ -26,8 +31,8 @@ fn this_thread_id() -> usize {
 ///
 /// ```
 /// use extendr_api::*;
-/// use std::sync::atomic::{AtomicUsize, Ordering};
-/// static GLOBAL_THREAD_COUNT: AtomicUsize = AtomicUsize::new(0);
+/// use std::sync::atomic::{AtomicU32, Ordering};
+/// static GLOBAL_THREAD_COUNT: AtomicU32 = AtomicU32::new(0);
 ///
 /// let threads : Vec<_> = (0..100).map(|_| {
 ///    std::thread::spawn(move|| {
@@ -81,6 +86,8 @@ where
 
 /// This function is used by the wrapper logic to catch
 /// panics on return.
+///
+#[doc(hidden)]
 pub fn handle_panic<F, R>(err_str: &str, f: F) -> R
 where
     F: FnOnce() -> R,
@@ -96,3 +103,62 @@ where
         }
     }
 }
+
+pub fn throw_r_error<S: AsRef<str>>(s: S) {
+    let s = s.as_ref();
+    unsafe { libR_sys::Rf_error(std::ffi::CString::new(s).unwrap().as_ptr()) };
+}
+
+/// Wrap an R function such as Rf_findFunction and convert errors and panics into results.
+/// ```
+/// use extendr_api::*;
+/// test! {
+///    let res = catch_r_error(|| unsafe {
+///        throw_r_error("bad things!");
+///        std::ptr::null_mut()
+///    });
+///    assert_eq!(res.is_ok(), false);
+/// }
+/// ```
+pub fn catch_r_error<F>(f: F) -> Result<SEXP>
+where
+    F: FnOnce() -> SEXP + Copy,
+    F: std::panic::UnwindSafe,
+{
+    use std::os::raw;
+
+    unsafe extern "C" fn do_call<F>(data: *mut raw::c_void) -> SEXP
+    where
+        F: FnOnce() -> SEXP + Copy,
+    {
+        let data = data as *const ();
+        let f: &F = std::mem::transmute(data);
+        f()
+    }
+
+    unsafe extern "C" fn do_cleanup(_: *mut raw::c_void, jump: Rboolean) {
+        if jump != 0 {
+            panic!("R has thrown an error.");
+        }
+    }
+
+    unsafe {
+        let fun_ptr = do_call::<F> as *const ();
+        let clean_ptr = do_cleanup as *const ();
+        let x = false;
+        let fun = std::mem::transmute(fun_ptr);
+        let cleanfun = std::mem::transmute(clean_ptr);
+        let data = std::mem::transmute(&f);
+        let cleandata = std::mem::transmute(&x);
+        let cont = R_MakeUnwindCont();
+        Rf_protect(cont);
+        let res = match std::panic::catch_unwind(|| {
+            R_UnwindProtect(fun, data, cleanfun, cleandata, cont)
+        }) {
+            Ok(res) => Ok(res),
+            Err(_) => Err("Error in protected R code".into()),
+        };
+        Rf_unprotect(1);
+        res
+    }
+}
diff --git a/extendr-api/src/wrapper.rs b/extendr-api/src/wrapper.rs
index 9ca1d57f9..fd2aca94e 100644
--- a/extendr-api/src/wrapper.rs
+++ b/extendr-api/src/wrapper.rs
@@ -510,9 +510,9 @@ impl Robj {
     /// let expr = r!(env.clone());
     /// assert_eq!(expr.len(), 100);
     /// let env2 = expr.as_environment().unwrap();
-    /// assert_eq!(env2, env);
+    /// assert_eq!(env2.names_and_values.len(), 100);
     /// ```
-    pub fn as_environment(&self) -> Option<Env<Robj, HashMap<String, Robj>>> {
+    pub fn as_environment(&self) -> Option<Env<Robj, HashMap<&str, Robj>>> {
         if self.sexptype() == ENVSXP {
             unsafe {
                 let parent = new_owned(ENCLOS(self.get()));
@@ -526,7 +526,7 @@ impl Robj {
                         {
                             for (obj, tag) in obj_iter.zip(tag_iter) {
                                 if !obj.is_unbound_value() && tag.is_some() {
-                                    names_and_values.insert(tag.unwrap().to_string(), obj);
+                                    names_and_values.insert(tag.unwrap(), obj);
                                 }
                             }
                         }
@@ -536,7 +536,7 @@ impl Robj {
                 {
                     for (obj, tag) in obj_iter.zip(tag_iter) {
                         if !obj.is_unbound_value() && tag.is_some() {
-                            names_and_values.insert(tag.unwrap().to_string(), obj);
+                            names_and_values.insert(tag.unwrap(), obj);
                         }
                     }
                 }


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index a42c5fe34..718778899 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -120,8 +120,15 @@ jobs:
       - name: Build
         run: |
           foreach($target in ($env:BUILD_TARGETS).Split(',')) {
-            echo $target
+            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::"
           }
 
       # For each target in the BUILD_TARGETS comma-separated list, run cargo test with appropriate target
@@ -129,8 +136,15 @@ jobs:
       - name: Run tests
         run: |
           foreach($target in ($env:BUILD_TARGETS).Split(',')) {
-            echo $target
+            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::"
           }
 
 
@@ -244,53 +258,71 @@ jobs:
       - name: Build
         run: |
           foreach($target in ($env:BUILD_TARGETS).Split(',')) {
-            echo $target
+            echo "::group::Build extendr-api for $target target"
+
             cargo build --manifest-path extendr-api/Cargo.toml --features tests-all $(if($target -ne 'default') {"--target=$target"} )
             if (!$?) { 
-              echo "::error::$target" ;
+              echo "::error::Build extendr-api for $target target" ;
               throw "Last exit code $LASTEXITCODE"
             }
+
+            echo "::endgroup::"
           }
           foreach($target in ($env:BUILD_TARGETS).Split(',')) {
-            echo $target
+            echo "::group::Build extendr-engine for $target target"
+
             cargo build --manifest-path extendr-engine/Cargo.toml --features tests-all $(if($target -ne 'default') {"--target=$target"} ) 
             if (!$?) { 
-              echo "::error::$target" ;
+              echo "::error::Build extendr-engine for $target target" ;
               throw "Last exit code $LASTEXITCODE"
             }
+
+            echo "::endgroup::"
           }
 
       - name: Run tests
         run: |
           foreach($target in ($env:BUILD_TARGETS).Split(',')) {
-            echo $target
+            echo "::group::Test extendr-engine \w tests-all for $target target"
+
             cargo test --manifest-path extendr-engine/Cargo.toml --features tests-all $(if($target -ne 'default') {"--target=$target"} ) -- --nocapture 
             if (!$?) { 
-              echo "::error::$target" ;
+              echo "::error::Test extendr-engine \w tests-all for $target target" ;
               throw "Last exit code $LASTEXITCODE"
             }
+
+            echo "::endgroup::"
           }
           foreach($target in ($env:BUILD_TARGETS).Split(',')) {
-            echo $target
+            echo "::group::Test extendr-api \w tests-all for $target target"
+
             cargo test --manifest-path extendr-api/Cargo.toml --features tests-all $(if($target -ne 'default') {"--target=$target"} ) -- --nocapture 
             if (!$?) { 
-              echo "::error::$target" ;
+              echo "::error::Test extendr-api \w tests-all for $target target" ;
               throw "Last exit code $LASTEXITCODE"
             }
+
+            echo "::endgroup::"
           }
-          foreach($target in ($env:BUILD_TARGETS).Split(',')) {
-            echo $target
+          foreach($target in ($env:BUILD_TARGETS).Split(',')) {            
+            echo "::group::Test extendr-api \w tests-minimal for $target target"
+
             cargo test --manifest-path extendr-api/Cargo.toml --features tests-minimal $(if($target -ne 'default') {"--target=$target"} ) -- --nocapture
             if (!$?) { 
-              echo "::error::$target" ;
+              echo "::error::Test extendr-api \w tests-minimal for $target target" ;
               throw "Last exit code $LASTEXITCODE"
             }
+
+            echo "::endgroup::"
           }
           foreach($target in ($env:BUILD_TARGETS).Split(',')) {
-            echo $target
+            echo "::group::Test extendr-macros for $target target"
+
             cargo test --manifest-path extendr-macros/Cargo.toml  $(if($target -ne 'default') {"--target=$target"} ) -- --nocapture
             if (!$?) { 
-              echo "::error::$target" ;
+              echo "::error::Test extendr-macros for $target target" ;
               throw "Last exit code $LASTEXITCODE"
             }
+            
+            echo "::endgroup::"
           }