Repository: extendr/extendr
Pull request #274 (2021-08-30 16:21:00)

Altrep Integers class.
This is a sample for an Integers class that uses Altrep
methods to handle very large vectors.

There are no iterators at present as we would need to implement
Rint first.

The USP of this is the ability to make vectors from iterators without
allocating memory.

We support elt(), get_region(), is_sorted() and no_na() correctly.

We do not have access to altrep min() and max() since these were removed
from Rinternals.h

Suggestions on a postcard...

clauswilke: Thanks for your efforts! Two comments:

- What about the non-altrep variant? I believe the idea was that `Integers` would be an opaque wrapper that can represent both altrep and regular vectors. @Ilia-Kosenkov suggested `AltIntegers` for an altrep-only wrapper for integers: https://github.com/extendr/extendr/blob/03d06173fd457c338bfe371a9d9f211c44f32051/marshaling_rules.md#naming-convention So maybe implement `AltIntegers` first and then use that to implement `Integers`?
- If `Rint` is needed for feature completeness should that be implemented first or at the same time? I assume it's relatively lightweight.
andy-thomason: This should handle both the ALTREP and regular vectors.

The methods we call like get_region and elt should also work
for regular vectors.

When constructing vectors, I'm chosing to use regular vectors
for small ones and ALTVEC for larger ones. This is because there
is some overhead in creating the class and vector and we
need to generate the class every time.

The regular conversion rules should accept anything with INTSXP
as input. The macro in mod.rs converts from Robj to Integers and
vice versa.

clauswilke: > The methods we call like get_region and elt should also work
for regular vectors.

I see, thanks!

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4ff257f9e..e43f49a0b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,6 +24,10 @@
 
 - `SymPair::sym_pair()` now returns `(Option<Robj>, Robj)`
 
