Repository: extendr/extendr
Pull request #350 (2021-12-28 18:46:21)

Complexes
This PR implements the Complexes type and provides complex number support via num_complex.

`Rcplx` wraps `num_complex::Complex<f64>` the same way that `Rfloat` wraps f64. 

```
fn mycplx(x: Rcplx) -> Rcplx {
  -x
}

fn mycplxvec(x: Complexes) -> Complexes {
  x
}

fn mycplxvec2() -> Complexes {
  Complexes::from_values([(0., 0.), (1., -1.)])
}
```

This PR provides only basic support.

andy-thomason: Are you good with this @clauswilke @yutannihilation @Ilia-Kosenkov ?
clauswilke: I gave this a quick look and it generally seems fine to me. One issue that caught my eye is `Rcomplex` vs. `Rcplx`. It's not immediately clear to me why there are two types, which one an enduser would be supposed to use, and whether the difference is documented. I only found some documentation for `Rcplx`.

If there is a good reason for having these two types, then I think this should be documented somewhere. Also, users should be told which one to use when.
andy-thomason: > One issue that caught my eye is `Rcomplex` vs. `Rcplx`.

Currently, we either wrap num_complex or a simple struct with the same layout in Rcplx.

num_complex gives us complex arithmetic, functions, nan tests and more but
is not the same as Rcomplex even though it has the same layout.

We could have implemented arithmetic for Rcomplex, but it would have been a lot
more work and would involve a lot of `transmute` calls.
clauswilke: > Currently, we either wrap num_complex or a simple struct with the same layout in Rcplx.

That's fine, and I have no concerns with the technical aspects, but I think this should be stated clearly in the docs somewhere.

If I understand correctly, endusers would never normally use `Rcplx`, only `Rcomplex`. So I would suggest adding these thoughts to the documentation:

- In the documentation for `Rcplx`, mention that endusers would typically use `Rcomplex`.
- In the documentation for `Rcomplex`, mention that it wraps two different types, depending on use case. Also mention how this can be configured via features.

Patch:
diff --git a/extendr-api/Cargo.toml b/extendr-api/Cargo.toml
index 08c61067b..79a3d89b8 100644
--- a/extendr-api/Cargo.toml
+++ b/extendr-api/Cargo.toml
@@ -22,12 +22,13 @@ ndarray = { version = "0.15.3", optional = true }
 lazy_static = "1.4"
 paste = "1.0.5"
 serde = { version = "1.0", features = ["derive"], optional = true }
+num-complex = { version = "0.4", optional = true }
 
 [features]
-default = ["serde"]
+default = []
 
 # All features to test
-tests-all = ["ndarray", "libR-sys/use-bindgen", "serde"]
+tests-all = ["ndarray", "libR-sys/use-bindgen", "serde", "num-complex"]
 
 # The minimal set of features without all optional ones
 tests-minimal = ["libR-sys/use-bindgen"]
diff --git a/extendr-api/src/lib.rs b/extendr-api/src/lib.rs
index 7f9d92650..f798e6bd3 100644
--- a/extendr-api/src/lib.rs
+++ b/extendr-api/src/lib.rs
@@ -278,8 +278,6 @@ use scalar::Rbool;
 
 //////////////////////////////////////////////////
 
-pub struct Cplx(pub f64, pub f64);
-
 /// TRUE value eg. `r!(TRUE)`
 pub const TRUE: Rbool = Rbool::true_value();
 
