Repository: extendr/extendr
Pull request #469 (2023-02-03 23:36:31)

Fix `extendr_module` importing
The issue is that these `get_<module>_metadata` are generated in their respective modules, but then they aren't realy imported in the place where they get recursively called.
Others experienced this, see [discord message](https://discord.com/channels/751419551132155925/751419551132155928/1067806341491609772)



CGMossa: I accidentally made a branch from a branch where I just was half done doing something.
Please accept the minor documentation edits. I was going to make a full PR of these things.
multimeric: Can you write a test for this?
CGMossa: I've no idea how to... 
multimeric: Maybe an [integration test](https://doc.rust-lang.org/rust-by-example/testing/integration_testing.html) that has multiple Rust modules in the same file, each with their own `extendr_module`? 

Patch:
diff --git a/.gitignore b/.gitignore
index 25c7b19ed..32be9e25d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,3 +33,4 @@
 extendr.Rproj
 tests/extendrtests.Rcheck/
 tests/extendrtests_0.1.0.tar.gz
+**/.cargo/*
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d9bf16f42..c29161a4e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,8 @@
 
 ### Added
 
-- `Sum` for scalars like `Rint`, `Rfloat` and `Rcplx`, which accept `Iterator<Item = &Rtype>` [[#454]](https://github.com/extendr/extendr/pull/454) 
+- [**ndarray**] `TryFrom<&Robj>` for `ArrayView1<T>` and `ArrayView2<T>`, where `T` is `i32`, `f64`, `c64`, `Rint`, `Rfloat`, `Rcplx`, `Rstr`, `Rbool` [[#443]](https://github.com/extendr/extendr/pull/443)
+- `Sum` for scalars like `Rint`, `Rfloat` and `Rcplx`, which accept `Iterator<Item = &Rtype>` [[#454]](https://github.com/extendr/extendr/pull/454)
 - `Nullable<T>` is now part of `extendr_api::prelude` [[#446]](https://github.com/extendr/extendr/pull/446)
 - `TryFrom<Robj>`, `From<Option<T>>`, `Into<Option<T>>` and their variations for `Nullable<T>` [[#446]](https://github.com/extendr/extendr/pull/446)
 - `Nullable<T>::map()` that acts on not null value and propagates `NULL` [[#446]](https://github.com/extendr/extendr/pull/446)
diff --git a/extendr-api/Cargo.toml b/extendr-api/Cargo.toml
index d9a11f87c..7353b092b 100644
--- a/extendr-api/Cargo.toml
+++ b/extendr-api/Cargo.toml
@@ -7,7 +7,7 @@ authors = [
     "Mossa Merhi Reimert <mossa@sund.ku.dk>",
     "Claus O. Wilke <wilke@austin.utexas.edu>",
     "Hiroaki Yutani",
-    "Ilia A. Kosenkov <ilia.kosenkov@outlook.com>"
+    "Ilia A. Kosenkov <ilia.kosenkov@outlook.com>",
 ]
 edition = "2021"
 description = "Safe and user friendly bindings to the R programming language."
@@ -16,8 +16,8 @@ repository = "https://github.com/extendr/extendr"
 
 [dependencies]
 libR-sys = "0.3.0"
-extendr-macros = { path = "../extendr-macros", version="0.3.1" }
-extendr-engine = { path = "../extendr-engine", version="0.3.1" }
+extendr-macros = { path = "../extendr-macros", version = "0.3.1" }
+extendr-engine = { path = "../extendr-engine", version = "0.3.1" }
 ndarray = { version = "0.15.3", optional = true }
 lazy_static = "1.4"
 paste = "1.0.5"
@@ -25,6 +25,9 @@ serde = { version = "1.0", features = ["derive"], optional = true }
 num-complex = { version = "0.4", optional = true }
 libc = { version = "0.2", optional = true }
 
+[dev-dependencies]
+rstest = "0.16.0"
+
 [features]
 default = []
 
diff --git a/extendr-api/src/error.rs b/extendr-api/src/error.rs
index f464c951b..e76345eda 100644
--- a/extendr-api/src/error.rs
+++ b/extendr-api/src/error.rs
@@ -81,6 +81,9 @@ pub enum Error {
 
     ExpectedExternalPtrType(Robj, String),
     Other(String),
+
+    #[cfg(feature = "ndarray")]
+    NDArrayShapeError(ndarray::ShapeError),
 }
 
 impl std::fmt::Display for Error {
@@ -159,6 +162,11 @@ impl std::fmt::Display for Error {
             }
             Error::NoGraphicsDevices(_robj) => write!(f, "No graphics devices active."),
             Error::Other(str) => write!(f, "{}", str),
+
+            #[cfg(feature = "ndarray")]
+            Error::NDArrayShapeError(shape_error) => {
+                write!(f, "NDArray failed with error: {}.", shape_error)
+            }
         }
     }
 }
diff --git a/extendr-api/src/robj/into_robj.rs b/extendr-api/src/robj/into_robj.rs
index a0a337caf..fda3b24de 100644
--- a/extendr-api/src/robj/into_robj.rs
+++ b/extendr-api/src/robj/into_robj.rs
@@ -6,7 +6,13 @@ pub(crate) fn str_to_character(s: &str) -> SEXP {
         if s.is_na() {
             R_NaString
         } else {
-            single_threaded(|| Rf_mkCharLen(s.as_ptr() as *const raw::c_char, s.len() as i32))
+            single_threaded(|| {
+                Rf_mkCharLenCE(
+                    s.as_ptr() as *const raw::c_char,
+                    s.len() as i32,
+                    cetype_t_CE_UTF8,
+                )
+            })
         }
     }
 }
diff --git a/extendr-api/src/robj/mod.rs b/extendr-api/src/robj/mod.rs
index 37ec7cbab..ca8dc0c0f 100644
--- a/extendr-api/src/robj/mod.rs
+++ b/extendr-api/src/robj/mod.rs
@@ -1,11 +1,11 @@
 //! R object handling.
 //!
-//! See. <https://cran.r-project.org/doc/manuals/R-exts.html>
+//! See. [Writing R Extensions](https://cran.r-project.org/doc/manuals/R-exts.html)
 //!
 //! Fundamental principals:
 //!
 //! * Any function that can break the protection mechanism is unsafe.
-//! * Users should be able to do almost everything without using libR_sys.
+//! * Users should be able to do almost everything without using `libR_sys`.
 //! * The interface should be friendly to R users without Rust experience.
 //!
 
@@ -778,6 +778,7 @@ macro_rules! make_typed_slice {
 
 make_typed_slice!(Rbool, INTEGER, LGLSXP);
 make_typed_slice!(i32, INTEGER, INTSXP);
+make_typed_slice!(u32, INTEGER, INTSXP);
 make_typed_slice!(Rint, INTEGER, INTSXP);
 make_typed_slice!(f64, REAL, REALSXP);
 make_typed_slice!(Rfloat, REAL, REALSXP);
diff --git a/extendr-api/src/robj/try_from_robj.rs b/extendr-api/src/robj/try_from_robj.rs
index 7b4712231..10cb4986e 100644
--- a/extendr-api/src/robj/try_from_robj.rs
+++ b/extendr-api/src/robj/try_from_robj.rs
@@ -384,7 +384,7 @@ impl TryFrom<&Robj> for Rcplx {
     }
 }
 
-// Convert TryFrom<&Robj> into TryFrom<&Robj>. Sadly, we are unable to make a blanket
+// Convert TryFrom<&Robj> into TryFrom<Robj>. Sadly, we are unable to make a blanket
 // conversion using GetSexp with the current version of Rust.
 macro_rules! impl_try_from_robj {
     ($($type : ty)*) => {
diff --git a/extendr-api/src/robj_ndarray.rs b/extendr-api/src/robj_ndarray.rs
index c0d244c55..8ea4aedcb 100644
--- a/extendr-api/src/robj_ndarray.rs
+++ b/extendr-api/src/robj_ndarray.rs
@@ -2,7 +2,7 @@
 use ndarray::prelude::*;
 use ndarray::{Data, ShapeBuilder};
 
-use crate::prelude::dim_symbol;
+use crate::prelude::{c64, dim_symbol, Rcplx, Rfloat, Rint};
 use crate::*;
 
 impl<'a, T> FromRobj<'a> for ArrayView1<'a, T>
@@ -19,44 +19,92 @@ where
     }
 }
 
+macro_rules! make_array_view_1 {
+    ($type: ty, $error_fn: expr) => {
+        impl<'a> TryFrom<&'a Robj> for ArrayView1<'a, $type> {
+            type Error = crate::Error;
+
+            fn try_from(robj: &Robj) -> Result<Self> {
+                if let Some(v) = robj.as_typed_slice() {
+                    Ok(ArrayView1::<'a, $type>::from(v))
+                } else {
+                    Err($error_fn(robj.clone()))
+                }
+            }
+        }
+    };
+
+    ($type: ty, $error_fn: expr) => {
+        impl<'a> TryFrom<Robj> for ArrayView1<'a, $type> {
+            type Error = crate::Error;
+
+            fn try_from(robj: Robj) -> Result<Self> {
+                Self::try_from(&robj)
+            }
+        }
+    };
+}
+
 macro_rules! make_array_view_2 {
-    ($type: ty, $error_str: expr) => {
+    ($type: ty, $error_str: expr, $error_fn: expr) => {
         impl<'a> FromRobj<'a> for ArrayView2<'a, $type> {
             /// Convert an R object to a `ndarray` ArrayView2.
             fn from_robj(robj: &'a Robj) -> std::result::Result<Self, &'static str> {
+                <ArrayView2<'a, $type>>::try_from(robj).map_err(|_| $error_str)
+            }
+        }
+
+        impl<'a> TryFrom<&'_ Robj> for ArrayView2<'a, $type> {
+            type Error = crate::Error;
+            fn try_from(robj: &Robj) -> Result<Self> {
                 if robj.is_matrix() {
                     let nrows = robj.nrows();
                     let ncols = robj.ncols();
                     if let Some(v) = robj.as_typed_slice() {
                         // use fortran order.
                         let shape = (nrows, ncols).into_shape().f();
-                        if let Ok(res) = ArrayView2::from_shape(shape, v) {
-                            return Ok(res);
-                        }
+                        return ArrayView2::from_shape(shape, v)
+                            .map_err(|err| Error::NDArrayShapeError(err));
+                    } else {
+                        return Err($error_fn(robj.clone()));
                     }
                 }
-                return Err($error_str);
+                return Err(Error::ExpectedMatrix(robj.clone()));
+            }
+        }
+
+        impl<'a> TryFrom<Robj> for ArrayView2<'a, $type> {
+            type Error = crate::Error;
+            fn try_from(robj: Robj) -> Result<Self> {
+                Self::try_from(&robj)
             }
         }
     };
 }
+make_array_view_1!(Rbool, Error::ExpectedLogical);
+make_array_view_1!(Rint, Error::ExpectedInteger);
+make_array_view_1!(i32, Error::ExpectedInteger);
+make_array_view_1!(u32, Error::ExpectedInteger);
+make_array_view_1!(Rfloat, Error::ExpectedReal);
+make_array_view_1!(f64, Error::ExpectedReal);
+make_array_view_1!(Rcplx, Error::ExpectedComplex);
+make_array_view_1!(c64, Error::ExpectedComplex);
+make_array_view_1!(Rstr, Error::ExpectedString);
+
+make_array_view_2!(Rbool, "Not a logical matrix.", Error::ExpectedLogical);
+make_array_view_2!(Rint, "Not an integer matrix.", Error::ExpectedInteger);
+make_array_view_2!(i32, "Not an integer matrix.", Error::ExpectedInteger);
+make_array_view_2!(u32, "Not an integer matrix.", Error::ExpectedInteger);
+make_array_view_2!(Rfloat, "Not a floating point matrix.", Error::ExpectedReal);
+make_array_view_2!(f64, "Not a floating point matrix.", Error::ExpectedReal);
+make_array_view_2!(
+    Rcplx,
+    "Not a complex number matrix.",
+    Error::ExpectedComplex
+);
+make_array_view_2!(c64, "Not a complex number matrix.", Error::ExpectedComplex);
+make_array_view_2!(Rstr, "Not a string matrix.", Error::ExpectedString);
 
-make_array_view_2!(Rbool, "Not a logical matrix.");
-make_array_view_2!(i32, "Not an integer matrix.");
-make_array_view_2!(f64, "Not a floating point matrix.");
-//make_array_view_2!(u8, "Not a raw matrix.");
-
-// impl<'a, T> From<ArrayView2<'a, T>> for Robj
-// where
-//     T : ToVectorValue
-// {
-//     fn from(array: ArrayView2<T>) -> Self {
-//         let dims = array.dim();
-//         let slice : &[T] = array.as_slice().unwrap();
-//         let mx = Matrix::new(slice, dims.0, dims.1);
-//         r!(mx)
-//     }
-// }
 impl<A, S, D> TryFrom<&ArrayBase<S, D>> for Robj
 where
     S: Data<Elem = A>,
@@ -94,138 +142,127 @@ where
     }
 }
 
-#[test]
-fn test_from_robj() {
-    test! {
-        assert_eq!(
-            <ArrayView1<f64>>::from_robj(&Robj::from(1.)),
-            Ok(ArrayView1::<f64>::from(&[1.][..]))
-        );
-        assert_eq!(
-            <ArrayView1<i32>>::from_robj(&Robj::from(1)),
-            Ok(ArrayView1::<i32>::from(&[1][..]))
-        );
-        assert_eq!(
-            <ArrayView1<Rbool>>::from_robj(&Robj::from(true)),
-            Ok(ArrayView1::<Rbool>::from(&[TRUE][..]))
-        );
-
-        let robj = R!("matrix(c(1, 2, 3, 4, 5, 6, 7, 8), ncol=2, nrow=4)")?;
-        let mx = <ArrayView2<f64>>::from_robj(&robj)?;
-        assert_eq!(mx[[0, 0]], 1.0);
-        assert_eq!(mx[[1, 0]], 2.0);
-        assert_eq!(mx[[2, 0]], 3.0);
-        assert_eq!(mx[[3, 0]], 4.0);
-        assert_eq!(mx[[0, 1]], 5.0);
-        assert_eq!(mx[[1, 1]], 6.0);
-        assert_eq!(mx[[2, 1]], 7.0);
-        assert_eq!(mx[[3, 1]], 8.0);
-
-        // check basic logic of fortran-order matrices.
-        let col0 = mx.column(0);
-        assert_eq!(col0[0], 1.0);
-        assert_eq!(col0[1], 2.0);
-        assert_eq!(col0[2], 3.0);
-        assert_eq!(col0[3], 4.0);
-
-        // check integer matrices
-        let robj = R!("matrix(c(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L), ncol=2, nrow=4)")?;
-        let mx = <ArrayView2<i32>>::from_robj(&robj)?;
-        assert_eq!(mx[[0, 0]], 1);
-        assert_eq!(mx[[1, 0]], 2);
-        assert_eq!(mx[[2, 0]], 3);
-        assert_eq!(mx[[3, 0]], 4);
-        assert_eq!(mx[[0, 1]], 5);
-        assert_eq!(mx[[1, 1]], 6);
-        assert_eq!(mx[[2, 1]], 7);
-        assert_eq!(mx[[3, 1]], 8);
-
-        // check logical matrices
-        let robj = R!("matrix(c(T, T, T, T, F, F, F, F), ncol=2, nrow=4)")?;
-        let mx = <ArrayView2<Rbool>>::from_robj(&robj)?;
-        assert_eq!(mx[[0, 0]], TRUE);
-        assert_eq!(mx[[1, 0]], TRUE);
-        assert_eq!(mx[[2, 0]], TRUE);
-        assert_eq!(mx[[3, 0]], TRUE);
-        assert_eq!(mx[[0, 1]], FALSE);
-        assert_eq!(mx[[1, 1]], FALSE);
-        assert_eq!(mx[[2, 1]], FALSE);
-        assert_eq!(mx[[3, 1]], FALSE);
-
-        // check raw matrices
-        // let robj = r!(Matrix::new(vec![1_u8, 2, 3, 4, 5, 6, 7, 8], 4, 2));
-        // let mx = <ArrayView2<u8>>::from_robj(&robj)?;
-        // assert_eq!(mx[[0, 0]], 1);
-        // assert_eq!(mx[[1, 0]], 2);
-        // assert_eq!(mx[[2, 0]], 3);
-        // assert_eq!(mx[[3, 0]], 4);
-        // assert_eq!(mx[[0, 1]], 5);
-        // assert_eq!(mx[[1, 1]], 6);
-        // assert_eq!(mx[[2, 1]], 7);
-        // assert_eq!(mx[[3, 1]], 8);
+#[cfg(test)]
+mod test {
+    use super::*;
+    use crate::FromRobj;
+    use ndarray::array;
+    use rstest::rstest;
+
+    #[rstest]
+    // Scalars
+    #[case(
+        "1.0",
+        ArrayView1::<f64>::from(&[1.][..])
+    )]
+    #[case(
+        "1L",
+        ArrayView1::<i32>::from(&[1][..])
+    )]
+    #[case(
+        "TRUE",
+        ArrayView1::<Rbool>::from(&[TRUE][..])
+    )]
+    // Matrices
+    #[case(
+       "matrix(c(1, 2, 3, 4, 5, 6, 7, 8), ncol=2, nrow=4)",
+        <Array2<f64>>::from_shape_vec((4, 2).f(), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]).unwrap()
+    )]
+    #[case(
+        // Testing the memory layout is Fortran
+        "matrix(c(1, 2, 3, 4, 5, 6, 7, 8), ncol=2, nrow=4)[, 1]",
+        <Array2<f64>>::from_shape_vec((4, 2).f(), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]).unwrap().column(0).to_owned()
+    )]
+    #[case(
+        "matrix(c(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L), ncol=2, nrow=4)",
+        <Array2<i32>>::from_shape_vec((4, 2).f(), vec![1, 2, 3, 4, 5, 6, 7, 8]).unwrap()
+    )]
+    #[case(
+        "matrix(c(T, T, T, T, F, F, F, F), ncol=2, nrow=4)",
+        <Array2<Rbool>>::from_shape_vec((4, 2).f(), vec![true.into(), true.into(), true.into(), true.into(), false.into(), false.into(), false.into(), false.into()]).unwrap()
+    )]
+    fn test_from_robj<DataType, DimType>(
+        #[case] left: &'static str,
+        #[case] right: ArrayBase<DataType, DimType>,
+    ) where
+        DataType: Data,
+        for<'a> ArrayView<'a, <DataType as ndarray::RawData>::Elem, DimType>: FromRobj<'a>,
+        DimType: Dimension,
+        <DataType as ndarray::RawData>::Elem: PartialEq + std::fmt::Debug,
+    {
+        // Tests for the R → Rust conversion
+        test! {
+            let left_robj = eval_string(left).unwrap();
+            let left_array = <ArrayView<DataType::Elem, DimType>>::from_robj(&left_robj).unwrap();
+            assert_eq!( left_array, right );
+        }
     }
-}
 
-#[test]
-fn test_to_robj() {
-    test! {
+    #[rstest]
+    #[case(
         // 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)")?
-        );
-
+        &Array4::<i32>::zeros((0, 1, 2, 3).f()),
+        "array(integer(), c(0, 1, 2, 3))"
+    )]
+    #[case(
+        &array![1., 2., 3.],
+        "array(c(1, 2, 3))"
+    )]
+    #[case(
+        // 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.
+        &Array::from_shape_vec((2, 3), vec![1., 2., 3., 4., 5., 6.]).unwrap(),
+        "matrix(c(1, 2, 3, 4, 5, 6), nrow=2, byrow=TRUE)"
+    )]
+    #[case(
         // 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)")?
-        );
-
+        &Array::from_shape_vec((2, 3).f(), vec![1., 2., 3., 4., 5., 6.]).unwrap(),
+        "matrix(c(1, 2, 3, 4, 5, 6), nrow=2, byrow=FALSE)"
+    )]
+    #[case(
         // 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))")?
-        );
-
-
+        &Array::from_shape_vec((1, 2, 3).f(), vec![1, 2, 3, 4, 5, 6]).unwrap(),
+        "array(1:6, c(1, 2, 3))"
+    )]
+    #[case(
         // 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();
+        &array![[[1, 5], [3, 7]], [[2, 6], [4, 8]]],
+        "array(1:8, dim=c(2, 2, 2))"
+    )]
+    fn test_to_robj<T>(#[case] array: T, #[case] r_expr: &str)
+    where
+        Robj: TryFrom<T>,
+        <Robj as TryFrom<T>>::Error: std::fmt::Debug,
+    {
+        // Tests for the Rust → R conversion, so we therefore perform the
+        // comparison in R
+        test! {
             assert_eq!(
-                rval,
-                r_arr
+                &(Robj::try_from(array).unwrap()),
+                &eval_string(r_expr).unwrap()
             );
         }
     }
+
+    #[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/rfloat.rs b/extendr-api/src/scalar/rfloat.rs
index 9e7b28266..eb11504fc 100644
--- a/extendr-api/src/scalar/rfloat.rs
+++ b/extendr-api/src/scalar/rfloat.rs
@@ -33,6 +33,9 @@ impl Rfloat {
     pub fn abs(&self) -> Rfloat {
         self.0.abs().into()
     }
+    pub fn sqrt(&self) -> Rfloat {
+        self.0.sqrt().into()
+    }
 }
 
 // `NA_real_` is a `NaN` with specific bit representation.
diff --git a/extendr-api/src/wrapper/strings.rs b/extendr-api/src/wrapper/strings.rs
index 6d54c9872..027a0dab3 100644
--- a/extendr-api/src/wrapper/strings.rs
+++ b/extendr-api/src/wrapper/strings.rs
@@ -52,7 +52,6 @@ impl Strings {
             for (i, v) in values.into_iter().take(maxlen).enumerate() {
                 let v = v.as_ref();
                 let ch = str_to_character(v);
-                // let ch = Rf_mkCharLen(v.as_ptr() as *mut c_char, v.len() as c_int);
                 SET_STRING_ELT(sexp, i as R_xlen_t, ch);
             }
             Self { robj }
diff --git a/extendr-engine/src/lib.rs b/extendr-engine/src/lib.rs
index 72562ee4d..19dead126 100644
--- a/extendr-engine/src/lib.rs
+++ b/extendr-engine/src/lib.rs
@@ -1,17 +1,18 @@
-//! A sigleton instance of the R interpreter.
+//! A singleton instance of the R interpreter.
 //!
-//! Only call this from main() if you want to run stand-alone.
+//! Only call this from `main()` if you want to run stand-alone.
 //!
 //! Its principal use is for testing.
 //!
-//! See <https://github.com/wch/r-source/blob/trunk/src/unix/Rembedded.c>
+//! See [Rembedded.c](https://github.com/wch/r-source/blob/trunk/src/unix/Rembedded.c).
+//!
 
 use libR_sys::*;
 use std::os::raw;
 use std::sync::Once;
 
 // Generate mutable static strings.
-// Much more efficient than CString.
+// Much more efficient than `CString`.
 // Generates asciiz.
 macro_rules! cstr_mut {
     ($s: expr) => {
diff --git a/extendr-macros/src/extendr_module.rs b/extendr-macros/src/extendr_module.rs
index 688b1c29f..d6f003454 100644
--- a/extendr-macros/src/extendr_module.rs
+++ b/extendr-macros/src/extendr_module.rs
@@ -48,8 +48,8 @@ pub fn extendr_module(item: TokenStream) -> TokenStream {
             #( #implmetanames(&mut impls); )*
 
             // Extends functions and impls with the submodules metadata
-            #( functions.extend(#usemetanames().functions); )*
-            #( impls.extend(#usemetanames().impls); )*
+            #( functions.extend(#usenames::#usemetanames().functions); )*
+            #( impls.extend(#usenames::#usemetanames().impls); )*
 
             // Add this function to the list, but set hidden: true.
             functions.push(extendr_api::metadata::Func {
@@ -88,6 +88,7 @@ pub fn extendr_module(item: TokenStream) -> TokenStream {
         #[no_mangle]
         #[allow(non_snake_case)]
         pub extern "C" fn #wrap_module_metadata_name() -> extendr_api::SEXP {
+            use extendr_api::GetSexp;
             unsafe { extendr_api::Robj::from(#module_metadata_name()).get() }
         }
 
@@ -99,6 +100,7 @@ pub fn extendr_module(item: TokenStream) -> TokenStream {
         ) -> extendr_api::SEXP {
             unsafe {
                 use extendr_api::robj::*;
+                use extendr_api::GetSexp;
                 let robj = new_owned(use_symbols_sexp);
                 let use_symbols: bool = <bool>::from_robj(&robj).unwrap();
 


Test patch:
diff --git a/extendr-api/tests/extendr_module_tests.rs b/extendr-api/tests/extendr_module_tests.rs
new file mode 100644
index 000000000..3aeea81ba
--- /dev/null
+++ b/extendr-api/tests/extendr_module_tests.rs
@@ -0,0 +1,41 @@
+//! Tests for [`extendr_module`] procedural macro.
+//!
+use extendr_api::{extendr, extendr_module};
+
+mod root {
+    use super::*;
+
+    mod nested_module {
+        use super::*;
+
+        #[extendr]
+        fn dummy() {}
+
+        extendr_module! {
+            mod nested_module;
+            fn dummy;
+        }
+    }
+
+    #[extendr]
+    fn hello_dummy() {}
+
+    extendr_module! {
+        mod top_level;
+        use nested_module;
+        use adjacent_module;
+        fn hello_dummy;
+    }
+}
+
+mod adjacent_module {
+    use super::*;
+
+    #[extendr]
+    fn foo() {}
+
+    extendr_module! {
+        mod adjacent_module;
+        fn foo;
+    }
+}
diff --git a/extendr-api/tests/externalptr_test.rs b/extendr-api/tests/externalptr_tests.rs
similarity index 100%
rename from extendr-api/tests/externalptr_test.rs
rename to extendr-api/tests/externalptr_tests.rs
diff --git a/extendr-api/tests/test_io.rs b/extendr-api/tests/io_tests.rs
similarity index 100%
rename from extendr-api/tests/test_io.rs
rename to extendr-api/tests/io_tests.rs
diff --git a/extendr-api/tests/macro_tests.rs b/extendr-api/tests/macro_tests.rs
index 96b819678..9af98b3a0 100644
--- a/extendr-api/tests/macro_tests.rs
+++ b/extendr-api/tests/macro_tests.rs
@@ -1,4 +1,3 @@
-// use extendr_api::prelude::*;
 use extendr_api::{list, test, List, Result};
 
 #[test]
diff --git a/extendr-api/tests/ndarray_try_from_tests.rs b/extendr-api/tests/ndarray_try_from_tests.rs
new file mode 100644
index 000000000..2d846b698
--- /dev/null
+++ b/extendr-api/tests/ndarray_try_from_tests.rs
@@ -0,0 +1,354 @@
+#[cfg(feature = "ndarray")]
+mod ndarray_try_from_tests {
+    use extendr_api::prelude::*;
+
+    #[test]
+    fn integer_matrix_2d() {
+        test! {
+             let robj = R!("matrix(c(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L), ncol = 2, nrow = 4, byrow = TRUE)")?;
+
+             let view = <ArrayView2<Rint>>::try_from(&robj)?;
+             assert_eq!(view.dim(), (4, 2));
+
+             for (&mapped, expected) in view.iter().zip(1..=8) {
+                 assert_eq!(mapped, expected);
+             }
+
+             let view = <ArrayView2<i32>>::try_from(&robj)?;
+             assert_eq!(view.dim(), (4, 2));
+
+             for (&mapped, expected) in view.iter().zip(1..=8) {
+                 assert_eq!(mapped, expected);
+             }
+
+              let view = <ArrayView2<u32>>::try_from(&robj)?;
+             assert_eq!(view.dim(), (4, 2));
+
+             for (&mapped, expected) in view.iter().zip(1..=8) {
+                 assert_eq!(mapped, expected);
+             }
+        }
+    }
+
+    #[test]
+    fn real_matrix_2d() {
+        test! {
+             let robj = R!("matrix(c(1, 2, 3, 4, 5, 6, 7, 8), ncol = 2, nrow = 4, byrow = TRUE)")?;
+
+             let view = <ArrayView2<Rfloat>>::try_from(&robj)?;
+             assert_eq!(view.dim(), (4, 2));
+             for (&mapped, expected) in view.iter().zip(1..=8) {
+                 assert_eq!(mapped, expected as f64);
+             }
+
+             let view = <ArrayView2<f64>>::try_from(&robj)?;
+             assert_eq!(view.dim(), (4, 2));
+             for (&mapped, expected) in view.iter().zip(1..=8) {
+                 assert_eq!(mapped, expected as f64);
+             }
+        }
+    }
+
+    #[test]
+    fn logical_matrix_2d() {
+        test! {
+             let robj = R!("matrix(c(TRUE, FALSE, TRUE, FALSE, FALSE, TRUE), ncol = 2, nrow = 3, byrow = TRUE)")?;
+
+             let view = <ArrayView2<Rbool>>::try_from(&robj)?;
+             assert_eq!(view.dim(), (3, 2));
+             for (&mapped, expected) in view.iter().zip(vec![true, false, true, false, false, true]) {
+                 assert_eq!(mapped, expected);
+             }
+        }
+    }
+
+    #[test]
+    fn complex_matrix_2d() {
+        test! {
+             let robj = R!("matrix(c(1, 2, 3i, 4i, 5 + 5i, 6 - 6i), ncol = 2, nrow = 3, byrow = TRUE)")?;
+
+             let expected = vec![1, 2, 0, 0, 5, 6].iter().zip(vec![0, 0, 3, 4, 5, -6]).map(|(&re, im)| <c64>::new(re as f64, im as f64))
+             .collect::<Vec<_>>();
+
+             let view = <ArrayView2<Rcplx>>::try_from(&robj)?;
+             assert_eq!(view.dim(), (3, 2));
+
+             for (&mapped, &expected) in view.iter().zip(expected.iter()) {
+                 assert_eq!(mapped, <Rcplx>::from(expected));
+             }
+
+             let view = <ArrayView2<c64>>::try_from(&robj)?;
+             assert_eq!(view.dim(), (3, 2));
+             for (&mapped, &expected) in view.iter().zip(expected.iter()) {
+                 assert_eq!(mapped, expected);
+             }
+        }
+    }
+
+    #[test]
+    fn character_matrix_2d() {
+        test! {
+             let robj = R!("matrix(c(\"Hello\", \"World\"), ncol = 1, nrow = 2, byrow = TRUE)")?;
+
+             let view = <ArrayView2<Rstr>>::try_from(&robj)?;
+             assert_eq!(view.dim(), (2, 1));
+             for (mapped, expected) in view.iter().zip(vec!["Hello", "World"]) {
+                 assert_eq!(mapped, expected);
+             }
+        }
+    }
+
+    #[test]
+    fn integer_matrix_1d() {
+        test! {
+             let robj = R!("matrix(c(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L), ncol = 1, nrow = 8)")?;
+
+             let view = <ArrayView1<Rint>>::try_from(&robj)?;
+             assert_eq!(view.dim(), 8);
+             for (&mapped, expected) in view.iter().zip(1..=8) {
+                 assert_eq!(mapped, expected);
+             }
+
+             let view = <ArrayView1<i32>>::try_from(&robj)?;
+             assert_eq!(view.dim(), 8);
+             for (&mapped, expected) in view.iter().zip(1..=8) {
+                 assert_eq!(mapped, expected);
+             }
+        }
+    }
+
+    #[test]
+    fn real_matrix_1d() {
+        test! {
+             let robj = R!("matrix(c(1, 2, 3, 4, 5, 6, 7, 8), ncol = 1, nrow = 8)")?;
+
+             let view = <ArrayView1<Rfloat>>::try_from(&robj)?;
+             assert_eq!(view.dim(), 8);
+             for (&mapped, expected) in view.iter().zip(1..=8) {
+                 assert_eq!(mapped, expected as f64);
+             }
+
+             let view = <ArrayView1<f64>>::try_from(&robj)?;
+             assert_eq!(view.dim(), 8);
+             for (&mapped, expected) in view.iter().zip(1..=8) {
+                 assert_eq!(mapped, expected as f64);
+             }
+        }
+    }
+
+    #[test]
+    fn logical_matrix_1d() {
+        test! {
+             let robj = R!("matrix(c(TRUE, FALSE, TRUE, FALSE, FALSE, TRUE), ncol = 1, nrow = 6)")?;
+
+             let view = <ArrayView1<Rbool>>::try_from(&robj)?;
+             assert_eq!(view.dim(), 6);
+             for (&mapped, expected) in view.iter().zip(vec![true, false, true, false, false, true]) {
+                 assert_eq!(mapped, expected);
+             }
+        }
+    }
+
+    #[test]
+    fn complex_matrix_1d() {
+        test! {
+             let robj = R!("matrix(c(1, 2, 3i, 4i, 5 + 5i, 6 - 6i), ncol = 1, nrow = 6)")?;
+
+             let expected = vec![1, 2, 0, 0, 5, 6].iter().zip(vec![0, 0, 3, 4, 5, -6]).map(|(&re, im)| <c64>::new(re as f64, im as f64))
+             .collect::<Vec<_>>();
+
+             let view = <ArrayView1<Rcplx>>::try_from(&robj)?;
+             assert_eq!(view.dim(), 6);
+             for (&mapped, &expected) in view.iter().zip(expected.iter()) {
+                 assert_eq!(mapped, <Rcplx>::from(expected));
+             }
+
+             let view = <ArrayView1<c64>>::try_from(&robj)?;
+             assert_eq!(view.dim(), 6);
+             for (&mapped, &expected) in view.iter().zip(expected.iter()) {
+                 assert_eq!(mapped, expected);
+             }
+        }
+    }
+
+    #[test]
+    fn character_matrix_1d() {
+        test! {
+             let robj = R!("matrix(c(\"Hello\", \"World\"), ncol = 1, nrow = 2)")?;
+
+             let view = <ArrayView1<Rstr>>::try_from(&robj)?;
+             assert_eq!(view.dim(), 2);
+             for (mapped, expected) in view.iter().zip(vec!["Hello", "World"]) {
+                 assert_eq!(mapped, expected);
+             }
+        }
+    }
+
+    #[test]
+    fn integer_matrix_1d_type_mismatch() {
+        test! {
+             let robj = R!("matrix(c(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L), ncol = 1, nrow = 8)")?;
+
+             assert!(<ArrayView1<Rfloat>>::try_from(&robj).is_err());
+             assert!(<ArrayView1<f64>>::try_from(&robj).is_err());
+
+             assert!(<ArrayView1<Rcplx>>::try_from(&robj).is_err());
+             assert!(<ArrayView1<c64>>::try_from(&robj).is_err());
+
+             assert!(<ArrayView1<Rbool>>::try_from(&robj).is_err());
+             assert!(<ArrayView1<Rstr>>::try_from(&robj).is_err());
+        }
+    }
+
+    #[test]
+    fn real_matrix_1d_type_mismatch() {
+        test! {
+            let robj = R!("matrix(c(1, 2, 3, 4, 5, 6, 7, 8), ncol = 1, nrow = 8)")?;
+
+             assert!(<ArrayView1<Rint>>::try_from(&robj).is_err());
+             assert!(<ArrayView1<i32>>::try_from(&robj).is_err());
+
+             assert!(<ArrayView1<Rcplx>>::try_from(&robj).is_err());
+             assert!(<ArrayView1<c64>>::try_from(&robj).is_err());
+
+             assert!(<ArrayView1<Rbool>>::try_from(&robj).is_err());
+             assert!(<ArrayView1<Rstr>>::try_from(&robj).is_err());
+        }
+    }
+
+    #[test]
+    fn logical_matrix_1d_type_mismatch() {
+        test! {
+             let robj = R!("matrix(c(TRUE, FALSE, TRUE, FALSE, FALSE, TRUE), ncol = 1, nrow = 6)")?;
+
+             assert!(<ArrayView1<Rint>>::try_from(&robj).is_err());
+             assert!(<ArrayView1<i32>>::try_from(&robj).is_err());
+
+             assert!(<ArrayView1<Rfloat>>::try_from(&robj).is_err());
+             assert!(<ArrayView1<f64>>::try_from(&robj).is_err());
+
+             assert!(<ArrayView1<Rcplx>>::try_from(&robj).is_err());
+             assert!(<ArrayView1<c64>>::try_from(&robj).is_err());
+
+             assert!(<ArrayView1<Rstr>>::try_from(&robj).is_err());
+        }
+    }
+
+    #[test]
+    fn complex_matrix_1d_type_mismatch() {
+        test! {
+             let robj = R!("matrix(c(1, 2, 3i, 4i, 5 + 5i, 6 - 6i), ncol = 1, nrow = 6)")?;
+
+             assert!(<ArrayView1<Rint>>::try_from(&robj).is_err());
+             assert!(<ArrayView1<i32>>::try_from(&robj).is_err());
+
+             assert!(<ArrayView1<Rfloat>>::try_from(&robj).is_err());
+             assert!(<ArrayView1<f64>>::try_from(&robj).is_err());
+
+             assert!(<ArrayView1<Rstr>>::try_from(&robj).is_err());
+             assert!(<ArrayView1<Rbool>>::try_from(&robj).is_err());
+        }
+    }
+
+    #[test]
+    fn character_matrix_1d_type_mismatch() {
+        test! {
+             let robj = R!("matrix(c(\"Hello\", \"World\"), ncol = 1, nrow = 2)")?;
+
+             assert!(<ArrayView1<Rint>>::try_from(&robj).is_err());
+             assert!(<ArrayView1<i32>>::try_from(&robj).is_err());
+
+             assert!(<ArrayView1<Rfloat>>::try_from(&robj).is_err());
+             assert!(<ArrayView1<f64>>::try_from(&robj).is_err());
+
+             assert!(<ArrayView1<Rcplx>>::try_from(&robj).is_err());
+             assert!(<ArrayView1<c64>>::try_from(&robj).is_err());
+
+             assert!(<ArrayView1<Rbool>>::try_from(&robj).is_err());
+        }
+    }
+
+    #[test]
+    fn integer_matrix_2d_type_mismatch() {
+        test! {
+             let robj = R!("matrix(c(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L), ncol = 2, nrow = 4, byrow = TRUE)")?;
+
+             assert!(<ArrayView2<Rfloat>>::try_from(&robj).is_err());
+             assert!(<ArrayView2<f64>>::try_from(&robj).is_err());
+
+             assert!(<ArrayView2<Rcplx>>::try_from(&robj).is_err());
+             assert!(<ArrayView2<c64>>::try_from(&robj).is_err());
+
+             assert!(<ArrayView1<Rbool>>::try_from(&robj).is_err());
+             assert!(<ArrayView1<Rstr>>::try_from(&robj).is_err());
+        }
+    }
+
+    #[test]
+    fn real_matrix_2d_type_mismatch() {
+        test! {
+             let robj = R!("matrix(c(1, 2, 3, 4, 5, 6, 7, 8), ncol = 2, nrow = 4, byrow = TRUE)")?;
+
+             assert!(<ArrayView2<Rint>>::try_from(&robj).is_err());
+             assert!(<ArrayView2<i32>>::try_from(&robj).is_err());
+
+             assert!(<ArrayView2<Rcplx>>::try_from(&robj).is_err());
+             assert!(<ArrayView2<c64>>::try_from(&robj).is_err());
+
+             assert!(<ArrayView1<Rbool>>::try_from(&robj).is_err());
+             assert!(<ArrayView1<Rstr>>::try_from(&robj).is_err());
+        }
+    }
+
+    #[test]
+    fn logical_matrix_2d_type_mismatch() {
+        test! {
+             let robj = R!("matrix(c(TRUE, FALSE, TRUE, FALSE, FALSE, TRUE), ncol = 2, nrow = 3, byrow = TRUE)")?;
+
+             assert!(<ArrayView2<Rint>>::try_from(&robj).is_err());
+             assert!(<ArrayView2<i32>>::try_from(&robj).is_err());
+
+             assert!(<ArrayView2<Rfloat>>::try_from(&robj).is_err());
+             assert!(<ArrayView2<f64>>::try_from(&robj).is_err());
+
+             assert!(<ArrayView2<Rcplx>>::try_from(&robj).is_err());
+             assert!(<ArrayView2<c64>>::try_from(&robj).is_err());
+
+             assert!(<ArrayView1<Rstr>>::try_from(&robj).is_err());
+        }
+    }
+
+    #[test]
+    fn complex_matrix_2d_type_mismatch() {
+        test! {
+             let robj = R!("matrix(c(1, 2, 3i, 4i, 5 + 5i, 6 - 6i), ncol = 2, nrow = 3, byrow = TRUE)")?;
+
+             assert!(<ArrayView2<Rint>>::try_from(&robj).is_err());
+             assert!(<ArrayView2<i32>>::try_from(&robj).is_err());
+
+             assert!(<ArrayView2<Rfloat>>::try_from(&robj).is_err());
+             assert!(<ArrayView2<f64>>::try_from(&robj).is_err());
+
+             assert!(<ArrayView1<Rstr>>::try_from(&robj).is_err());
+             assert!(<ArrayView1<Rbool>>::try_from(&robj).is_err());
+        }
+    }
+
+    #[test]
+    fn character_matrix_2d_type_mismatch() {
+        test! {
+             let robj = R!("matrix(c(\"Hello\", \"World\"), ncol = 1, nrow = 2, byrow = TRUE)")?;
+
+             assert!(<ArrayView2<Rint>>::try_from(&robj).is_err());
+             assert!(<ArrayView2<i32>>::try_from(&robj).is_err());
+
+             assert!(<ArrayView2<Rfloat>>::try_from(&robj).is_err());
+             assert!(<ArrayView2<f64>>::try_from(&robj).is_err());
+
+             assert!(<ArrayView2<Rcplx>>::try_from(&robj).is_err());
+             assert!(<ArrayView2<c64>>::try_from(&robj).is_err());
+
+             assert!(<ArrayView1<Rbool>>::try_from(&robj).is_err());
+        }
+    }
+}
diff --git a/extendr-api/tests/serializer_tests.rs b/extendr-api/tests/serializer_tests.rs
index c4c9c64fe..5253a0509 100644
--- a/extendr-api/tests/serializer_tests.rs
+++ b/extendr-api/tests/serializer_tests.rs
@@ -4,8 +4,6 @@ mod test {
     use extendr_api::serializer::to_robj;
     use serde::Serialize;
 
-    ////////////////////////////////////////////////////////////////////////////////
-
     #[test]
     fn test_serialize_struct() {
         test! {
diff --git a/tests/extendrtests/DESCRIPTION b/tests/extendrtests/DESCRIPTION
index 8884c5104..53e1c8edf 100644
--- a/tests/extendrtests/DESCRIPTION
+++ b/tests/extendrtests/DESCRIPTION
@@ -1,6 +1,6 @@
 Package: extendrtests
 Title: Integration Tests for Calling Extendr from an R Package
-Version: 0.2.0
+Version: 0.2.1
 Authors@R: c(
     person("Andy", "Thomason", , "andy@andythomason.com", c("aut")),
     person("Claus O.", "Wilke", , "wilke@austin.utexas.edu", c("aut", "cre"),
diff --git a/tests/extendrtests/NAMESPACE b/tests/extendrtests/NAMESPACE
index 5a65f144d..7e10b63b4 100644
--- a/tests/extendrtests/NAMESPACE
+++ b/tests/extendrtests/NAMESPACE
@@ -11,6 +11,7 @@ S3method("[[",MySubmoduleClass)
 export("__00__special_function_name")
 export(MyClass)
 export(MySubmoduleClass)
+export(euclidean_dist)
 export(hello_submodule)
 export(make_wrappers)
 export(my_device)
diff --git a/tests/extendrtests/R/extendr-wrappers.R b/tests/extendrtests/R/extendr-wrappers.R
index 29d71c157..a9d2c4b83 100644
--- a/tests/extendrtests/R/extendr-wrappers.R
+++ b/tests/extendrtests/R/extendr-wrappers.R
@@ -81,6 +81,12 @@ my_device <- function(welcome_message) invisible(.Call(wrap__my_device, welcome_
 #' @export
 hello_submodule <- function() .Call(wrap__hello_submodule)
 
+#' Calculate Euclidean distance matrix
+#' Test case adopted from https://github.com/mikemahoney218/examplerust/blob/23d21b1ced4e24b7a7c00dd36290114dc1bbd113/src/rust/src/lib.rs#L5
+#' @param a : Matrix of real values or `NULL`
+#' @export
+euclidean_dist <- function(a) .Call(wrap__euclidean_dist, a)
+
 #' Class for testing (exported)
 #' @examples
 #' x <- MyClass$new()
diff --git a/tests/extendrtests/man/euclidean_dist.Rd b/tests/extendrtests/man/euclidean_dist.Rd
new file mode 100644
index 000000000..74516a5b7
--- /dev/null
+++ b/tests/extendrtests/man/euclidean_dist.Rd
@@ -0,0 +1,16 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/extendr-wrappers.R
+\name{euclidean_dist}
+\alias{euclidean_dist}
+\title{Calculate Euclidean distance matrix
+Test case adopted from https://github.com/mikemahoney218/examplerust/blob/23d21b1ced4e24b7a7c00dd36290114dc1bbd113/src/rust/src/lib.rs#L5}
+\usage{
+euclidean_dist(a)
+}
+\arguments{
+\item{a}{: Matrix of real values or \code{NULL}}
+}
+\description{
+Calculate Euclidean distance matrix
+Test case adopted from https://github.com/mikemahoney218/examplerust/blob/23d21b1ced4e24b7a7c00dd36290114dc1bbd113/src/rust/src/lib.rs#L5
+}
diff --git a/tests/extendrtests/src/rust/Cargo.toml b/tests/extendrtests/src/rust/Cargo.toml
index 2ebef8f15..e5382426a 100644
--- a/tests/extendrtests/src/rust/Cargo.toml
+++ b/tests/extendrtests/src/rust/Cargo.toml
@@ -2,7 +2,7 @@
 
 [package]
 name = "extendrtests"
-version = "0.2.0"
+version = "0.2.1"
 authors = [
     "andy-thomason <andy@andythomason.com>",
     "Claus O. Wilke <wilke@austin.utexas.edu>",
@@ -14,7 +14,7 @@ edition = "2021"
 crate-type = ["staticlib"]
 
 [dependencies]
-extendr-api = { version = "*", features = ["graphics"] }
+extendr-api = { version = "*", features = ["graphics", "ndarray"] }
 
 [patch.crates-io]
 ## This is configured to work with RStudio features.
diff --git a/tests/extendrtests/src/rust/src/lib.rs b/tests/extendrtests/src/rust/src/lib.rs
index af0c8341f..bccc77b91 100644
--- a/tests/extendrtests/src/rust/src/lib.rs
+++ b/tests/extendrtests/src/rust/src/lib.rs
@@ -1,7 +1,9 @@
 use extendr_api::{graphics::*, prelude::*};
 
 mod submodule;
-use submodule::*;
+
+mod ndarray;
+use ndarray::*;
 
 mod graphic_device;
 
@@ -167,7 +169,7 @@ fn check_default(#[default = "NULL"] x: Robj) -> bool {
 }
 
 // Weird behavior of parameter descriptions:
-// first passes tests as is, second -- only in backqutoes.
+// first passes tests as is, second -- only in backquotes.
 /// Test whether `_arg` parameters are treated correctly in R
 /// Executes \code{`_x` - `_y`}
 /// @param _x an integer scalar, ignored
@@ -352,4 +354,5 @@ extendr_module! {
     fn my_device;
 
     use submodule;
+    use ndarray;
 }
diff --git a/tests/extendrtests/src/rust/src/ndarray.rs b/tests/extendrtests/src/rust/src/ndarray.rs
new file mode 100644
index 000000000..bad132843
--- /dev/null
+++ b/tests/extendrtests/src/rust/src/ndarray.rs
@@ -0,0 +1,33 @@
+use extendr_api::prelude::*;
+
+/// Calculate Euclidean distance matrix
+/// Test case adopted from https://github.com/mikemahoney218/examplerust/blob/23d21b1ced4e24b7a7c00dd36290114dc1bbd113/src/rust/src/lib.rs#L5
+/// @param a : Matrix of real values or `NULL`
+/// @export
+#[extendr(use_try_from = true)]
+fn euclidean_dist(a: Nullable<ArrayView2<Rfloat>>) -> Nullable<Doubles> {
+    if let NotNull(a) = a {
+        let nrow = a.nrows();
+
+        let result = (0..(nrow - 1))
+            .map(|x| {
+                ((x + 1)..nrow).map(move |y| {
+                    let z = &a.slice(s![x, ..]) - &a.slice(s![y, ..]);
+                    (&z * &z).iter().sum::<Rfloat>().sqrt()
+                })
+            })
+            .flatten()
+            .collect();
+
+        Nullable::NotNull(result)
+    } else {
+        Nullable::Null
+    }
+}
+
+// Macro to generate exports
+extendr_module! {
+    fn euclidean_dist;
+
+    mod ndarray;
+}
diff --git a/tests/extendrtests/tests/testthat/test-ndarray.R b/tests/extendrtests/tests/testthat/test-ndarray.R
new file mode 100644
index 000000000..b20467976
--- /dev/null
+++ b/tests/extendrtests/tests/testthat/test-ndarray.R
@@ -0,0 +1,13 @@
+test_that("[ndarray] Euclidean distance", {
+    m <- matrix(rnorm(100), nrow = 10, ncol = 10)
+    dist <- euclidean_dist(m)
+    expected_dist <- dist(m, method = "euclidean") |> as.numeric()
+
+    expect_equal(dist, expected_dist)
+})
+
+test_that("[ndarray] Euclidean distance returns `NULL` when `NULL` is passed", {
+    dist <- euclidean_dist(NULL)
+
+    expect_equal(dist, NULL)
+})