+- Bump `ndarray` to 0.15.3. Under [RFC 1977](https://github.com/rust-lang/rfcs/pull/1977) this is a "public dependency" change, and therefore can be considered a breaking change, as consumers of `extendr` that use an older version of `ndarray` will no longer be compatible until they also bump `ndarray` to a compatible version.
+
+- Implement `TryFrom<&ArrayBase> for Robj`, allowing `extendr`-annotated functions to return Arrays from the `ndarray` crate and have them automatically converted to R arrays. Note, even if 1D arrays are returned they will not be returned as vectors.
+
 ## extendr 0.2.0
 
 - Added contributing guidelines and code of conduct.
diff --git a/design_specifications/marshaling_rules.md b/design_specifications/marshaling_rules.md
new file mode 100644
index 000000000..c205e5182
--- /dev/null
+++ b/design_specifications/marshaling_rules.md
@@ -0,0 +1,165 @@
+# `R` <--> `Rust` marshaling rules
+
+This document outlines conversion and validation rules between `R` and `Rust` types.
+The aim is to maintain type safety on the `Rust` side without sacrificing usability on the `R` side.
+
+
+# Conversion and validation 
+
+`extendr` takes into account the following problems when converting `R` object to `Rust` object:
+
+- `NA` handling
+  - If `Rust` type is `NA`-aware, `NA`-validation falls onto the user code
+  - If `Rust` type is `NA`-oblivious (e.g., vector of basic type), `extendr` performs `NA` validation at runtime and `panic!`s if `NA` value is found (introduces overhead)
+- ALTREP handling
+  - If `extendr`-provided iterator is used, then ALTREP is exposed as an obscure type with iteration and indexing capabilities
+  - If `Rust` type has no notion of ALTREP, then `extendr` expands ALTREP vector into an array, allocating memory (introduces overhead)
+- Type conversion (applicable to `Vec<T>`)
+  - `logical()`, `raw()`, `character()` are treated as-is. If there is a `Rust` - `R` type mismatch, `extendr` wrapper `panic!`s
+  - `integer()` can be passed to functions that expect `double()` or `complex()`. `extendr` performs type cast, allocating memory for the new vector
+  - `double()` can be safely passed as `complex()` 
+  - `double()` can sometimes be passed as `integer()`, if its values are representable by `i32`
+  - `complex()` can sometimes be passed as `double()` or `integer()` (see reasoning above)
+  - Whenever a numeric type-mismatch happens, a guaranteed allocation occurs
+  - An obscure iterator that accepts one of `integer()`, `double()` (and maybe `complex()`) handles both vectors and ALTREPs, does not allocate and offloads all validation and type checks onto user
+
+
+Here is a list of examples:
+- `Vec<i32>` triggers `NA` validation, altrep expanding, and type coercion if compatible (so `1.0` or `1.0 + 0i` convert to `1L`). Heavy on overhead and memory allocation, good for prototypes and testing things out.
+
+- `Integers` is an obscure wrapper of either `&[Rint]` or some `AltIntegers`. Can be used to obtain an iterator, items of which are of type `Rint`, providing correct `NA` handling. Preferred way to handle vectors, similar to that of `{cpp11}`. 
+
+- `Numerics` is a discriminated union of `Integers | Doubles`. Accepts all numeric inputs, but leaves it up to the user to decipher what exactly was received from `R`. No runtime validation, no extra allocation, ALTREPS remain not expanded. 
+- `ComplexNumerics` represents either `Complexes` or `Numerics`
+
+
+----------------------------------------------------------------------------
+
+# Underlying vector types
+## Terminology
+A 'vector' is a primitive type used in `R`. Vectors are designed to behave as a strongly typed 1D array of objects. There are two different implementations of vector types: one is basically a pointer to a contiguous block of memory with known length (and some additional metadata), another is an iterator deigned to store rules for generating sequences of elements (instead of storing potentially very large vectors in memory). Array-based vectors shall be referred to as 'plain old data' (POD), iterators -- as ALTREP.
+
+`R` recognizes the following vector types that are directly exposed to the user:
+ - `logical (i32)`
+ - `integer (i32)`
+ - `real (f64)`
+ - `complex (f64, f64)`
+ - `raw (u8)`
+ - `character (usize)` (collection of pointers to character arrays)
+
+Each vector can contain special `NA` values. None of the primitive types have built-in support for `NA` (including `f64`, which has notion of `NaN`, a different thing), so `R` treats one value from the range of allowed values as `NA`. For instance, `NA_integer_` is `i32::MIN`, which is `1i32 << 31 = -2147483648`. As a result, `x <- -2147483648L` results in an error in `R`.
+
+## `Rust` counterparts
+
+`R` objects passed to `Rust` require additional validation and transformation. Let us define the following types:
+- `struct Rint(i32)`
+- `struct Rbool(i32)`
+- `struct Rfloat(f64)`
+- `struct Rbyte(u8)`
+- `struct Rcomplex((f64, f64))`
+- `struct Rstr(usize)` (?)
+
+Note: `complex` is an `(f64, f64)` struct. Support of implementations such as `num::complex::Complex` can be enabled using feature-guards (see example of arrays and `ndarray`).
+
+Each of these types is binary compatible with their underlying type. An array of, say `i32`, represented by a `*i32` pointer and length, can be viewed as `*Rint` of the same length. 
+This can be the preferred solution when dealing with `R` plain vectors.
+
+For each supported primitive type `T` `Rt` would be its minimal wrapper. E.g., for `T = i32`, `Rt = Rint`.
+`extendr` prefers `Rt` over `T` types. Parameters that use `T`-derived types will require runtime `NA` validation, introducing implicit overhead.
+
+Type conversion traits for `Rt` are:
+- `Into<Option<T>>` (this is always a valid conversion),
+- `TryInto<T>`, errors on `NA`,
+- `TryFrom<T>`, errors if provided argument equals to the value reserved for `NA`,
+- `TryFrom<Option<T>>`, errors if provided argument is `Some(NA)`, i.e. wraps value reserved for `NA`. 
+
+These conversions can be grouped in a trait `Rtype<T>`, which exposes conversions `Rt` <--> `T` mentioned above, as well as some `is_na() -> bool` method (and perhaps some other useful ones).
+
+A limited number of binary-incompatible type conversions is also allowed. These rules are required to support common use scenarios on `R` side.
+
+For `Rint` the following is allowed:
+- `Into<Rfloat>`, this is always correct (all `i32` are within `f64` with no loss of accuracy)
+- `Into<Rcomplex>`, for the same reason
+  
+For `Rfloat`
+- `Into<Rcomplex>`, (`Real(f64)` are within `(Real(f64), Imaginary(f64))`)
+- `TryInto<Rint>`; this conversion succeeds only when `f64` can be precisely represented as `i32` (lossless), e.g. `1.0f64` convert to `1i32`
+
+For `Rcomplex` (see reasoning above)
+- `TryInto<Rfloat>`
+- `TryInto<Rint>`
+
+Other primitive types are treated as-is and any type conversion should be performed by extracting the underlying value (or `NA`) and casting/converting it to another type manually.
+
+### ALTREP
+
+A separate public API for ALTREPs is not needed, there are no real use cases for a method to only accept ALTREPs. Instead, expose the following wrapper types:
+- `Integers`
+- `Logicals`
+- `Doubles`
+- `Raws`
+- `Complexes`
+- `Strings`
+
+
+These opaque types wrap either plain data vectors (e.g., storing pointer & length) or ALTREPs. 
+They should implement `std::iter::Iterator<Item = Rt>` to support `NA` validation, as well as `std::ops::Index<Output = Rt>`.
+
+Another suggested methods:
+- `len() -> usize` as both plain data and ALTREP know their size,
+- `is_altrep() -> bool` to avoid unnecessary random access in case of ALTREP
+
+The iterators are enriched with the following discriminated unions:
+- `Numerics = Integers | Doubles`
+- `ComplexNumerics = Integers | Doubles | Complexes`
+
+
+## Naming convention
+- Public (exported) wrapper types should have simple and concise names, possibly derived from names of their counterparts on `R` side, or from `{cpp11}` naming convention. Types that wrap vectors should have pluralized names, e.g. `Integers` (not `Integer`), to emphasize that they wrap a collection, not a single value. The notable exception is `Strings`, which is the preferred name for `R`'s `character()` (which is not a *character* collection, but rather a collection of pointers to strings, thus *strings*). Wrappers of non-vector types should (in most cases) have the same name as the type they wrap, e.g. `List`, `PairList`, `Environment`, `DataFrame` (period is removed).
+
+
+- Rust iterator types should be suffixed with `Iter`, e.g. `IntegersIter` or `ListIter`. This establishes a 1-to-1 relationship between a wrapper and its iterator.
+
+
+- Rust types wrapping altreps should have `Alt` prefix, e.g. `AltIntegers`. This is somewhat similar to the naming convention adopted in `R`' headers (see `include/R_ext/Altrep.h`). If an iterator type is provided for `AltT`, then both prefix and suffix should be used, e.g. `AltIntegersIter` or `AltNumericsIter`.
+
+
+
+
+-----------------------------------------------------------------------------------------------
+<details>
+<summary> TL;DR </summary>
+Here is a set of functions with different parameter types and allowed arguments.
+
+1. Default (aka comfortable on both ends)
+```Rust
+#[extendr]
+fn fn_1(x : Vec<i32>)
+```
+| `R` type               | Allocation  | Coercion | Error            | Validation         |
+| ---------------------- | ----------- | -------- | ---------------- | ------------------ |
+| `integer()`            | Yes         | No       |  If `NA` found   | Runtime            |
+| `altrep_integer()`     | Yes         | No       |  If `NA` found   | Runtime            |
+| `real()` / `complex()` | Yes         | Yes      |  If `NA` found   | Runtime            |
+
+2. Close to metal (aka performance)
+```Rust
+#[extendr]
+fn fn_2(x : ComplexNumerics)
+```
+| `R` type               | Allocation  | Coercion | Error            | Validation  |
+| ---------------------- | ----------- | -------- | ---------------- | ----------- |
+| `integer()`            | No          | No       |  No              | User        |
+| `altrep_integer()`     | No          | No       |  No              | User        |
+| `double()`             | No          | No       |  No              | User        |
+| `altrep_double()`      | No          | No       |  No              | User        |
+| `complex()`            | No          | No       |  No              | User        |
+| `altrep_complex()`     | No          | No       |  No              | User        |
+
+
+
+
+</details>
+
+# Return type conversions
+The procedure is reversed. The preferred way it so return a `Vec<Rt>`, which is correctly encodes `NA`s. If `Vec<T>` is returned, then validation is performed by the wrapper, and `panic!` occurs if an invalid value is found (i.e., if `Vec<i32>` contains `i32::MIN`, which is an invalid value in `R`).
diff --git a/extendr-api/Cargo.toml b/extendr-api/Cargo.toml
index 22551b21e..7f82383bd 100644
--- a/extendr-api/Cargo.toml
+++ b/extendr-api/Cargo.toml
@@ -17,7 +17,7 @@ repository = "https://github.com/extendr/extendr"
 libR-sys = "0.2.2"
 extendr-macros = { path = "../extendr-macros", version="0.2.0" }
 extendr-engine = { path = "../extendr-engine", version="0.2.0" }
-ndarray = { version = "0.13.1", optional = true }
+ndarray = { version = "0.15.3", optional = true }
 lazy_static = "1.4"
 
 [features]
diff --git a/extendr-api/src/lib.rs b/extendr-api/src/lib.rs
index 8c56f5093..2b1efa969 100644
--- a/extendr-api/src/lib.rs
+++ b/extendr-api/src/lib.rs
@@ -233,6 +233,7 @@ pub mod prelude;
 pub mod rmacros;
 
 pub mod robj;
+pub mod scalar;
 pub mod thread_safety;
 pub mod wrapper;
 
@@ -241,6 +242,7 @@ pub mod robj_ndarray;
 
 pub use std::convert::{TryFrom, TryInto};
 pub use std::ops::Deref;
+pub use std::ops::DerefMut;
 
 //////////////////////////////////////////////////
 // Note these pub use statements are deprectaed
diff --git a/extendr-api/src/prelude.rs b/extendr-api/src/prelude.rs
index cb05daff3..47518d58c 100644
--- a/extendr-api/src/prelude.rs
+++ b/extendr-api/src/prelude.rs
@@ -51,8 +51,8 @@ pub use super::thread_safety::{
 };
 
 pub use super::wrapper::{
-    EnvIter, Environment, Expression, FromList, Function, Language, List, ListIter, Nullable,
-    Pairlist, Primitive, Promise, Raw, Rstr, Symbol,
+    EnvIter, Environment, Expression, FromList, Function, Integers, Language, List, ListIter,
+    Nullable, Pairlist, Primitive, Promise, Raw, Rstr, Symbol,
 };
 
 #[cfg(feature = "ndarray")]
@@ -68,3 +68,5 @@ pub use super::iter::{Int, Logical, Real, StrIter};
 pub use std::convert::{TryFrom, TryInto};
 
 pub use std::ops::Index;
+
+pub use super::scalar::*;
diff --git a/extendr-api/src/robj/mod.rs b/extendr-api/src/robj/mod.rs
index 0d1c1b7a2..4990353a6 100644
--- a/extendr-api/src/robj/mod.rs
+++ b/extendr-api/src/robj/mod.rs
@@ -14,6 +14,7 @@ use std::os::raw;
 
 use crate::*;
 
+use crate::scalar::Rint;
 use std::collections::HashMap;
 use std::iter::IntoIterator;
 use std::ops::{Range, RangeInclusive};
@@ -707,6 +708,7 @@ macro_rules! make_typed_slice {
 
 make_typed_slice!(Bool, INTEGER, LGLSXP);
 make_typed_slice!(i32, INTEGER, INTSXP);
+make_typed_slice!(Rint, INTEGER, INTSXP);
 make_typed_slice!(f64, REAL, REALSXP);
 make_typed_slice!(u8, RAW, RAWSXP);
 
diff --git a/extendr-api/src/robj/try_from_robj.rs b/extendr-api/src/robj/try_from_robj.rs
index f23381b5f..54164d54e 100644
--- a/extendr-api/src/robj/try_from_robj.rs
+++ b/extendr-api/src/robj/try_from_robj.rs
@@ -40,7 +40,7 @@ macro_rules! impl_try_from_scalar_integer {
                 // might eventually become available in future, though.
                 if let Some(v) = robj.as_real() {
                     let result = v as Self;
-                    if ((result as f64 - v).abs() < f64::EPSILON) {
+                    if (result as f64 - v).abs() < f64::EPSILON {
                         return Ok(result);
                     } else {
                         return Err(Error::ExpectedWholeNumber(robj));
diff --git a/extendr-api/src/robj_ndarray.rs b/extendr-api/src/robj_ndarray.rs
index 3e770c95f..d8413bc79 100644
--- a/extendr-api/src/robj_ndarray.rs
+++ b/extendr-api/src/robj_ndarray.rs
@@ -1,6 +1,8 @@
 #[doc(hidden)]
 use ndarray::prelude::*;
+use ndarray::{Data, ShapeBuilder};
 
+use crate::prelude::dim_symbol;
 use crate::*;
 
 impl<'a, T> FromRobj<'a> for ArrayView1<'a, T>
@@ -55,6 +57,42 @@ make_array_view_2!(f64, "Not a floating point matrix.");
 //         r!(mx)
 //     }
 // }
+impl<A, S, D> TryFrom<&ArrayBase<S, D>> for Robj
+where
+    S: Data<Elem = A>,
+    A: Copy + ToVectorValue,
+    D: Dimension,
+{
+    type Error = Error;
+
+    fn try_from(value: &ArrayBase<S, D>) -> Result<Self> {
+        // Refer to https://github.com/rust-ndarray/ndarray/issues/1060 for an excellent discussion
+        // on how to convert from `ndarray` types to R/fortran arrays
+        // This thread has informed the design decisions made here.
+
+        // In general, transposing and then iterating an ndarray in C-order (`iter()`) is exactly
+        // equivalent to iterating that same array in Fortan-order (which `ndarray` doesn't currently support)
+        value
+            .t()
+            .iter()
+            // Since we only have a reference, we have to copy all elements so that we can own the entire R array
+            .copied()
+            .collect_robj()
+            .set_attrib(
+                dim_symbol(),
+                value
+                    .shape()
+                    .iter()
+                    .map(|x| i32::try_from(*x))
+                    .collect::<std::result::Result<Vec<i32>, <i32 as TryFrom<usize>>::Error>>()
+                    .map_err(|_err| {
+                        Error::Other(String::from(
+                            "One or more array dimensions were too large to be handled by R.",
+                        ))
+                    })?,
+            )
+    }
+}
 
 #[test]
 fn test_from_robj() {
@@ -127,3 +165,67 @@ fn test_from_robj() {
         // assert_eq!(mx[[3, 1]], 8);
     }
 }
+
+#[test]
+fn test_to_robj() {
+    test! {
+        // An empty array should still convert to an empty R array with the same shape
+        assert_eq!(
+            &Robj::try_from(&Array4::<i32>::zeros((0, 1, 2, 3).f()))?,
+            &R!("array(integer(), c(0, 1, 2, 3))")?
+        );
+
+        assert_eq!(
+            &Robj::try_from(&array![1., 2., 3.])?,
+            &R!("array(c(1, 2, 3))")?
+        );
+
+        // We give both R and Rust the same 1d vector and tell them both to read it as a matrix
+        // in C order. Therefore these arrays should be the same.
+        assert_eq!(
+            &Robj::try_from(&Array::from_shape_vec((2, 3), vec![1., 2., 3., 4., 5., 6.]).unwrap())?,
+            &R!("matrix(c(1, 2, 3, 4, 5, 6), nrow=2, byrow=TRUE)")?
+        );
+
+        // We give both R and Rust the same 1d vector and tell them both to read it as a matrix
+        // in fortran order. Therefore these arrays should be the same.
+        assert_eq!(
+            &Robj::try_from(&Array::from_shape_vec((2, 3).f(), vec![1., 2., 3., 4., 5., 6.]).unwrap())?,
+            &R!("matrix(c(1, 2, 3, 4, 5, 6), nrow=2, byrow=FALSE)")?
+        );
+
+        // We give both R and Rust the same 1d vector and tell them both to read it as a 3d array
+        // in fortran order. Therefore these arrays should be the same.
+        assert_eq!(
+            &Robj::try_from(&Array::from_shape_vec((1, 2, 3).f(), vec![1, 2, 3, 4, 5, 6]).unwrap())?,
+            &R!("array(1:6, c(1, 2, 3))")?
+        );
+
+
+        // We give R a 1d vector and tell it to read it as a 3d vector
+        // Then we give Rust the equivalent vector manually split out.
+        assert_eq!(
+            &Robj::try_from(&array![[[1, 5], [3, 7]], [[2, 6], [4, 8]]])?,
+            &R!("array(1:8, dim=c(2, 2, 2))")?
+        );
+    }
+}
+
+#[test]
+fn test_round_trip() {
+    test! {
+        let rvals = [
+            R!("matrix(c(1L, 2L, 3L, 4L, 5L, 6L), nrow=2)"),
+            R!("array(1:8, c(4, 2))")
+        ];
+        for rval in rvals {
+            let rval = rval.unwrap();
+            let rust_arr= <ArrayView2<i32>>::from_robj(&rval).unwrap();
+            let r_arr: Robj = (&rust_arr).try_into().unwrap();
+            assert_eq!(
+                rval,
+                r_arr
+            );
+        }
+    }
+}
diff --git a/extendr-api/src/scalar/mod.rs b/extendr-api/src/scalar/mod.rs
new file mode 100644
index 000000000..ba5905479
--- /dev/null
+++ b/extendr-api/src/scalar/mod.rs
@@ -0,0 +1,3 @@
+mod rint;
+
+pub use rint::Rint;
diff --git a/extendr-api/src/scalar/rint.rs b/extendr-api/src/scalar/rint.rs
new file mode 100644
index 000000000..e538fcf26
--- /dev/null
+++ b/extendr-api/src/scalar/rint.rs
@@ -0,0 +1,284 @@
+use crate::*;
+use std::convert::TryFrom;
+use std::ops::{Add, Div, Mul, Neg, Not, Sub};
+
+/// Rint is a wrapper for i32 in the context of an R's integer vector.
+///
+/// Rint can have a value between i32::MIN+1 and i32::MAX
+///
+/// The value i32::MIN is used as "NA".
+///
+/// Rint has the same footprint as an i32 value allowing us to use it in zero copy slices.
+#[derive(PartialEq, Eq)]
+pub struct Rint(pub i32);
+
+impl Rint {
+    /// Construct a NA Rint.
+    pub fn na() -> Self {
+        Rint(i32::MIN)
+    }
+
+    /// Get an integer or i32::MIN for NA.
+    pub fn inner(&self) -> i32 {
+        self.0
+    }
+}
+
+impl Clone for Rint {
+    fn clone(&self) -> Self {
+        Self(self.0)
+    }
+}
+
+impl Copy for Rint {}
+
+impl IsNA for Rint {
+    /// Return true is the is a NA value.
+    fn is_na(&self) -> bool {
+        self.0 == std::i32::MIN
+    }
+}
+
+impl From<i32> for Rint {
+    /// Construct a Rint from an integer.
+    /// i32::MIN gives an NA.
+    fn from(v: i32) -> Self {
+        Self(v)
+    }
+}
+
+impl From<Option<i32>> for Rint {
+    /// Construct an Rint from an optional integer.
+    /// None or Some(i32::MIN) gives an NA.
+    fn from(v: Option<i32>) -> Self {
+        if let Some(v) = v {
+            v.into()
+        } else {
+            Rint::na()
+        }
+    }
+}
+
+impl From<Rint> for Option<i32> {
+    /// Convert an Rint to an optional integer.
+    /// NA gives None.
+    fn from(v: Rint) -> Self {
+        if v.is_na() {
+            None
+        } else {
+            Some(v.0)
+        }
+    }
+}
+
+impl std::fmt::Debug for Rint {
+    /// Debug format an Rint.
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let z: Option<i32> = (*self).into();
+        if let Some(val) = z {
+            write!(f, "{}", val)
+        } else {
+            write!(f, "na")
+        }
+    }
+}
+
+macro_rules! gen_binop {
+    ($opname1 : ident, $opname2: ident, $expr: expr, $docstring: expr) => {
+        impl $opname1<Rint> for Rint {
+            type Output = Rint;
+
+            #[doc = $docstring]
+            fn $opname2(self, rhs: Rint) -> Self::Output {
+                if let Some(lhs) = self.clone().into() {
+                    if let Some(rhs) = rhs.into() {
+                        let f = $expr;
+                        if let Some(res) = f(lhs, rhs) {
+                            // Note that if res = i32::MIN, this will also be NA.
+                            return Rint::from(res);
+                        }
+                    }
+                }
+                Rint::na()
+            }
+        }
+
+        impl $opname1<Rint> for &Rint {
+            type Output = Rint;
+
+            #[doc = $docstring]
+            fn $opname2(self, rhs: Rint) -> Self::Output {
+                if let Some(lhs) = self.clone().into() {
+                    if let Some(rhs) = rhs.into() {
+                        let f = $expr;
+                        if let Some(res) = f(lhs, rhs) {
+                            // Note that if res = i32::MIN, this will also be NA.
+                            return Rint::from(res);
+                        }
+                    }
+                }
+                Rint::na()
+            }
+        }
+
+        impl $opname1<i32> for Rint {
+            type Output = Rint;
+
+            #[doc = $docstring]
+            fn $opname2(self, rhs: i32) -> Self::Output {
+                if let Some(lhs) = self.clone().into() {
+                    let f = $expr;
+                    if let Some(res) = f(lhs, rhs) {
+                        // Note that if res = i32::MIN, this will also be NA.
+                        return Rint::from(res);
+                    }
+                }
+                Rint::na()
+            }
+        }
+    };
+}
+
+macro_rules! gen_unnop {
+    ($opname1 : ident, $opname2: ident, $expr: expr, $docstring: expr) => {
+        impl $opname1 for Rint {
+            type Output = Rint;
+
+            #[doc = $docstring]
+            fn $opname2(self) -> Self::Output {
+                if let Some(lhs) = self.into() {
+                    let f = $expr;
+                    if let Some(res) = f(lhs) {
+                        // Note that if res = i32::MIN, this will also be NA.
+                        return Rint::from(res);
+                    }
+                }
+                Rint::na()
+            }
+        }
+
+        impl $opname1 for &Rint {
+            type Output = Rint;
+
+            #[doc = $docstring]
+            fn $opname2(self) -> Self::Output {
+                if let Some(lhs) = (*self).into() {
+                    let f = $expr;
+                    if let Some(res) = f(lhs) {
+                        // Note that if res = i32::MIN, this will also be NA.
+                        return Rint::from(res);
+                    }
+                }
+                Rint::na()
+            }
+        }
+    };
+}
+
+// Generate binary ops for +, -, * and /
+gen_binop!(
+    Add,
+    add,
+    |lhs: i32, rhs| lhs.checked_add(rhs),
+    "Add two Rint values or an option of i32, overflows to NA."
+);
+gen_binop!(
+    Sub,
+    sub,
+    |lhs: i32, rhs| lhs.checked_sub(rhs),
+    "Subtract two Rint values or an option of i32, overflows to NA."
+);
+gen_binop!(
+    Mul,
+    mul,
+    |lhs: i32, rhs| lhs.checked_mul(rhs),
+    "Multiply two Rint values or an option of i32, overflows to NA."
+);
+gen_binop!(
+    Div,
+    div,
+    |lhs: i32, rhs| lhs.checked_div(rhs),
+    "Divide two Rint values or an option of i32, overflows to NA."
+);
+
+// Generate unary ops for -, !
+gen_unnop!(
+    Neg,
+    neg,
+    |lhs: i32| Some(-lhs),
+    "Negate a Rint value, overflows to NA."
+);
+gen_unnop!(
+    Not,
+    not,
+    |lhs: i32| Some(!lhs),
+    "Logical not a Rint value, overflows to NA."
+);
+
+impl std::iter::Sum for Rint {
+    /// Sum an integer iterator over Rint.
+    /// Yields NA on overflow of NAs present.
+    fn sum<I: Iterator<Item = Rint>>(iter: I) -> Rint {
+        iter.fold(Rint::from(0), |a, b| a + b)
+    }
+}
+
+impl PartialEq<i32> for Rint {
+    /// Compare a Rint with an integer. NA always fails.
+    fn eq(&self, other: &i32) -> bool {
+        !self.is_na() && self.0 == *other
+    }
+}
+
+impl TryFrom<Robj> for Rint {
+    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(Rint::na());
+        }
+
+        // If the conversion is int-to-int, check the limits. This
+        // needs to be done by `TryFrom` because the conversion by `as`
+        // is problematic when converting a negative value to unsigned
+        // integer types (e.g. `-1i32 as u8` becomes 255).
+        if let Some(v) = robj.as_integer() {
+            if let Ok(v) = Self::try_from(v) {
+                return Ok(v);
+            } else {
+                return Err(Error::OutOfLimits(robj));
+            }
+        }
+
+        // If the conversion is float-to-int, check if the value is
+        // integer-like (i.e., an integer, or a float representing a
+        // whole number). This needs to be down with `as`, as no
+        // `TryFrom` is implemented for float types. `FloatToInt` trait
+        // might eventually become available in future, though.
+        if let Some(v) = robj.as_real() {
+            let result = v as i32;
+            if (result as f64 - v).abs() < f64::EPSILON {
+                return Ok(Rint::from(result));
+            } else {
+                return Err(Error::ExpectedWholeNumber(robj));
+            }
+        }
+
+        Err(Error::ExpectedNumeric(robj))
+    }
+}
+
+impl From<Rint> for Robj {
+    /// Convert am Rint into an robj.
+    fn from(value: Rint) -> Self {
+        Robj::from(value.0)
+    }
+}
diff --git a/extendr-api/src/wrapper/altrep.rs b/extendr-api/src/wrapper/altrep.rs
index c52dcdff8..6e7afb196 100644
--- a/extendr-api/src/wrapper/altrep.rs
+++ b/extendr-api/src/wrapper/altrep.rs
@@ -474,6 +474,11 @@ impl Altrep {
         })
     }
 
+    /// Return true if the ALTREP object has been manifested (copied into memory).
+    pub fn is_manifest(&self) -> bool {
+        unsafe { !DATAPTR_OR_NULL(self.get()).is_null() }
+    }
+
     #[allow(dead_code)]
     pub(crate) fn get_state<StateType>(x: SEXP) -> &'static StateType {
         unsafe {
@@ -973,4 +978,44 @@ impl Altrep {
             class
         })
     }