@@ -424,7 +422,7 @@ pub enum Rany<'a> {
     Logical(&'a Logicals),        // LGLSXP
     Integer(&'a Integers),        // INTSXP
     Real(&'a Doubles),            // REALSXP
-    Complex(&'a Robj),            // CPLXSXP
+    Complex(&'a Complexes),       // CPLXSXP
     String(&'a Robj),             // STRSXP
     Dot(&'a Robj),                // DOTSXP
     Any(&'a Robj),                // ANYSXP
diff --git a/extendr-api/src/prelude.rs b/extendr-api/src/prelude.rs
index 1b7c24a05..184f8d86a 100644
--- a/extendr-api/src/prelude.rs
+++ b/extendr-api/src/prelude.rs
@@ -36,8 +36,6 @@ pub use crate::{
     data_frame, factor, global, list, r, reprint, reprintln, rprint, rprintln, sym, test, var,
 };
 
-pub use super::Cplx;
-
 pub use super::wrapper::{
     AltComplexImpl, AltIntegerImpl, AltLogicalImpl, AltRawImpl, AltRealImpl, AltStringImpl, Altrep,
     AltrepImpl, RArray, RColumn, RMatrix, RMatrix3D,
@@ -57,8 +55,9 @@ pub use super::thread_safety::{
 };
 
 pub use super::wrapper::{
-    Doubles, EnvIter, Environment, Expression, ExternalPtr, FromList, Function, Integers, Language,
-    List, ListIter, Logicals, Nullable, Pairlist, Primitive, Promise, Raw, Rstr, Strings, Symbol,
+    Complexes, Doubles, EnvIter, Environment, Expression, ExternalPtr, FromList, Function,
+    Integers, Language, List, ListIter, Logicals, Nullable, Pairlist, Primitive, Promise, Raw,
+    Rstr, Strings, Symbol,
 };
 
 #[cfg(feature = "ndarray")]
diff --git a/extendr-api/src/robj/into_robj.rs b/extendr-api/src/robj/into_robj.rs
index be2485aed..9ede082c9 100644
--- a/extendr-api/src/robj/into_robj.rs
+++ b/extendr-api/src/robj/into_robj.rs
@@ -80,6 +80,13 @@ pub trait ToVectorValue {
         0.
     }
 
+    fn to_complex(&self) -> Rcomplex
+    where
+        Self: Sized,
+    {
+        Rcomplex { r: 0., i: 0. }
+    }
+
     fn to_integer(&self) -> i32
     where
         Self: Sized,
@@ -157,6 +164,34 @@ impl_real_tvv!(u32);
 impl_real_tvv!(u64);
 impl_real_tvv!(usize);
 
+macro_rules! impl_complex_tvv {
+    ($t: ty) => {
+        impl ToVectorValue for $t {
+            fn sexptype() -> SEXPTYPE {
+                CPLXSXP
+            }
+
+            fn to_complex(&self) -> Rcomplex {
+                unsafe { std::mem::transmute(*self) }
+            }
+        }
+
+        impl ToVectorValue for &$t {
+            fn sexptype() -> SEXPTYPE {
+                CPLXSXP
+            }
+
+            fn to_complex(&self) -> Rcomplex {
+                unsafe { std::mem::transmute(**self) }
+            }
+        }
+    };
+}
+
+impl_complex_tvv!(c64);
+impl_complex_tvv!(Rcplx);
+impl_complex_tvv!((f64, f64));
+
 macro_rules! impl_integer_tvv {
     ($t: ty) => {
         impl ToVectorValue for $t {
@@ -356,6 +391,12 @@ where
                         *ptr.add(i) = v.to_real();
                     }
                 }
+                CPLXSXP => {
+                    let ptr = COMPLEX(sexp);
+                    for (i, v) in iter.enumerate() {
+                        *ptr.add(i) = v.to_complex();
+                    }
+                }
                 INTSXP => {
                     let ptr = INTEGER(sexp);
                     for (i, v) in iter.enumerate() {
diff --git a/extendr-api/src/robj/mod.rs b/extendr-api/src/robj/mod.rs
index 69490d126..cac300054 100644
--- a/extendr-api/src/robj/mod.rs
+++ b/extendr-api/src/robj/mod.rs
@@ -10,6 +10,7 @@
 //!
 
 use libR_sys::*;
+use prelude::{c64, Rcplx};
 use std::os::raw;
 
 use crate::*;
@@ -336,6 +337,7 @@ impl Robj {
                     INTSXP => *(INTEGER(sexp)) == libR_sys::R_NaInt,
                     LGLSXP => *(LOGICAL(sexp)) == libR_sys::R_NaInt,
                     REALSXP => R_IsNA(*(REAL(sexp))) != 0,
+                    CPLXSXP => R_IsNA((*COMPLEX(sexp)).r) != 0,
                     _ => false,
                 }
             }
@@ -775,6 +777,9 @@ make_typed_slice!(Rfloat, REAL, REALSXP);
 make_typed_slice!(u8, RAW, RAWSXP);
 make_typed_slice!(Robj, VECTOR_PTR, VECSXP);
 make_typed_slice!(Rstr, STRING_PTR, STRSXP);
+make_typed_slice!(c64, COMPLEX, CPLXSXP);
+make_typed_slice!(Rcplx, COMPLEX, CPLXSXP);
+make_typed_slice!(Rcomplex, COMPLEX, CPLXSXP);
 
 /// These are helper functions which give access to common properties of R objects.
 #[allow(non_snake_case)]
diff --git a/extendr-api/src/scalar/mod.rs b/extendr-api/src/scalar/mod.rs
index 451b1dbc5..2d46f0cb2 100644
--- a/extendr-api/src/scalar/mod.rs
+++ b/extendr-api/src/scalar/mod.rs
@@ -5,3 +5,15 @@ mod rint;
 pub use rbool::Rbool;
 pub use rfloat::Rfloat;
 pub use rint::Rint;
+
+#[cfg(feature = "num-complex")]
+mod rcplx_full;
+
+#[cfg(feature = "num-complex")]
+pub use rcplx_full::{c64, Rcplx};
+
+#[cfg(not(feature = "num-complex"))]
+mod rcplx_default;
+
+#[cfg(not(feature = "num-complex"))]
+pub use rcplx_default::{c64, Rcplx};
diff --git a/extendr-api/src/scalar/rcplx_default.rs b/extendr-api/src/scalar/rcplx_default.rs
new file mode 100644
index 000000000..05e094806
--- /dev/null
+++ b/extendr-api/src/scalar/rcplx_default.rs
@@ -0,0 +1,156 @@
+use crate::scalar::Rfloat;
+use crate::*;
+use std::convert::TryFrom;
+
+#[allow(non_camel_case_types)]
+#[derive(Clone, Copy, Debug, Default, PartialEq)]
+pub struct c64 {
+    re: f64,
+    im: f64,
+}
+
+impl c64 {
+    pub fn new(re: f64, im: f64) -> Self {
+        Self { re, im }
+    }
+}
+
+impl From<f64> for c64 {
+    fn from(val: f64) -> Self {
+        c64::new(val, 0.0)
+    }
+}
+
+impl std::fmt::Display for c64 {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "({}, {})", self.re, self.im)
+    }
+}
+
+impl CanBeNA for c64 {
+    fn is_na(&self) -> bool {
+        unsafe { R_IsNA(self.re) != 0 }
+    }
+
+    fn na() -> c64 {
+        unsafe { c64::new(R_NaReal, R_NaReal) }
+    }
+}
+
+/// Rcplx is a wrapper for f64 in the context of an R's complex vector.
+///
+/// Rcplx has a special NA value, obtained from R headers via R_NaReal.
+///
+/// Rcplx has the same footprint as R's complex value allowing us to use it in zero copy slices.
+#[derive(Clone, Copy, Debug, Default, PartialEq)]
+pub struct Rcplx(c64);
+
+impl Rcplx {
+    // gen_impl!(Rcplx, c64);
+
+    pub fn re(&self) -> Rfloat {
+        Rfloat::from(self.0.re)
+    }
+
+    pub fn im(&self) -> Rfloat {
+        Rfloat::from(self.0.im)
+    }
+}
+
+impl CanBeNA for Rcplx {
+    fn is_na(&self) -> bool {
+        unsafe { R_IsNA(self.0.re) != 0 }
+    }
+
+    fn na() -> Rcplx {
+        unsafe { Rcplx::from(c64::new(R_NaReal, R_NaReal)) }
+    }
+}
+
+impl From<c64> for Rcplx {
+    fn from(val: c64) -> Self {
+        Rcplx(val)
+    }
+}
+
+impl From<f64> for Rcplx {
+    fn from(val: f64) -> Self {
+        Rcplx(c64::from(val))
+    }
+}
+
+impl From<(f64, f64)> for Rcplx {
+    fn from(val: (f64, f64)) -> Self {
+        Rcplx(c64::new(val.0, val.1))
+    }
+}
+
+impl From<(Rfloat, Rfloat)> for Rcplx {
+    fn from(val: (Rfloat, Rfloat)) -> Self {
+        Rcplx(c64::new(val.0.inner(), val.1.inner()))
+    }
+}
+
+impl From<Rfloat> for Rcplx {
+    fn from(val: Rfloat) -> Self {
+        Rcplx(c64::from(val.inner()))
+    }
+}
+
+impl From<Rcomplex> for Rcplx {
+    fn from(val: Rcomplex) -> Self {
+        Rcplx(c64::new(val.r, val.i))
+    }
+}
+
+impl From<Rcplx> for Option<c64> {
+    fn from(val: Rcplx) -> Self {
+        if val.is_na() {
+            None
+        } else {
+            Some(c64::new(val.re().inner(), val.im().inner()))
+        }
+    }
+}
+
+impl TryFrom<Robj> for Rcplx {
+    type Error = Error;
+
+    fn try_from(robj: Robj) -> Result<Self> {
+        // Check if the value is a scalar
+        match robj.len() {
+            0 => return Err(Error::ExpectedNonZeroLength(robj)),
+            1 => {}
+            _ => return Err(Error::ExpectedScalar(robj)),
+        };
+
+        // Check if the value is not a missing value.
+        if robj.is_na() {
+            return Ok(Rcplx::na());
+        }
+
+        // This should always work, NA is handled above.
+        if let Some(v) = robj.as_real() {
+            return Ok(Rcplx::from(v));
+        }
+
+        // Any integer (32 bit) can be represented as f64,
+        // this always works.
+        if let Some(v) = robj.as_integer() {
+            return Ok(Rcplx::from(v as f64));
+        }
+
+        // Complex slices return their first element.
+        if let Some(s) = robj.as_typed_slice() {
+            return Ok(s[0]);
+        }
+
+        Err(Error::ExpectedComplex(robj))
+    }
+}
+
+impl PartialEq<f64> for Rcplx {
+    fn eq(&self, other: &f64) -> bool {
+        self.re().inner() == *other && self.im() == 0.0
+    }
+}
diff --git a/extendr-api/src/scalar/rcplx_full.rs b/extendr-api/src/scalar/rcplx_full.rs
new file mode 100644
index 000000000..b54ae5bc7
--- /dev/null
+++ b/extendr-api/src/scalar/rcplx_full.rs
@@ -0,0 +1,196 @@
+use crate::scalar::macros::*;
+use crate::scalar::Rfloat;
+use crate::*;
+use std::convert::TryFrom;
+use std::ops::{Add, Div, Mul, Neg, Sub};
+use std::ops::{AddAssign, DivAssign, MulAssign, SubAssign};
+
+#[allow(non_camel_case_types)]
+pub type c64 = num_complex::Complex<f64>;
+
+impl CanBeNA for c64 {
+    fn is_na(&self) -> bool {
+        unsafe { R_IsNA(self.re) != 0 }
+    }
+
+    fn na() -> c64 {
+        unsafe { c64::new(R_NaReal, R_NaReal) }
+    }
+}
+
+/// Rcplx is a wrapper for f64 in the context of an R's complex vector.
+///
+/// Rcplx has a special NA value, obtained from R headers via R_NaReal.
+///
+/// Rcplx has the same footprint as R's complex value allowing us to use it in zero copy slices.
+pub struct Rcplx(c64);
+
+impl Rcplx {
+    gen_impl!(Rcplx, c64);
+
+    pub fn is_nan(&self) -> bool {
+        self.0.is_nan()
+    }
+
+    pub fn is_infinite(&self) -> bool {
+        self.0.is_infinite()
+    }
+
+    pub fn re(&self) -> Rfloat {
+        Rfloat::from(self.0.re)
+    }
+
+    pub fn im(&self) -> Rfloat {
+        Rfloat::from(self.0.im)
+    }
+}
+
+impl From<f64> for Rcplx {
+    fn from(val: f64) -> Self {
+        Rcplx(c64::from(val))
+    }
+}
+
+impl From<(f64, f64)> for Rcplx {
+    fn from(val: (f64, f64)) -> Self {
+        Rcplx(c64::new(val.0, val.1))
+    }
+}
+
+impl From<(Rfloat, Rfloat)> for Rcplx {
+    fn from(val: (Rfloat, Rfloat)) -> Self {
+        Rcplx(c64::new(val.0.inner(), val.1.inner()))
+    }
+}
+
+impl From<Rfloat> for Rcplx {
+    fn from(val: Rfloat) -> Self {
+        Rcplx(c64::from(val.inner()))
+    }
+}
+
+impl From<Rcomplex> for Rcplx {
+    fn from(val: Rcomplex) -> Self {
+        Rcplx(c64::new(val.r, val.i))
+    }
+}
+
+impl From<Rcplx> for Option<c64> {
+    fn from(val: Rcplx) -> Self {
+        if val.is_na() {
+            None
+        } else {
+            Some(c64::new(val.re().inner(), val.im().inner()))
+        }
+    }
+}
+
+// `NA_real_` is a `NaN` with specific bit representation.
+// Check that underlying `f64` is `NA_real_`.
+gen_trait_impl!(Rcplx, c64, |x: &Rcplx| x.inner().re.is_na(), c64::na());
+gen_from_primitive!(Rcplx, c64);
+// gen_from_scalar!(Rcplx, c64);
+gen_sum_iter!(Rcplx, 0f64);
+
+// Generate binary ops for +, -, * and /
+gen_binop!(
+    Rcplx,
+    c64,
+    Add,
+    |lhs: c64, rhs: c64| Some(lhs + rhs),
+    "Add two Rcplx values or an option of c64."
+);
+gen_binop!(
+    Rcplx,
+    c64,
+    Sub,
+    |lhs: c64, rhs: c64| Some(lhs - rhs),
+    "Subtract two Rcplx values or an option of c64."
+);
+gen_binop!(
+    Rcplx,
+    c64,
+    Mul,
+    |lhs: c64, rhs: c64| Some(lhs * rhs),
+    "Multiply two Rcplx values or an option of c64."
+);
+gen_binop!(
+    Rcplx,
+    c64,
+    Div,
+    |lhs: c64, rhs: c64| Some(lhs / rhs),
+    "Divide two Rcplx values or an option of c64."
+);
+gen_binopassign!(
+    Rcplx,
+    c64,
+    AddAssign,
+    |lhs: c64, rhs: c64| Some(lhs + rhs),
+    "Add two Rcplx values or an option of c64, modifying the left-hand side in place. Overflows to NA."
+);
+gen_binopassign!(
+    Rcplx,
+    c64,
+    SubAssign,
+    |lhs: c64, rhs: c64| Some(lhs - rhs),
+    "Subtract two Rcplx values or an option of c64, modifying the left-hand side in place. Overflows to NA."
+);
+gen_binopassign!(
+    Rcplx,
+    c64,
+    MulAssign,
+    |lhs: c64, rhs: c64| Some(lhs * rhs),
+    "Multiply two Rcplx values or an option of c64, modifying the left-hand side in place. Overflows to NA."
+);
+gen_binopassign!(
+    Rcplx,
+    c64,
+    DivAssign,
+    |lhs: c64, rhs: c64| Some(lhs / rhs),
+    "Divide two Rcplx values or an option of c64, modifying the left-hand side in place. Overflows to NA."
+);
+
+// Generate unary ops for -, !
+gen_unop!(Rcplx, Neg, |lhs: c64| Some(-lhs), "Negate a Rcplx value.");
+
+impl TryFrom<Robj> for Rcplx {
+    type Error = Error;
+
+    fn try_from(robj: Robj) -> Result<Self> {
+        // Check if the value is a scalar
+        match robj.len() {
+            0 => return Err(Error::ExpectedNonZeroLength(robj)),
+            1 => {}
+            _ => return Err(Error::ExpectedScalar(robj)),
+        };
+
+        // Check if the value is not a missing value.
+        if robj.is_na() {
+            return Ok(Rcplx::na());
+        }
+
+        // This should always work, NA is handled above.
+        if let Some(v) = robj.as_real() {
+            return Ok(Rcplx::from(v));
+        }
+
+        // Any integer (32 bit) can be represented as f64,
+        // this always works.
+        if let Some(v) = robj.as_integer() {
+            return Ok(Rcplx::from(v as f64));
+        }
+
+        // Complex slices return their first element.
+        if let Some(s) = robj.as_typed_slice() {
+            return Ok(s[0]);
+        }
+
+        Err(Error::ExpectedComplex(robj))
+    }
+}
+
+impl PartialEq<f64> for Rcplx {
+    fn eq(&self, other: &f64) -> bool {
+        self.re().inner() == *other && self.im() == 0.0
+    }
+}
diff --git a/extendr-api/src/wrapper/altrep.rs b/extendr-api/src/wrapper/altrep.rs
index a2074c096..ae6796a9a 100644
--- a/extendr-api/src/wrapper/altrep.rs
+++ b/extendr-api/src/wrapper/altrep.rs
@@ -1,4 +1,4 @@
-use prelude::{Rbool, Rfloat, Rint};
+use prelude::{Rbool, Rcplx, Rfloat, Rint};
 
 use super::*;
 
@@ -419,10 +419,10 @@ pub trait AltRawImpl: AltrepImpl {
 
 pub trait AltComplexImpl: AltrepImpl {
     /// Get a single element from this vector.
-    fn elt(&self, _index: usize) -> Cplx;
+    fn elt(&self, _index: usize) -> Rcplx;
 
     /// Get a multiple elements from this vector.
-    fn get_region(&self, index: usize, data: &mut [Cplx]) -> usize {
+    fn get_region(&self, index: usize, data: &mut [Rcplx]) -> usize {
         let len = self.length();
         if index > len {
             0
@@ -959,7 +959,7 @@ impl Altrep {
                 n: R_xlen_t,
                 buf: *mut Rcomplex,
             ) -> R_xlen_t {
-                let slice = std::slice::from_raw_parts_mut(buf as *mut Cplx, n as usize);
+                let slice = std::slice::from_raw_parts_mut(buf as *mut Rcplx, n as usize);
                 Altrep::get_state::<StateType>(x).get_region(i as usize, slice) as R_xlen_t
             }
 
@@ -1044,6 +1044,13 @@ impl Altrep {
         Rfloat,
         f64
     );
+    make_from_iterator!(
+        make_altcomplex_from_iterator,
+        make_altcomplex_class,
+        AltComplexImpl,
+        Rcplx,
+        c64
+    );
 }
 
 impl<Iter: ExactSizeIterator + std::fmt::Debug + Clone> AltrepImpl for Iter {
diff --git a/extendr-api/src/wrapper/complexes.rs b/extendr-api/src/wrapper/complexes.rs
new file mode 100644
index 000000000..9b677a530
--- /dev/null
+++ b/extendr-api/src/wrapper/complexes.rs
@@ -0,0 +1,70 @@
+use super::scalar::{c64, Rcplx};
+use super::*;
+use std::iter::FromIterator;
+
+/// An obscure `NA`-aware wrapper for R's complex vectors.
+/// Can be used to iterate over vectors obtained from R
+/// or to create new vectors that can be returned back to R.
+/// ```
+/// use extendr_api::prelude::*;
+/// test! {
+///     let mut vec = (0..5).map(|i| (i as f64).into()).collect::<Complexes>();
+///     assert_eq!(vec.len(), 5);
+/// }
+/// ```  
+#[derive(Debug, PartialEq, Clone)]
+pub struct Complexes {
+    pub(crate) robj: Robj,
+}
+
+crate::wrapper::macros::gen_vector_wrapper_impl!(
+    vector_type: Complexes,
+    scalar_type: Rcplx,
+    primitive_type: c64,
+    r_prefix: COMPLEX,
+    SEXP: CPLXSXP,
+    doc_name: complex,
+    altrep_constructor: make_altcomplex_from_iterator,
+);
+
+impl Complexes {
+    /// Get a region of elements from the vector.
+    pub fn get_region(&self, index: usize, dest: &mut [Rcplx]) -> usize {
+        unsafe {
+            let ptr: *mut Rcomplex = dest.as_mut_ptr() as *mut Rcomplex;
+            COMPLEX_GET_REGION(self.get(), index as R_xlen_t, dest.len() as R_xlen_t, ptr) as usize
+        }
+    }
+}
+
+// There is no SET_COMPLEX_ELT
+//
+// impl Complexes {
+//     pub fn set_elt(&mut self, index: usize, val: Rcplx) {
+//         unsafe {
+//             SET_COMPLEX_ELT(self.get(), index as R_xlen_t, val.inner());
+//         }
+//     }
+// }
+
+impl Deref for Complexes {
+    type Target = [Rcplx];
+
+    /// Treat Complexes as if it is a slice, like Vec<Rcplx>
+    fn deref(&self) -> &Self::Target {
+        unsafe {
+            let ptr = DATAPTR_RO(self.get()) as *const Rcplx;
+            std::slice::from_raw_parts(ptr, self.len())
+        }
+    }
+}
+
+impl DerefMut for Complexes {
+    /// Treat Complexes as if it is a mutable slice, like Vec<Rcplx>
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        unsafe {
+            let ptr = DATAPTR(self.get()) as *mut Rcplx;
+            std::slice::from_raw_parts_mut(ptr, self.len())
+        }
+    }
+}
diff --git a/extendr-api/src/wrapper/doubles.rs b/extendr-api/src/wrapper/doubles.rs
index e9440293b..c8117fce0 100644
--- a/extendr-api/src/wrapper/doubles.rs
+++ b/extendr-api/src/wrapper/doubles.rs
@@ -30,67 +30,23 @@ crate::wrapper::macros::gen_vector_wrapper_impl!(
     altrep_constructor: make_altreal_from_iterator,
 );
 
-#[cfg(test)]
-mod tests {
-    use crate::prelude::*;
-
-    #[test]
-    fn from_iterator() {
-        test! {
-            let vec : Doubles = (0..3).map(|i| (i as f64).into()).collect();
-            assert_eq!(vec, Doubles::from_values([0.0, 1.0, 2.0]));
-        }
-    }
-    #[test]
-    fn iter_mut() {
-        test! {
-            let mut vec = Doubles::from_values([0.0, 1.0, 2.0, 3.0]);
-            vec.iter_mut().for_each(|v| *v = *v + 1.0);
-            assert_eq!(vec, Doubles::from_values([1.0, 2.0, 3.0, 4.0]));
-        }
-    }
-
-    #[test]
-    fn iter() {
-        test! {
-            let vec = Doubles::from_values([0.0, 1.0, 2.0, 3.0]);
-            assert_eq!(vec.iter().sum::<Rfloat>(), 6.0);
+impl Doubles {
+    /// Get a region of elements from the vector.
+    pub fn get_region(&self, index: usize, dest: &mut [Rfloat]) -> usize {
+        unsafe {
+            let ptr: *mut f64 = dest.as_mut_ptr() as *mut f64;
+            REAL_GET_REGION(self.get(), index as R_xlen_t, dest.len() as R_xlen_t, ptr) as usize
         }
     }
 
-    #[test]
-    fn from_values_short() {
-        test! {
-            // Short (<64k) vectors are allocated.
-            let vec = Doubles::from_values((0..3).map(|i| 2.0 - i as f64));
-            assert_eq!(vec.is_altrep(), false);
-            assert_eq!(r!(vec.clone()), r!([2.0, 1.0, 0.0]));
-            assert_eq!(vec.elt(1), 1.0);
-            let mut dest = [0.0.into(); 2];
-            vec.get_region(1, &mut dest);
-            assert_eq!(dest, [1.0, 0.0]);
-        }
-    }
-    #[test]
-    fn from_values_long() {
-        test! {
-            // Long (>=64k) vectors are lazy ALTREP objects.
-            let vec = Doubles::from_values((0..1000000000).map(|x| x as f64));
-            assert_eq!(vec.is_altrep(), true);
-            assert_eq!(vec.elt(12345678), 12345678.0);
-            let mut dest = [0.0.into(); 2];
-            vec.get_region(12345678, &mut dest);
-            assert_eq!(dest, [12345678.0, 12345679.0]);
-        }
+    /// Return `TRUE` if the vector is sorted, `FALSE` if not, or `NA_BOOL` if unknown.
+    pub fn is_sorted(&self) -> Rbool {
+        unsafe { REAL_IS_SORTED(self.get()).into() }
     }
 
-    #[test]
-    fn new() {
-        test! {
-            let vec = Doubles::new(10);
-            assert_eq!(vec.is_real(), true);
-            assert_eq!(vec.len(), 10);
-        }
+    /// Return `TRUE` if the vector has no `NA`s, `FALSE` if any, or `NA_BOOL` if unknown.
+    pub fn no_na(&self) -> Rbool {
+        unsafe { REAL_NO_NA(self.get()).into() }
     }
 }
 
diff --git a/extendr-api/src/wrapper/integers.rs b/extendr-api/src/wrapper/integers.rs
index b86080ebf..7b8c3365c 100644
--- a/extendr-api/src/wrapper/integers.rs
+++ b/extendr-api/src/wrapper/integers.rs
@@ -30,6 +30,26 @@ crate::wrapper::macros::gen_vector_wrapper_impl!(
     altrep_constructor: make_altinteger_from_iterator,
 );
 
+impl Integers {
+    /// Get a region of elements from the vector.
+    pub fn get_region(&self, index: usize, dest: &mut [Rint]) -> usize {
+        unsafe {
+            let ptr: *mut i32 = dest.as_mut_ptr() as *mut i32;
+            INTEGER_GET_REGION(self.get(), index as R_xlen_t, dest.len() as R_xlen_t, ptr) as usize
+        }
+    }
+
+    /// Return `TRUE` if the vector is sorted, `FALSE` if not, or `NA_BOOL` if unknown.
+    pub fn is_sorted(&self) -> Rbool {
+        unsafe { INTEGER_IS_SORTED(self.get()).into() }
+    }
+
+    /// Return `TRUE` if the vector has no `NA`s, `FALSE` if any, or `NA_BOOL` if unknown.
+    pub fn no_na(&self) -> Rbool {
+        unsafe { INTEGER_NO_NA(self.get()).into() }
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use crate::prelude::*;
diff --git a/extendr-api/src/wrapper/logicals.rs b/extendr-api/src/wrapper/logicals.rs
index 5d4c92c6f..278b004ad 100644
--- a/extendr-api/src/wrapper/logicals.rs
+++ b/extendr-api/src/wrapper/logicals.rs
@@ -31,6 +31,16 @@ crate::wrapper::macros::gen_vector_wrapper_impl!(
     altrep_constructor: make_altlogical_from_iterator,
 );
 
+impl Logicals {
+    /// Get a region of elements from the vector.
+    pub fn get_region(&self, index: usize, dest: &mut [Rbool]) -> usize {
+        unsafe {
+            let ptr: *mut i32 = dest.as_mut_ptr() as *mut i32;
+            LOGICAL_GET_REGION(self.get(), index as R_xlen_t, dest.len() as R_xlen_t, ptr) as usize
+        }
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use crate::prelude::*;
diff --git a/extendr-api/src/wrapper/macros.rs b/extendr-api/src/wrapper/macros.rs
index 1a9cafef7..7095f4cbc 100644
--- a/extendr-api/src/wrapper/macros.rs
+++ b/extendr-api/src/wrapper/macros.rs
@@ -91,24 +91,6 @@ macro_rules! gen_vector_wrapper_impl {
                 }
             }
 
-            /// Get a region of elements from the vector.
-            pub fn get_region(&self, index: usize, dest: &mut [$scalar_type]) -> usize {
-                unsafe {
-                    let ptr : *mut $primitive_type = dest.as_mut_ptr() as *mut $primitive_type;
-                    paste::paste!{ [<$r_prefix _GET_REGION>](self.get(), index as R_xlen_t, dest.len() as R_xlen_t, ptr) as usize }
-                }
-            }
-
-            /// Return `TRUE` if the vector is sorted, `FALSE` if not, or `NA_BOOL` if unknown.
-            pub fn is_sorted(&self) -> Rbool {
-                unsafe { paste::paste!{ [<$r_prefix _IS_SORTED>](self.get()).into() } }
-            }
-
-            /// Return `TRUE` if the vector has no `NA`s, `FALSE` if any, or `NA_BOOL` if unknown.
-            pub fn no_na(&self) -> Rbool {
-                unsafe { paste::paste!{ [<$r_prefix _NO_NA>](self.get()).into() } }
-            }
-
             paste::paste!{
                 #[doc = "Return an iterator for a " $doc_name " object."]
                 #[doc = "Forces ALTREP objects to manifest."]
diff --git a/extendr-api/src/wrapper/mod.rs b/extendr-api/src/wrapper/mod.rs
index f4ec8bd60..9a1481134 100644
--- a/extendr-api/src/wrapper/mod.rs
+++ b/extendr-api/src/wrapper/mod.rs
@@ -6,6 +6,7 @@ use crate::*;
 use libR_sys::*;
 
 pub mod altrep;
+pub mod complexes;
 pub mod doubles;
 pub mod environment;
 pub mod expr;
@@ -32,6 +33,7 @@ pub use altrep::{
     AltComplexImpl, AltIntegerImpl, AltLogicalImpl, AltRawImpl, AltRealImpl, AltStringImpl, Altrep,
     AltrepImpl,
 };
+pub use complexes::Complexes;
 pub use doubles::Doubles;
 pub use environment::{EnvIter, Environment};
 pub use expr::Expression;
@@ -197,6 +199,12 @@ make_conversions!(S4, ExpectedS4, is_s4, "Not a S4 type");
 make_conversions!(Integers, ExpectedInteger, is_integer, "Not an integer type");
 make_conversions!(Logicals, ExpectedLogical, is_logical, "Not a logical type");
 make_conversions!(Doubles, ExpectedReal, is_real, "Not a floating point type");
+make_conversions!(
+    Complexes,
+    ExpectedComplex,
+    is_complex,
+    "Not a complex number or vector"
+);
 // make_conversions!(Function, ExpectedFunction, is_function, "Not a function");
 
 make_conversions!(Strings, ExpectedString, is_string, "Not a string vector");
diff --git a/extendr-engine/src/lib.rs b/extendr-engine/src/lib.rs
index d841cca8b..72562ee4d 100644
--- a/extendr-engine/src/lib.rs
+++ b/extendr-engine/src/lib.rs
@@ -43,7 +43,7 @@ pub fn start_r() {
             // In case you are curious.
             // Maybe 8MB is a bit small.
             // eprintln!("R_CStackLimit={:016x}", R_CStackLimit);
-            R_CStackLimit = usize::max_value();
+            R_CStackLimit = usize::MAX;
 
             setup_Rmainloop();
         }


Test patch:
diff --git a/extendr-api/tests/altrep_tests.rs b/extendr-api/tests/altrep_tests.rs
index ea909fc31..58497c8d4 100644
--- a/extendr-api/tests/altrep_tests.rs
+++ b/extendr-api/tests/altrep_tests.rs
@@ -191,8 +191,8 @@ fn test_altcomplex() {
         }
 
         impl AltComplexImpl for MyCompactComplexRange {
-            fn elt(&self, index: usize) -> Cplx {
-                Cplx(self.start + self.step * index as f64, self.start + self.step * index as f64)
+            fn elt(&self, index: usize) -> Rcplx {
+                Rcplx::from(c64::new(self.start + self.step * index as f64, self.start + self.step * index as f64))
             }
         }
 
diff --git a/extendr-api/tests/scalar_tests.rs b/extendr-api/tests/scalar_tests.rs
index 9d35ab82b..8956989af 100644
--- a/extendr-api/tests/scalar_tests.rs
+++ b/extendr-api/tests/scalar_tests.rs
@@ -232,6 +232,80 @@ fn test_rfloat() {
     }
 }
 
+#[test]
+#[cfg(feature = "num-complex")]
+fn test_rcplx() {
+    test! {
+        let a = Rcplx::from((20., 300.));
+        let b = Rcplx::from((10., 400.));
+        assert_eq!(a + b, Rcplx::from((30., 700.)));
+        assert_eq!(a - b, Rcplx::from((10., -100.)));
+        assert_eq!(a * b, Rcplx::from((-119800.0, 11000.0)));
+
+        let a = Rcplx::from(20.);
+        let b = Rcplx::from(10.);
+        assert_eq!(a / b, Rcplx::from(2.));
+        assert_eq!(-a, Rcplx::from(-20.));
+
+        assert_eq!(&a + b, Rcplx::from(30.));
+        assert_eq!(&a - b, Rcplx::from(10.));
+        assert_eq!(&a * b, Rcplx::from(200.));
+        assert_eq!(&a / b, Rcplx::from(2.));
+        assert_eq!(-&a, Rcplx::from(-20.));
+
+        // NA lhs
+        let a = Rcplx::na();
+        let b = Rcplx::from(10.);
+        assert!((a + b).is_na());
+        assert!((a - b).is_na());
+        assert!((a * b).is_na());
+        assert!((a / b).is_na());
+        assert!((-a).is_na());
+
+        // NA rhs
+        let a = Rcplx::from(10.);
+        let b = Rcplx::na();
+        assert!((a + b).is_na());
+        assert!((a - b).is_na());
+        assert!((a * b).is_na());
+        assert!((a / b).is_na());
+
+        // Inf is a single value, so can be tested for equality
+        let a = Rcplx::from(f64::INFINITY);
+        let b = Rcplx::from(42.);
+        assert_eq!(a + b, a);
+        assert_eq!(a - b, a);
+        assert_eq!(b - a, Rcplx::from(f64::NEG_INFINITY));
+        // assert_eq!(a * b, a);
+        // assert_eq!(a / b, a);
+        assert_eq!(-a, Rcplx::from(f64::NEG_INFINITY));
+
+        let a = Rcplx::from(f64::NEG_INFINITY);
+        assert_eq!(a + b, a);
+        assert_eq!(a - b, a);
+        assert_eq!(b - a, Rcplx::from(f64::INFINITY));
+        // assert_eq!(a * b, a);
+        // assert_eq!(a / b, a);
+        assert_eq!(-a, Rcplx::from(f64::INFINITY));
+
+        // Operations with NaN produce NaN
+        let a = Rcplx::from(f64::NAN);
+        assert!((a + b).is_nan());
+        assert!((a - b).is_nan());
+        assert!((a * b).is_nan());
+        assert!((a / b).is_nan());
+        assert!((-a).is_nan());
+
+        // Infinity
+        assert!(Rcplx::from(f64::INFINITY).is_infinite());
+        assert!(Rcplx::from(f64::NEG_INFINITY).is_infinite());
+        assert!(!Rcplx::from(0.).is_infinite());
+
+        // Some more, testing mixed binary operators
+        assert!((Rcplx::from(f64::INFINITY) + Rcplx::from(1.)).is_infinite());
+    }
+}
+
 #[test]
 fn test_rfloat_opassign() {
     test! {
diff --git a/extendr-api/tests/vector_tests.rs b/extendr-api/tests/vector_tests.rs
index 292c8e0c1..793aaec29 100644
--- a/extendr-api/tests/vector_tests.rs
+++ b/extendr-api/tests/vector_tests.rs
@@ -129,6 +129,56 @@ fn test_doubles() {
     }
 }
 
+#[test]
+fn test_complexes() {
+    test! {
+        let s = Complexes::new(10);
+        assert_eq!(s.len(), 10);
+        assert_eq!(s.rtype(), RType::Complex);
+
+        let s = Complexes::from_values([1.0, 2.0, 3.0]);
+        assert_eq!(s.len(), 3);
+        assert_eq!(s.rtype(), RType::Complex);
+        assert_eq!(s.elt(0), 1.0);
+        assert_eq!(s.elt(1), 2.0);
+        assert_eq!(s.elt(2), 3.0);
+        assert!(s.elt(3).is_na());
+
+        let v = s.iter().collect::<Vec<Rcplx>>();
+        assert_eq!(v, [1.0, 2.0, 3.0]);
+
+        // s.set_elt(1, 5.0.into());
+        // assert_eq!(s.elt(1), 5.0);
+
+        let s : Complexes = [1.0, 2.0, 3.0].iter().map(|i| Rcplx::from(*i)).collect();
+        let v = s.iter().collect::<Complexes>();
+        assert_eq!(v, Complexes::from_values([1.0, 2.0, 3.0]));
+
+        // Bug: from_values should be Into<Rcplx>
+        //let s = Complexes::from_values([Rint::from(1), Rint::na(), Rint::from(3)]);
+        //assert_eq!(s.elt(1).is_na(), true);
+
+        // let robj = r!([Rcplx::from(1.0), Rcplx::from(2.0), Rcplx::from(3.0)]);
+        let robj = r!([(1.0, 0.0), (2.0, 0.0), (3.0, 0.0)]);
+        let s = Complexes::try_from(robj)?;
+        assert_eq!(s.len(), 3);
+        assert_eq!(s.elt(0), 1.0);
+
+        // Test Deref and DerefMut.
+        let mut s = Complexes::from_values([1.0, 2.0, 3.0]);
+        assert_eq!(s[0], 1.0);
+        assert_eq!(s[1], 2.0);
+        assert_eq!(s[2], 3.0);
+
+        s[0] = 4.0.into();
+        s[1] = 5.0.into();
+        s[2] = 6.0.into();
+        assert_eq!(s[0], 4.0);
+        assert_eq!(s[1], 5.0);
+        assert_eq!(s[2], 6.0);
+    }
+}
+
 #[test]
 fn test_integers() {
     test! {
@@ -191,3 +241,126 @@ fn test_rstr() {
         assert_eq!(x, "xyz");
     }
 }
+
+#[test]
+fn test_doubles_from_iterator() {
+    test! {
+        let vec : Doubles = (0..3).map(|i| (i as f64).into()).collect();
+        assert_eq!(vec, Doubles::from_values([0.0, 1.0, 2.0]));
+    }
+}
+#[test]
+fn test_doubles_iter_mut() {
+    test! {
+        let mut vec = Doubles::from_values([0.0, 1.0, 2.0, 3.0]);
+        vec.iter_mut().for_each(|v| *v = *v + 1.0);
+        assert_eq!(vec, Doubles::from_values([1.0, 2.0, 3.0, 4.0]));
+    }
+}
+
+#[test]
+fn test_doubles_iter() {
+    test! {
+        let vec = Doubles::from_values([0.0, 1.0, 2.0, 3.0]);
+        assert_eq!(vec.iter().sum::<Rfloat>(), 6.0);
+    }
+}
+
+#[test]
+fn test_doubles_from_values_short() {
+    test! {
+        // Short (<64k) vectors are allocated.
+        let vec = Doubles::from_values((0..3).map(|i| 2.0 - i as f64));
+        assert_eq!(vec.is_altrep(), false);
+        assert_eq!(r!(vec.clone()), r!([2.0, 1.0, 0.0]));
+        assert_eq!(vec.elt(1), 1.0);
+        let mut dest = [0.0.into(); 2];
+        vec.get_region(1, &mut dest);
+        assert_eq!(dest, [1.0, 0.0]);
+    }
+}
+#[test]
+fn test_doubles_from_values_long() {
+    test! {
+        // Long (>=64k) vectors are lazy ALTREP objects.
+        let vec = Doubles::from_values((0..1000000000).map(|x| x as f64));
+        assert_eq!(vec.is_altrep(), true);
+        assert_eq!(vec.elt(12345678), 12345678.0);
+        let mut dest = [0.0.into(); 2];
+        vec.get_region(12345678, &mut dest);
+        assert_eq!(dest, [12345678.0, 12345679.0]);
+    }
+}
+
+#[test]
+fn test_doubles_new() {
+    test! {
+        let vec = Doubles::new(10);
+        assert_eq!(vec.is_real(), true);
+        assert_eq!(vec.len(), 10);
+    }
+}
+
+#[cfg(feature = "num-complex")]
+mod num_complex {
+    use extendr_api::prelude::*;
+
+    #[test]
+    fn from_iterator() {
+        test! {
+            let vec : Complexes = (0..3).map(|i| (i as f64).into()).collect();
+            assert_eq!(vec, Complexes::from_values([0.0, 1.0, 2.0]));
+        }
+    }
+    #[test]
+    fn iter_mut() {
+        test! {
+            let mut vec = Complexes::from_values([0.0, 1.0, 2.0, 3.0]);
+            vec.iter_mut().for_each(|v| *v = *v + Rcplx::from(1.0));
+            assert_eq!(vec, Complexes::from_values([1.0, 2.0, 3.0, 4.0]));
+        }
+    }
+
+    #[test]
+    fn iter() {
+        test! {
+            let vec = Complexes::from_values([0.0, 1.0, 2.0, 3.0]);
+            assert_eq!(vec.iter().sum::<Rcplx>(), Rcplx::from(6.0));
+        }
+    }
+
+    #[test]
+    fn from_values_short() {
+        test! {
+            // Short (<64k) vectors are allocated.
+            let vec = Complexes::from_values((0..3).map(|i| 2.0 - i as f64));
+            assert_eq!(vec.is_altrep(), false);
+            assert_eq!(r!(vec.clone()), r!([Rcplx::from(2.0), Rcplx::from(1.0), Rcplx::from(0.0)]));
+            assert_eq!(vec.elt(1), Rcplx::from(1.0));
+            let mut dest = [0.0.into(); 2];
+            vec.get_region(1, &mut dest);
+            assert_eq!(dest, [Rcplx::from(1.0), Rcplx::from(0.0)]);
+        }
+    }
+    #[test]
+    fn from_values_long() {
+        test! {
+            // Long (>=64k) vectors are lazy ALTREP objects.
+            let vec = Complexes::from_values((0..1000000000).map(|x| x as f64));
+            assert_eq!(vec.is_altrep(), true);
+            assert_eq!(vec.elt(12345678), Rcplx::from(12345678.0));
+            let mut dest = [0.0.into(); 2];
+            vec.get_region(12345678, &mut dest);
+            assert_eq!(dest, [Rcplx::from(12345678.0), Rcplx::from(12345679.0)]);
+        }
+    }
+
+    #[test]
+    fn new() {
+        test! {
+            let vec = Complexes::new(10);
+            assert_eq!(vec.is_complex(), true);
+            assert_eq!(vec.len(), 10);
+        }
+    }
+}
diff --git a/tests/extendrtests/R/extendr-wrappers.R b/tests/extendrtests/R/extendr-wrappers.R
index df938957b..7d11e762a 100644
--- a/tests/extendrtests/R/extendr-wrappers.R
+++ b/tests/extendrtests/R/extendr-wrappers.R
@@ -34,6 +34,8 @@ get_logicals_element <- function(x, i) .Call(wrap__get_logicals_element, x, i)
 
 doubles_square <- function(input) .Call(wrap__doubles_square, input)
 
+complexes_square <- function(input) .Call(wrap__complexes_square, input)
+
 integers_square <- function(input) .Call(wrap__integers_square, input)
 
 logicals_not <- function(input) .Call(wrap__logicals_not, input)
diff --git a/tests/extendrtests/src/rust/src/lib.rs b/tests/extendrtests/src/rust/src/lib.rs
index 7475b3c7a..73ab9da07 100644
--- a/tests/extendrtests/src/rust/src/lib.rs
+++ b/tests/extendrtests/src/rust/src/lib.rs
@@ -123,6 +123,17 @@ fn doubles_square(input: Doubles) -> Doubles {
     result
 }
 
+#[extendr(use_try_from = true)]
+fn complexes_square(input: Complexes) -> Complexes {
+    let mut result = Complexes::new(input.len());
+
+    for (x, y) in result.iter_mut().zip(input.iter()) {
+        *x = Rcplx::from((y.re() * y.re(), 0.0.into()));
+    }
+
+    result
+}
+
 #[extendr(use_try_from = true)]
 fn integers_square(input: Integers) -> Integers {
     let mut result = Integers::new(input.len());
@@ -265,6 +276,7 @@ extendr_module! {
     fn get_logicals_element;
 
     fn doubles_square;
+    fn complexes_square;
     fn integers_square;
     fn logicals_not;
 
diff --git a/tests/extendrtests/tests/testthat/test-vector-wrappers.R b/tests/extendrtests/tests/testthat/test-vector-wrappers.R
index a82df929c..e8433ac1e 100644
--- a/tests/extendrtests/tests/testthat/test-vector-wrappers.R
+++ b/tests/extendrtests/tests/testthat/test-vector-wrappers.R
@@ -33,6 +33,11 @@ test_that("Construct double vector from squares of given values", {
     expect_equal(doubles_square(x), x * x)
 })
 
+test_that("Construct complex vector from squares of given values", {
+    x <- as.complex(c(1.0 * (1:100)))
+    expect_equal(complexes_square(x), x * x)
+})
+
 test_that("Construct integer vector from squares of given values", {
   x <- c(1:100)
   expect_equal(integers_square(x), x * x)