+
+    pub fn make_altinteger_from_iterator<Iter>(iter: Iter) -> Altrep
+    where
+        Iter: ExactSizeIterator + std::fmt::Debug + Clone + 'static + std::any::Any,
+        Iter::Item: Into<i32>,
+    {
+        impl<Iter: ExactSizeIterator + std::fmt::Debug + Clone> AltrepImpl for Iter {
+            fn length(&self) -> usize {
+                self.len()
+            }
+        }
+
+        impl<Iter: ExactSizeIterator + std::fmt::Debug + Clone> AltIntegerImpl for Iter
+        where
+            Iter::Item: Into<i32>,
+        {
+            fn elt(&self, index: usize) -> i32 {
+                self.clone().nth(index).unwrap().into()
+            }
+
+            fn get_region(&self, index: usize, data: &mut [i32]) -> usize {
+                let len = self.len();
+                if index > len {
+                    0
+                } else {
+                    let mut iter = self.clone().skip(index);
+                    let num_elems = data.len().min(len - index);
+                    let dest = &mut data[0..num_elems];
+                    for d in dest.iter_mut() {
+                        *d = iter.next().unwrap().into();
+                    }
+                    num_elems
+                }
+            }
+        }
+
+        let class = Altrep::make_altinteger_class::<Iter>(std::any::type_name::<Iter>(), "extendr");
+        let robj: Robj = Altrep::from_state_and_class(iter, class, false).into();
+        Altrep { robj }
+    }
 }
diff --git a/extendr-api/src/wrapper/integers.rs b/extendr-api/src/wrapper/integers.rs
new file mode 100644
index 000000000..e23ab5f00
--- /dev/null
+++ b/extendr-api/src/wrapper/integers.rs
@@ -0,0 +1,163 @@
+use super::scalar::Rint;
+use super::*;
+use std::iter::FromIterator;
+
+#[derive(Debug, PartialEq, Clone)]
+pub struct Integers {
+    pub(crate) robj: Robj,
+}
+
+impl Default for Integers {
+    fn default() -> Self {
+        Integers::new(0)
+    }
+}
+
+// Under this size, vectors are manifest.
+// Above this size, vectors are lazy ALTREP objects.
+const SHORT_VECTOR_LENGTH: usize = 64 * 1024;
+
+impl Integers {
+    /// Create a new vector of integers.
+    /// ```
+    /// use extendr_api::prelude::*;
+    /// test! {
+    ///     let vec = Integers::new(10);
+    ///     assert_eq!(vec.is_integer(), true);
+    ///     assert_eq!(vec.len(), 10);
+    /// }
+    /// ```
+    pub fn new(len: usize) -> Integers {
+        let iter = (0..len).map(|_| 0);
+        Integers::from_values(iter)
+    }
+
+    /// Wrapper for creating ALTREP integer (INTSXP) vectors from iterators.
+    /// The iterator must be exact, clonable and implement Debug.
+    ///
+    /// If you want a more generalised constructor, use `iter.collect::<Integers>()`.
+    ///
+    /// ```
+    /// use extendr_api::prelude::*;
+    /// test! {
+    ///     // Short (<64k) vectors are allocated.
+    ///     let vec = Integers::from_values((0..3).map(|i| 2-i));
+    ///     assert_eq!(vec.is_altrep(), false);
+    ///     assert_eq!(r!(vec.clone()), r!([2, 1, 0]));
+    ///     assert_eq!(vec.elt(1), 1);
+    ///     let mut dest = [0; 2];
+    ///     vec.get_region(1, &mut dest);
+    ///     assert_eq!(dest, [1, 0]);
+    ///
+    ///     // Long (>=64k) vectors are lazy ALTREP objects.
+    ///     let vec = Integers::from_values(0..1000000000);
+    ///     assert_eq!(vec.is_altrep(), true);
+    ///     assert_eq!(vec.elt(12345678), 12345678);
+    ///     let mut dest = [0; 2];
+    ///     vec.get_region(12345678, &mut dest);
+    ///     assert_eq!(dest, [12345678, 12345679]);
+    /// }
+    /// ```
+    pub fn from_values<V>(values: V) -> Self
+    where
+        V: IntoIterator,
+        V::IntoIter: ExactSizeIterator + std::fmt::Debug + Clone + 'static + std::any::Any,
+        V::Item: Into<i32>,
+    {
+        single_threaded(|| {
+            let values: V::IntoIter = values.into_iter();
+
+            let robj = if values.len() >= SHORT_VECTOR_LENGTH {
+                Altrep::make_altinteger_from_iterator(values)
+                    .try_into()
+                    .unwrap()
+            } else {
+                let mut robj = Robj::alloc_vector(INTSXP, values.len());
+                let dest: &mut [i32] = robj.as_typed_slice_mut().unwrap();
+
+                for (d, v) in dest.iter_mut().zip(values) {
+                    *d = v.into();
+                }
+                robj
+            };
+            Self { robj }
+        })
+    }
+
+    /// Get a single element from the vector.
+    /// Note that this is very inefficient in a tight loop.
+    pub fn elt(&self, index: usize) -> Rint {
+        unsafe { INTEGER_ELT(self.get(), index as R_xlen_t).into() }
+    }
+
+    /// Get a region of elements from the vector.
+    pub fn get_region(&self, index: usize, dest: &mut [i32]) {
+        unsafe {
+            let ptr = dest.as_mut_ptr();
+            INTEGER_GET_REGION(self.get(), index as R_xlen_t, dest.len() as R_xlen_t, ptr);
+        }
+    }
+
+    /// Return TRUE if the vector is sorted, FALSE if not, or NA_BOOL if unknown.
+    pub fn is_sorted(&self) -> Bool {
+        unsafe { INTEGER_IS_SORTED(self.get()).into() }
+    }
+
+    /// Return TRUE if the vector has NAs, FALSE if not, or NA_BOOL if unknown.
+    pub fn no_na(&self) -> Bool {
+        unsafe { INTEGER_NO_NA(self.get()).into() }
+    }
+
+    /// Return an iterator for for an integer object.
+    /// Forces ALTREP objects to manifest.
+    /// ```
+    /// use extendr_api::prelude::*;
+    /// test! {
+    ///     let vec = Integers::from_values(0..3);
+    ///     assert_eq!(vec.iter().sum::<Rint>(), 3);
+    /// }
+    /// ```
+    pub fn iter(&self) -> impl Iterator<Item = Rint> {
+        self.as_typed_slice().unwrap().iter().cloned()
+    }
+
+    /// Return a writable iterator for an integer object.
+    /// Forces ALTREP objects to manifest.
+    /// ```
+    /// use extendr_api::prelude::*;
+    /// test! {
+    ///     let mut vec = Integers::from_values(0..3);
+    ///     vec.iter_mut().for_each(|v| *v = *v + 1);
+    ///     assert_eq!(vec, Integers::from_values(1..4));
+    /// }
+    /// ```
+    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Rint> {
+        self.as_typed_slice_mut().unwrap().iter_mut()
+    }
+}
+
+impl FromIterator<Rint> for Integers {
+    /// A more generalised iterator collector for small vectors.
+    /// Generates a non-ALTREP vector.
+    /// ```
+    /// use extendr_api::prelude::*;
+    /// test! {
+    ///     let vec : Integers = (0..3).map(|i| i.into()).collect();
+    ///     assert_eq!(vec, Integers::from_values([0, 1, 2]));
+    /// }
+    /// ```
+    fn from_iter<T: IntoIterator<Item = Rint>>(iter: T) -> Self {
+        // Collect into a vector first.
+        // TODO: specialise for ExactSizeIterator.
+        let values: Vec<Rint> = iter.into_iter().collect();
+
+        let mut robj = Robj::alloc_vector(INTSXP, values.len());
+        let dest: &mut [Rint] = robj.as_typed_slice_mut().unwrap();
+
+        for (d, v) in dest.iter_mut().zip(values) {
+            *d = v;
+        }
+
+        Integers { robj }
+    }
+}
diff --git a/extendr-api/src/wrapper/mod.rs b/extendr-api/src/wrapper/mod.rs
index fdfecbe65..fc729ca54 100644
--- a/extendr-api/src/wrapper/mod.rs
+++ b/extendr-api/src/wrapper/mod.rs
@@ -9,6 +9,7 @@ pub mod char;
 pub mod environment;
 pub mod expr;
 pub mod function;
+pub mod integers;
 pub mod lang;
 pub mod list;
 pub mod matrix;
@@ -28,6 +29,7 @@ pub use altrep::{
 pub use environment::{EnvIter, Environment};
 pub use expr::Expression;
 pub use function::Function;
+pub use integers::Integers;
 pub use lang::Language;
 pub use list::{FromList, List, ListIter};
 pub use matrix::{RArray, RColumn, RMatrix, RMatrix3D};
@@ -103,6 +105,13 @@ macro_rules! make_conversions {
                 &self.robj
             }
         }
+
+        impl DerefMut for $typename {
+            /// Make a wrapper behave like a writable Robj.
+            fn deref_mut(&mut self) -> &mut Self::Target {
+                &mut self.robj
+            }
+        }
     };
 }
 
@@ -157,6 +166,8 @@ make_conversions!(Altrep, ExpectedAltrep, is_altrep, "Not an Altrep type");
 
 make_conversions!(S4, ExpectedS4, is_s4, "Not a S4 type");
 
+make_conversions!(Integers, ExpectedInteger, is_integer, "Not an integer type");
+
 impl Robj {
     /// Convert a symbol object to a Symbol wrapper.
     /// ```


Test patch:
diff --git a/extendr-api/tests/extendr_macro.rs b/extendr-api/tests/extendr_macro.rs
index e40cf5cca..5ad4deaec 100644
--- a/extendr-api/tests/extendr_macro.rs
+++ b/extendr-api/tests/extendr_macro.rs
@@ -37,6 +37,26 @@ fn test_option_i16(val: Option<i16>) -> i16 {
     }
 }
 
+#[extendr(use_try_from = true)]
+fn test_rint(val: Rint) -> Rint {
+    val
+}
+
+#[extendr(use_try_from = true)]
+fn test_integers(val: Integers) -> Integers {
+    val
+}
+
+#[extendr(use_try_from = true)]
+fn test_integers2(val: Integers) -> Integers {
+    val.iter().map(|i| i + 1).collect()
+}
+
+#[extendr(use_try_from = true)]
+fn test_integers3(val: Integers) -> Rint {
+    val.iter().sum()
+}
+
 #[test]
 fn tests_with_successful_outcomes() {
     unsafe {
@@ -85,6 +105,16 @@ fn tests_with_successful_outcomes() {
 
             // NA input.
             assert_eq!(new_owned(wrap__test_option_f64(r!(NA_INTEGER).get())), r!(-1.0));
+
+            // Rint.
+            assert_eq!(new_owned(wrap__test_rint(r!(1).get())), r!(1));
+            assert_eq!(new_owned(wrap__test_rint(r!(1.0).get())), r!(1));
+            assert_eq!(new_owned(wrap__test_rint(r!(NA_INTEGER).get())), r!(NA_INTEGER));
+
+            // Integers
+            assert_eq!(new_owned(wrap__test_integers(r!([1, 2]).get())), r!([1, 2]));
+            assert_eq!(new_owned(wrap__test_integers2(r!([1, 2]).get())), r!([2, 3]));
+            assert_eq!(new_owned(wrap__test_integers3(r!(0..4).get())), r!(6));
         }
     }
 }
@@ -108,8 +138,10 @@ fn tests_with_unsuccessful_outcomes() {
             assert!(catch_r_error(|| wrap__test_i32(r!(pairlist!(x=1)).get())).is_err());
             assert!(catch_r_error(|| wrap__test_i32(r!(list!(1, 2, 3)).get())).is_err());
 
-            // TODO: check for overflow.
-            // assert!(catch_r_error(|| wrap__test_i16(r!(1234567890).get())).is_err());
+            assert!(catch_r_error(|| wrap__test_rint(r!([1, 2]).get())).is_err());
+            assert!(catch_r_error(|| wrap__test_integers(r!([1.0, 2.0]).get())).is_err());
+
+            assert!(catch_r_error(|| wrap__test_i16(r!(1234567890).get())).is_err());
             std::panic::set_hook(old_hook);
         }
     });
diff --git a/extendr-api/tests/scalar_tests.rs b/extendr-api/tests/scalar_tests.rs
new file mode 100644
index 000000000..708b38d0f
--- /dev/null
+++ b/extendr-api/tests/scalar_tests.rs
@@ -0,0 +1,50 @@
+use extendr_api::prelude::*;
+
+#[test]
+fn test_rint() {
+    let a = Rint::from(20);
+    let b = Rint::from(10);
+    assert_eq!(a + b, Rint::from(30));
+    assert_eq!(a - b, Rint::from(10));
+    assert_eq!(a * b, Rint::from(200));
+    assert_eq!(a / b, Rint::from(2));
+    assert_eq!(-a, Rint::from(-20));
+    assert_eq!(!a, Rint::from(-21));
+
+    assert_eq!(&a + b, Rint::from(30));
+    assert_eq!(&a - b, Rint::from(10));
+    assert_eq!(&a * b, Rint::from(200));
+    assert_eq!(&a / b, Rint::from(2));
+    assert_eq!(-&a, Rint::from(-20));
+    assert_eq!(!&a, Rint::from(-21));
+
+    // NA lhs
+    let a = Rint::na();
+    let b = Rint::from(10);
+    assert_eq!(a + b, Rint::na());
+    assert_eq!(a - b, Rint::na());
+    assert_eq!(a * b, Rint::na());
+    assert_eq!(a / b, Rint::na());
+    assert_eq!(-a, Rint::na());
+    assert_eq!(!a, Rint::na());
+
+    // NA rhs
+    let a = Rint::from(10);
+    let b = Rint::na();
+    assert_eq!(a + b, Rint::na());
+    assert_eq!(a - b, Rint::na());
+    assert_eq!(a * b, Rint::na());
+    assert_eq!(a / b, Rint::na());
+
+    // Overflow
+    let a = Rint::from(i32::MAX - 1);
+    let b = Rint::from(10);
+    assert_eq!(a * b, Rint::na());
+    assert_eq!(Rint::from(1) / Rint::from(0), Rint::na());
+    assert_eq!(Rint::from(-1) / Rint::na(), Rint::na());
+
+    // Underflow
+    let a = Rint::from(i32::MIN + 1);
+    let b = Rint::from(-10);
+    assert_eq!(a + b, Rint::na());
+}