Repository: extendr/extendr
Pull request #706 (2024-03-03 12:57:04)

Introduce optional faer support 
This PR introduces optional support for `faer::Mat<f64>` into and from a `Robj` in order to integrate with the [faer](https://github.com/sarah-ek/faer-rs) crate.

CGMossa: Alright! I have been waiting for this to happen!

`faer` is a linear algebra library written in Rust, with excellent performance, comparable to BLAS / LAPACK / etc. See https://github.com/sarah-ek/faer-rs
It has been on-going for years now. There are regular releases, and many use-ful algorithms has been added.

This could be to extendr, what Armadillo is to Rcpp. Although the focus is slightly more scoped.

Feedback: I've since we spoke about this, learned a few things about our code-base. We are meant to provide both FromRobj and TryFrom. The "main" implementation should be on `&Robj`, and then for `TryFrom<Robj>` should use the `&Robj`-implementation.

We also have a section for optional crate integrations. if it is possible to move it to there, and then have the `cfg(feature = "faer")` attached to the module itself, that would be great. 

faer implements sparse matrices, that is implemented in R by the `{Matrix}` package. Making PR that helps with these conversions could be a good next step.

Also, I think we need a test or two, but we can defer that to a bit later. 


I'm very happy that you've embarked on this PR.
0urobor0s: > Feedback: I've since we spoke about this, learned a few things about our code-base. We are meant to provide both FromRobj and TryFrom. The "main" implementation should be on `&Robj`, and then for `TryFrom<Robj>` should use the `&Robj`-implementation.

Not sure I understood this, should the traits be implemented for `&Robj` instead of `Robj`? If the implementation doesn't take ownership of the `Robj` shouldn't it always have to copy the values?

> faer implements sparse matrices, that is implemented in R by the `{Matrix}` package. Making PR that helps with these conversions could be a good next step.

Currently I haven't explored sparse matrix from R's side, so I would say for this PR, only Dense would be provided.

> By _test_ I meant we might want this https://github.com/extendr/extendr/blob/master/tests/extendrtests/tests/testthat/test-ndarray.R but for `test-faer.R`.

Not sure I understood the tests, are these tests "integration"-like tests? It seems they were testing the underlying library instead and the code indirectly more than the code itself. In theory shouldn't the tests be constrained to the affected structs and the upper abstractions already be tested?
JosiahParry: This is exciting! I'm not familiar with `faer` but there's a few things to address and document from my perspective: 

## Supported Types 

In R, every vector type can be stored in a matrix but not every type can likely be converted in to a faer `Mat`. 

I believe the ones that _can be_ converted are:

- double
- integer
- factor
- complex

The ones that _cannot be_ converted are 

- character
- list

For example in R this is a valid list:

```r
matrix(
  list(R6::R6Class("tst"), 1L, letters, function(){}),
  ncol = 2
)
```

## Conversion

### From RMatrix

We can support conversion from RMatrix with `TryFrom` for supported types. These should fail (IIUC) if NA is present or the types are not supported (list, character)

### From faer

We can support From for faer::Mat<T> where T is i32, u32, f64, and Complex I believe.

### FromRobj

I think FromRobj should respect multiple types like described above and should not be prescribed to only f64. 

0urobor0s: > We also have a section for optional crate integrations. if it is possible to move it to there, and then have the `cfg(feature = "faer")` attached to the module itself, that would be great.

I think this is now addressed by commit https://github.com/extendr/extendr/pull/706/commits/1a8bdd52d994761306e5b49a8a9f02dbc8f99a32.
0urobor0s: Regarding [this comment](https://github.com/extendr/extendr/pull/706#issuecomment-1975239242), [faer currently only supports](https://docs.rs/faer-core/0.17.1/faer_core/trait.Entity.html) `f32`, `f64`, `c32` and `c64`. The reason for only f64 being supported is because R doesn't support single precision, as for integer types, I don't even know if those should even be supported but chose to include them because LAPACK seems to also convert to double precision and wanted that interoperability, however for a library like `extendr` that is also an API design decision. As for complex types, I'm not familiar with their behaviour in other libraries so I cannot say. For factors, I don't know if they are a good fit for this kind of purpose.
CGMossa: This is how we could add support for complex numbers: https://discord.com/channels/751419551132155925/751419551132155928/1214741810857902100

Answer provided by author of faer.
JosiahParry: I've had a chance to test this locally. There are a couple of issues. There is a namespace conflict (present in ndarray feature too). So we need to change the `pub mod` to `mod` so we do not have a namespace conflict—this allows us to also import `faer` in our `Cargo.toml`

```rust
#[cfg(feature = "faer")]
mod faer;
```

Additionally, the conversion from `faer` to Robj does not return a matrix but rather a single vector:

```rust
#[extendr]
fn faer_mat(x: Robj) -> Robj {
    let m = Mat::<f64>::from_robj(&x).unwrap();
    m.into_robj()
}
```

The `dim` attribute needs to be added so that it gets turned into a matrix additionally, integer's `NA` are not respected. 

``` r
library(tst2)
m <- matrix(1:9, ncol = 3)
m[1,1] <- NA
m
#>      [,1] [,2] [,3]
#> [1,]   NA    4    7
#> [2,]    2    5    8
#> [3,]    3    6    9

faer_mat(m)
#> [1] -2147483648           2           3           4           5           6
#> [7]           7           8           9
```

There's also a segfault when I run 

```r
faer_mat(as.double(m))
```
0urobor0s: The comments from https://github.com/extendr/extendr/pull/706#issuecomment-1984120974 are mostly address by https://github.com/extendr/extendr/pull/706/commits/fb24d037ad5e053aa00dd06448163d0ce411cf58, and in addition it also adds a way for `cargo extendr devtools-test` to compile the R test project, for better development experience. This addition is probably out of the scope of this PR or can introduce some issues, so maybe it's best to open it in a new PR, let me know.

> The `dim` attribute needs to be added so that it gets turned into a matrix additionally, integer's `NA` are not respected.

Should a `RArray`/`RMatrix` be returned when the trait is implemented for `Robj`? Wouldn't it be up to the caller to convert from `Robj` to the desired output? Or maybe a new trait implementation would be necessary for `RArray`/`RMatrix`. This could probably be done with something like:
```rust
mat.col_chunks(1).collect_rarray([mat.nrows(); 2])
```

> There's also a segfault when I run
> 
> ```r
> faer_mat(as.double(m))
> ```
> 

I think this is because of the `unwrap()` in your function definition, as the R `as_double()` function doesn't return a matrix, https://github.com/extendr/extendr/pull/706/commits/fb24d037ad5e053aa00dd06448163d0ce411cf58#diff-78d342e81d701ac77edd01fe11cab6da7cf941774e2ea660bfefc8076997a7da shows a possible way to check if the call was succeful or not and return an appropriate response. Does `extendr` have any implicit way of handling these cases?

JosiahParry: I spent some time revisiting this PR today. I would quite like to see it merged, however, I think in its current state it just isn't quite there yet. 

The two big things for me are the fact that the conversion to Robj are done in the context of Robj and not some more specific struct. In the case of `faer`, I think it makes most sense to add conversion to `RMatrix<f64>` then delegate to RMatrix's conversion to Robj. 

The second thing is that the current implementation crashes R (i think due to dereferencing?). I've tried taking a stab at this in a branch off of your branch, @0urobor0s. Lines 1-55 are the main changes I've made https://github.com/extendr/extendr/blob/fd3848f892faa211a9f4a8529139df6931754aae/extendr-api/src/optional/faer.rs#L7-L55. 

I made some rough integration tests as well for this purpose. All of the below work well! 

I got a bit caught up with lifetimes and the borrow checker when trying to get faer to work with `&Robj` and `&RMatrix<f64>`. Perhaps you or @CGMossa know a bit more about that.

```rust

#[extendr]
fn mat_to_mat(x: Mat<f64>) -> Mat<f64> {
    x
}

#[extendr]
fn mat_to_rmat(x: Mat<f64>) -> RMatrix<f64> {
    RMatrix::<f64>::from(x)
}

#[extendr]
fn mat_to_robj(x: Mat<f64>) -> Robj {
    x.into_robj()
}

#[extendr]
fn mat_to_rmatfloat(x: Mat<f64>) -> RMatrix<Rfloat> {
    RMatrix::<Rfloat>::from(x)
}

// convert to Mat<f64> from other things
#[extendr]
fn rmat_to_mat(x: RMatrix<f64>) -> Mat<f64> {
    Mat::<f64>::from(x)
}

#[extendr]
fn robj_to_mat(x: Robj) -> Mat<f64> {
    Mat::<f64>::try_from(x).unwrap()
}

// MatRef input
fn matref_to_mat(x: MatRef<'_, f64>) -> Robj {
    RMatrix::<f64>::from(x).into()
}
```
```r
library(testthat)

m <- matrix(rnorm(100), ncol = 10)
expect_identical(m, mat_to_rmat(m))
expect_identical(m, mat_to_robj(m))
expect_identical(m, mat_to_rmatfloat(m))
expect_identical(m, mat_to_mat(m))
expect_identical(m, rmat_to_mat(m))
expect_identical(m, robj_to_mat(m))
```
0urobor0s: > The two big things for me are the fact that the conversion to Robj are done in the context of Robj and not some more specific struct. In the case of `faer`, I think it makes most sense to add conversion to `RMatrix<f64>` then delegate to RMatrix's conversion to Robj.

Agree with this.

> The second thing is that the current implementation crashes R (i think due to dereferencing?). I've tried taking a stab at this in a branch off of your branch, @0urobor0s. Lines 1-55 are the main changes I've made

Can you point to a test that crashes the R session? Applied your changes in the PR with some changes in https://github.com/extendr/extendr/pull/706/commits/2338bfdc77e3e3e6b4e449a2373b5cdadc0105a1.




> I got a bit caught up with lifetimes and the borrow checker when trying to get faer to work with `&Robj` and `&RMatrix<f64>`. Perhaps you or @CGMossa know a bit more about that.

I think this mostly because `as_real_slice()` doesn't have the necessary lifetime notation for it to work, with the change to `as_typed_slice()` and the necessary lifetime notation it works.
0urobor0s: Trying to do something like:
```rust

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d9972d118..0f1013489 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
 
 ### Added
 
+- New optional `faer` feature which enables conversion between `faer` matrix and `RMatrix<f64>` [[#706]](https://github.com/extendr/extendr/pull/706)
 - Adds `TryFrom<Robj>` and `<TryFrom<&Robj>` for `impl` blocks marked with `#[extendr]` macro allowing falliable conversion to `&Self` `&mut Self` 
 - Adds `From<T> for Robj` for impl blocks marked with `#[extendr]` macro
 - The new `ExpectedExternalNonNullPtr` error variant provides a more informative error when a null pointer is accessed
diff --git a/extendr-api/Cargo.toml b/extendr-api/Cargo.toml
index e3733d1be..8397d9f24 100644
--- a/extendr-api/Cargo.toml
+++ b/extendr-api/Cargo.toml
@@ -20,6 +20,7 @@ libc = { version = "0.2", optional = true }
 ndarray = { version = "0.15.3", optional = true }
 num-complex = { version = "0.4", optional = true }
 serde = { version = "1.0", features = ["derive"], optional = true }
+faer = { version = "0.18.0", optional = true }
 
 [dev-dependencies]
 extendr-engine = { path = "../extendr-engine" }
@@ -33,10 +34,10 @@ rstest = "0.19.0"
 result_list = []
 result_condition = []
 
-# This dummy feature enables all features that increase the functionality of 
+# This dummy feature enables all features that increase the functionality of
 # extendr, via conversions or R features. Features that change behaviour
 # but do not add functionality (such as `libR-sys/use-bindgen`) are excluded
-full-functionality = ["graphics", "either", "ndarray", "num-complex", "serde"]
+full-functionality = ["graphics", "either", "ndarray", "faer", "num-complex", "serde"]
 
 # libc is needed to allocate a DevDesc (c.f., https://bugs.r-project.org/show_bug.cgi?id=18292)
 graphics = ["libc"]
@@ -46,7 +47,7 @@ tests-minimal = ["libR-sys/use-bindgen"]
 
 # All features to test except for graphics; graphics tests are currently unstable
 # and require --test-threads=1, so we decided to exclude it from here (c.f. #378).
-tests = ["tests-minimal", "ndarray", "serde", "num-complex", "either"]
+tests = ["tests-minimal", "ndarray", "faer", "serde", "num-complex", "either"]
 
 tests-graphics = ["tests-minimal", "graphics"]
 
diff --git a/extendr-api/src/optional/faer.rs b/extendr-api/src/optional/faer.rs
new file mode 100644
index 000000000..38340805f
--- /dev/null
+++ b/extendr-api/src/optional/faer.rs
@@ -0,0 +1,280 @@
+use faer::{mat, Mat, MatRef};
+
+use crate::scalar::Rfloat;
+use crate::scalar::Scalar;
+use crate::*;
+
+/// Convert a `faer::Mat<f64>` into an `RMatrix<f64>` which is not NA aware.
+impl From<Mat<f64>> for RMatrix<f64> {
+    fn from(value: Mat<f64>) -> Self {
+        RMatrix::new_matrix(value.nrows(), value.ncols(), |i, j| value.read(i, j))
+    }
+}
+
+impl From<Mat<f64>> for Robj {
+    fn from(value: Mat<f64>) -> Self {
+        RMatrix::<f64>::from(value).into()
+    }
+}
+
+/// Convert a `faer::Mat<f64>` into an `RMatrix<f64>` which is not NA aware.
+impl From<MatRef<'_, f64>> for RMatrix<f64> {
+    /// Convert a faer MatRef<f64> into Robj.
+    fn from(value: MatRef<'_, f64>) -> Self {
+        RMatrix::new_matrix(value.nrows(), value.ncols(), |i, j| value.read(i, j))
+    }
+}
+
+impl From<MatRef<'_, f64>> for Robj {
+    fn from(value: MatRef<'_, f64>) -> Self {
+        RMatrix::<f64>::from(value).into_robj()
+    }
+}
+
+impl From<Mat<f64>> for RMatrix<Rfloat> {
+    fn from(value: Mat<f64>) -> Self {
+        RMatrix::new_matrix(value.nrows(), value.ncols(), |i, j| value.read(i, j).into())
+    }
+}
+
+impl From<MatRef<'_, f64>> for RMatrix<Rfloat> {
+    fn from(value: MatRef<f64>) -> Self {
+        RMatrix::new_matrix(value.nrows(), value.ncols(), |i, j| {
+            Rfloat::from(value.read(i, j))
+        })
+    }
+}
+
+impl From<RMatrix<f64>> for Mat<f64> {
+    fn from(value: RMatrix<f64>) -> Self {
+        let nrow = value.nrows();
+        let ncol = value.ncols();
+        let slice = value.as_real_slice().expect("RMatrix should be doubles");
+        Mat::from_fn(nrow, ncol, |i, j| slice[i + j * nrow])
+    }
+}
+
+impl<'a> From<&'_ RMatrix<f64>> for MatRef<'a, f64> {
+    fn from(value: &RMatrix<f64>) -> Self {
+        let nrow = value.nrows();
+        let ncol = value.ncols();
+        let slice = value.as_typed_slice().expect("RMatrix should be doubles");
+        let mat_ref = faer::mat::from_column_major_slice::<f64>(slice, nrow, ncol);
+        mat_ref
+    }
+}
+
+impl TryFrom<&Robj> for Mat<f64> {
+    type Error = Error;
+
+    fn try_from(robj: &Robj) -> Result<Self> {
+        let rmat = &RMatrix::<f64>::from_robj(robj)?;
+        let nrow = rmat.nrows();
+        let ncol = rmat.ncols();
+
+        if let Some(slice) = robj.as_real_slice() {
+            let fmat = Mat::from_fn(nrow, ncol, |i, j| slice[i + j * nrow]);
+            Ok(fmat)
+        } else {
+            Err(Error::ExpectedReal(robj.clone()))
+        }
+    }
+}
+
+impl<'a> TryFrom<&'_ Robj> for MatRef<'a, f64> {
+    type Error = Error;
+
+    fn try_from(robj: &Robj) -> Result<Self> {
+        let rmat = &RMatrix::<f64>::from_robj(robj)?;
+        let nrows = rmat.nrows();
+        let ncols = rmat.ncols();
+
+        if let Some(slice) = robj.as_typed_slice() {
+            let fmat = mat::from_column_major_slice::<f64>(slice, nrows, ncols);
+            Ok(fmat)
+        } else {
+            Err(Error::ExpectedReal(robj.clone()))
+        }
+    }
+}
+
+impl FromRobj<'_> for Mat<f64> {
+    fn from_robj(robj: &'_ Robj) -> std::result::Result<Self, &'static str> {
+        robj.try_into()
+            .map_err(|_| "unable to convert R object to Mat<f64>")
+    }
+}
+
+impl<'a> FromRobj<'_> for MatRef<'a, f64> {
+    fn from_robj(robj: &'_ Robj) -> std::result::Result<Self, &'static str> {
+        robj.try_into()
+            .map_err(|_| "unable to convert R object to MatRef<'_, f64>")
+    }
+}
+
+impl TryFrom<Robj> for Mat<f64> {
+    type Error = crate::Error;
+
+    fn try_from(robj: Robj) -> Result<Self> {
+        Self::try_from(&robj)
+    }
+}
+
+impl<'a> TryFrom<Robj> for MatRef<'a, f64> {
+    type Error = crate::Error;
+
+    fn try_from(robj: Robj) -> Result<Self> {
+        Self::try_from(&robj)
+    }
+}
+
+impl From<RMatrix<i32>> for Mat<f64> {
+    fn from(value: RMatrix<i32>) -> Self {
+        let nrow = value.nrows();
+        let ncol = value.ncols();
+        let slice = value
+            .as_integer_slice()
+            .expect("RMatrix should be integers");
+        Mat::from_fn(nrow, ncol, |i, j| slice[i + j * nrow] as f64)
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use crate::*;
+    use faer::{mat, Mat, MatRef};
+
+    #[test]
+    fn test_rmatrix_to_faer_mat() {
+        test! {
+            let values = [
+                [1.0, 5.0, 9.0],
+                [2.0, 6.0, 10.0],
+                [3.0, 7.0, 11.0],
+                [4.0, 8.0, 12.0f64]
+            ];
+            let a = Mat::<f64>::from_fn(4, 3, |i, j| values[i][j] as f64);
+
+            let rmatrix = RMatrix::new_matrix(4, 3, |i, j| values[i][j]);
+            let b = Mat::<f64>::try_from(rmatrix);
+            assert_eq!(a, b.expect("matrix to be converted"));
+        }
+    }
+
+    #[test]
+    fn test_rmatrix_to_faer_mat_with_nan() {
+        test! {
+            let values = [
+                [1.0, 5.0, 9.0],
+                [2.0, 6.0, 10.0],
+                [3.0, 7.0, 11.0],
+                [f64::NAN, 8.0, 12.0f64]
+            ];
+
+            let rmatrix = RMatrix::new_matrix(4, 3, |i, j| values[i][j]);
+            let b = Mat::<f64>::try_from(rmatrix);
+            assert!(b.expect("matrix to be converted").read(3, 0).is_nan());
+        }
+    }
+
+    #[test]
+    fn test_rmatrix_to_faer_mat_ref() {
+        test! {
+            let values = [
+                [1.0, 5.0, 9.0],
+                [2.0, 6.0, 10.0],
+                [3.0, 7.0, 11.0],
+                [4.0, 8.0, 12.0f64]
+            ];
+            let mat = Mat::<f64>::from_fn(4, 3, |i, j| values[i][j] as f64);
+            let a = mat.as_ref();
+
+            let rmatrix = RMatrix::new_matrix(4, 3, |i, j| values[i][j]);
+            let b = MatRef::<f64>::try_from(&rmatrix);
+            assert_eq!(a, b.expect("matrix to be converted"));
+        }
+    }
+
+    #[test]
+    fn test_faer_mat_to_rmatrix() {
+        test! {
+            let vec: Vec<f64> = (1..13).map(f64::from).collect();
+            let a = mat![
+                [1.0, 5.0, 9.0],
+                [2.0, 6.0, 10.0],
+                [3.0, 7.0, 11.0],
+                [4.0, 8.0, 12.0f64],
+            ];
+            let rmatrix: RMatrix<f64> = a.into();
+            assert_eq!(rmatrix.as_real_slice().expect("slice"), &vec);
+        }
+    }
+
+    #[test]
+    fn test_faer_mat_ref_to_rmatrix() {
+        test! {
+            let vec: Vec<f64> = (1..13).map(f64::from).collect();
+            let a = mat![
+                [1.0, 5.0, 9.0],
+                [2.0, 6.0, 10.0],
+                [3.0, 7.0, 11.0],
+                [4.0, 8.0, 12.0f64],
+            ];
+            let rmatrix: RMatrix<f64> = a.as_ref().into();
+            assert_eq!(rmatrix.as_real_slice().expect("slice"), &vec);
+        }
+    }
+
+    #[test]
+    fn test_try_from_robj_to_faer_mat() {
+        test! {
+            let values = [
+                [1.0, 5.0, 9.0],
+                [2.0, 6.0, 10.0],
+                [3.0, 7.0, 11.0],
+                [4.0, 8.0, 12.0f64]
+            ];
+            let a = Mat::<f64>::from_fn(4, 3, |i, j| values[i][j] as f64);
+
+            let rmatrix = RMatrix::new_matrix(4, 3, |i, j| values[i][j]);
+            let b = Mat::<f64>::try_from(&Robj::from(rmatrix));
+            assert_eq!(a, b.expect("matrix to be converted"));
+        }
+    }
+
+    #[test]
+    fn test_try_from_robj_to_faer_mat_ref() {
+        test! {
+            let values = [
+                [1.0, 5.0, 9.0],
+                [2.0, 6.0, 10.0],
+                [3.0, 7.0, 11.0],
+                [4.0, 8.0, 12.0f64]
+            ];
+            let mat = Mat::<f64>::from_fn(4, 3, |i, j| values[i][j] as f64);
+            let a = mat.as_ref();
+
+            let rmatrix = RMatrix::new_matrix(4, 3, |i, j| values[i][j]);
+            let robj = Robj::from(rmatrix);
+            let b = MatRef::<f64>::try_from(&robj);
+            assert_eq!(a, b.expect("matrix to be converted"));
+        }
+    }
+
+    #[test]
+    fn test_int_rmatrix_to_faer_mat() {
+        test! {
+            let values = [
+                [1, 5, 9],
+                [2, 6, 10],
+                [3, 7, 11],
+                [4, 8, 12]
+            ];
+            let a = Mat::<f64>::from_fn(4, 3, |i, j| values[i][j] as f64);
+
+            let rmatrix = RMatrix::new_matrix(4, 3, |i, j| values[i][j]);
+            let b = Mat::<f64>::try_from(rmatrix);
+            assert_eq!(a, b.expect("matrix to be converted"));
+        }
+    }
+}
diff --git a/extendr-api/src/optional/mod.rs b/extendr-api/src/optional/mod.rs
index 73bf3be16..86f838432 100644
--- a/extendr-api/src/optional/mod.rs
+++ b/extendr-api/src/optional/mod.rs
@@ -3,5 +3,7 @@ A set of optional features and third-party crate integrations, usually hidden be
 */
 #[cfg(feature = "either")]
 pub mod either;
+#[cfg(feature = "faer")]
+mod faer;
 #[cfg(feature = "ndarray")]
 pub mod ndarray;


Test patch:
diff --git a/tests/extendrtests/R/extendr-wrappers.R b/tests/extendrtests/R/extendr-wrappers.R
index 111d20689..29a72dd4f 100644
--- a/tests/extendrtests/R/extendr-wrappers.R
+++ b/tests/extendrtests/R/extendr-wrappers.R
@@ -110,6 +110,20 @@ type_aware_sum <- function(input) .Call(wrap__type_aware_sum, input)
 #' @export
 euclidean_dist <- function(a) .Call(wrap__euclidean_dist, a)
 
+mat_to_mat <- function(x) .Call(wrap__mat_to_mat, x)
+
+mat_to_rmat <- function(x) .Call(wrap__mat_to_rmat, x)
+
+mat_to_robj <- function(x) .Call(wrap__mat_to_robj, x)
+
+mat_to_rmatfloat <- function(x) .Call(wrap__mat_to_rmatfloat, x)
+
+rmat_to_mat <- function(x) .Call(wrap__rmat_to_mat, x)
+
+robj_to_mat <- function(x) .Call(wrap__robj_to_mat, x)
+
+matref_to_mat <- function(x) .Call(wrap__matref_to_mat, x)
+
 #' Test raw identifiers (`r#`) in function arguments are parsed correctly.
 #' See [Issue #582](https://github.com/extendr/extendr/issues/528) for details.
 #' @param type : i32 or `NULL`
diff --git a/tests/extendrtests/src/rust/Cargo.toml b/tests/extendrtests/src/rust/Cargo.toml
index 93136c806..45dbfa9b1 100644
--- a/tests/extendrtests/src/rust/Cargo.toml
+++ b/tests/extendrtests/src/rust/Cargo.toml
@@ -16,7 +16,8 @@ publish = false
 crate-type = ["staticlib"]
 
 [dependencies]
-extendr-api = { version = "*", features = ["graphics", "ndarray", "either"] }
+extendr-api = { version = "*", features = ["graphics", "ndarray", "faer", "either"] }
+faer = "0.18.0"
 
 # TODO: I couldn't find any nice way to add the condition based on the R version
 # except for using libR-sys just for "DEP_R_*" envvars.
diff --git a/tests/extendrtests/src/rust/src/lib.rs b/tests/extendrtests/src/rust/src/lib.rs
index 6a3498753..eeb9b71fc 100644
--- a/tests/extendrtests/src/rust/src/lib.rs
+++ b/tests/extendrtests/src/rust/src/lib.rs
@@ -5,6 +5,7 @@ mod dataframe;
 mod graphic_device;
 mod memory_leaks;
 mod optional_either;
+mod optional_faer;
 mod optional_ndarray;
 mod raw_identifiers;
 mod submodule;
@@ -447,6 +448,7 @@ extendr_module! {
     use memory_leaks;
     use optional_either;
     use optional_ndarray;
+    use optional_faer;
     use raw_identifiers;
     use submodule;
 
diff --git a/tests/extendrtests/src/rust/src/optional_either.rs b/tests/extendrtests/src/rust/src/optional_either.rs
index 750f064b2..c0bd20aea 100644
--- a/tests/extendrtests/src/rust/src/optional_either.rs
+++ b/tests/extendrtests/src/rust/src/optional_either.rs
@@ -1,7 +1,7 @@
 use extendr_api::prelude::*;
 
 #[extendr(use_try_from = true)]
-fn type_aware_sum(input : Either<Integers, Doubles>) -> Either<Rint, Rfloat> {
+fn type_aware_sum(input: Either<Integers, Doubles>) -> Either<Rint, Rfloat> {
     match input {
         Left(left) => Left(left.iter().sum()),
         Right(right) => Right(right.iter().sum()),
diff --git a/tests/extendrtests/src/rust/src/optional_faer.rs b/tests/extendrtests/src/rust/src/optional_faer.rs
new file mode 100644
index 000000000..3bf6828d9
--- /dev/null
+++ b/tests/extendrtests/src/rust/src/optional_faer.rs
@@ -0,0 +1,51 @@
+use extendr_api::prelude::*;
+use faer::{Mat, MatRef};
+
+#[extendr]
+fn mat_to_mat(x: Mat<f64>) -> Mat<f64> {
+    x
+}
+
+#[extendr]
+fn mat_to_rmat(x: Mat<f64>) -> RMatrix<f64> {
+    RMatrix::<f64>::from(x)
+}
+
+#[extendr]
+fn mat_to_robj(x: Mat<f64>) -> Robj {
+    x.into_robj()
+}
+
+#[extendr]
+fn mat_to_rmatfloat(x: Mat<f64>) -> RMatrix<Rfloat> {
+    RMatrix::<Rfloat>::from(x)
+}
+
+// convert to Mat<f64> from other things
+#[extendr]
+fn rmat_to_mat(x: RMatrix<f64>) -> Mat<f64> {
+    Mat::<f64>::from(x)
+}
+
+#[extendr]
+fn robj_to_mat(x: Robj) -> Mat<f64> {
+    Mat::<f64>::try_from(x).unwrap()
+}
+
+// MatRef input
+#[extendr]
+fn matref_to_mat(x: MatRef<'_, f64>) -> Robj {
+    RMatrix::<f64>::from(x).into()
+}
+
+// Macro to generate exports
+extendr_module! {
+    mod optional_faer;
+    fn mat_to_mat;
+    fn mat_to_rmat;
+    fn mat_to_robj;
+    fn mat_to_rmatfloat;
+    fn rmat_to_mat;
+    fn robj_to_mat;
+    fn matref_to_mat;
+}
diff --git a/tests/extendrtests/tests/testthat/_snaps/macro-snapshot.md b/tests/extendrtests/tests/testthat/_snaps/macro-snapshot.md
index 28dcc4890..3b473a57a 100644
--- a/tests/extendrtests/tests/testthat/_snaps/macro-snapshot.md
+++ b/tests/extendrtests/tests/testthat/_snaps/macro-snapshot.md
@@ -1921,6 +1921,721 @@
               };
           }
       }
+      mod optional_faer {
+          use extendr_api::prelude::*;
+          use faer::{Mat, MatRef};
+          fn mat_to_mat(x: Mat<f64>) -> Mat<f64> {
+              x
+          }
+          #[no_mangle]
+          #[allow(non_snake_case, clippy::not_unsafe_ptr_arg_deref)]
+          pub extern "C" fn wrap__mat_to_mat(x: extendr_api::SEXP) -> extendr_api::SEXP {
+              use extendr_api::robj::*;
+              let wrap_result_state: std::result::Result<
+                  std::result::Result<Robj, extendr_api::Error>,
+                  Box<dyn std::any::Any + Send>,
+              > = unsafe {
+                  let _x_robj = extendr_api::robj::Robj::from_sexp(x);
+                  std::panic::catch_unwind(
+                      std::panic::AssertUnwindSafe(|| -> std::result::Result<
+                          Robj,
+                          extendr_api::Error,
+                      > {
+                          Ok(
+                              extendr_api::Robj::from(
+                                  mat_to_mat(<Mat<f64>>::from_robj(&_x_robj)?),
+                              ),
+                          )
+                      }),
+                  )
+              };
+              match wrap_result_state {
+                  Ok(Ok(zz)) => {
+                      return unsafe { zz.get() };
+                  }
+                  Ok(Err(conversion_err)) => {
+                      let err_string = conversion_err.to_string();
+                      drop(conversion_err);
+                      extendr_api::throw_r_error(&err_string);
+                  }
+                  Err(unwind_err) => {
+                      drop(unwind_err);
+                      let err_string = {
+                          let res = ::alloc::fmt::format(
+                              format_args!("user function panicked: {0}", "mat_to_mat"),
+                          );
+                          res
+                      };
+                      extendr_api::handle_panic(
+                          err_string.as_str(),
+                          || {
+                              #[cold]
+                              #[track_caller]
+                              #[inline(never)]
+                              const fn panic_cold_explicit() -> ! {
+                                  ::core::panicking::panic_explicit()
+                              }
+                              panic_cold_explicit();
+                          },
+                      );
+                  }
+              }
+              {
+                  ::core::panicking::panic_fmt(
+                      format_args!(
+                          "internal error: entered unreachable code: {0}",
+                          format_args!("internal extendr error, this should never happen."),
+                      ),
+                  );
+              }
+          }
+          #[allow(non_snake_case)]
+          fn meta__mat_to_mat(metadata: &mut Vec<extendr_api::metadata::Func>) {
+              let mut args = <[_]>::into_vec(
+                  #[rustc_box]
+                  ::alloc::boxed::Box::new([
+                      extendr_api::metadata::Arg {
+                          name: "x",
+                          arg_type: "Mat",
+                          default: None,
+                      },
+                  ]),
+              );
+              metadata
+                  .push(extendr_api::metadata::Func {
+                      doc: "",
+                      rust_name: "mat_to_mat",
+                      r_name: "mat_to_mat",
+                      mod_name: "mat_to_mat",
+                      args: args,
+                      return_type: "Mat",
+                      func_ptr: wrap__mat_to_mat as *const u8,
+                      hidden: false,
+                  })
+          }
+          fn mat_to_rmat(x: Mat<f64>) -> RMatrix<f64> {
+              RMatrix::<f64>::from(x)
+          }
+          #[no_mangle]
+          #[allow(non_snake_case, clippy::not_unsafe_ptr_arg_deref)]
+          pub extern "C" fn wrap__mat_to_rmat(x: extendr_api::SEXP) -> extendr_api::SEXP {
+              use extendr_api::robj::*;
+              let wrap_result_state: std::result::Result<
+                  std::result::Result<Robj, extendr_api::Error>,
+                  Box<dyn std::any::Any + Send>,
+              > = unsafe {
+                  let _x_robj = extendr_api::robj::Robj::from_sexp(x);
+                  std::panic::catch_unwind(
+                      std::panic::AssertUnwindSafe(|| -> std::result::Result<
+                          Robj,
+                          extendr_api::Error,
+                      > {
+                          Ok(
+                              extendr_api::Robj::from(
+                                  mat_to_rmat(<Mat<f64>>::from_robj(&_x_robj)?),
+                              ),
+                          )
+                      }),
+                  )
+              };
+              match wrap_result_state {
+                  Ok(Ok(zz)) => {
+                      return unsafe { zz.get() };
+                  }
+                  Ok(Err(conversion_err)) => {
+                      let err_string = conversion_err.to_string();
+                      drop(conversion_err);
+                      extendr_api::throw_r_error(&err_string);
+                  }
+                  Err(unwind_err) => {
+                      drop(unwind_err);
+                      let err_string = {
+                          let res = ::alloc::fmt::format(
+                              format_args!("user function panicked: {0}", "mat_to_rmat"),
+                          );
+                          res
+                      };
+                      extendr_api::handle_panic(
+                          err_string.as_str(),
+                          || {
+                              #[cold]
+                              #[track_caller]
+                              #[inline(never)]
+                              const fn panic_cold_explicit() -> ! {
+                                  ::core::panicking::panic_explicit()
+                              }
+                              panic_cold_explicit();
+                          },
+                      );
+                  }
+              }
+              {
+                  ::core::panicking::panic_fmt(
+                      format_args!(
+                          "internal error: entered unreachable code: {0}",
+                          format_args!("internal extendr error, this should never happen."),
+                      ),
+                  );
+              }
+          }
+          #[allow(non_snake_case)]
+          fn meta__mat_to_rmat(metadata: &mut Vec<extendr_api::metadata::Func>) {
+              let mut args = <[_]>::into_vec(
+                  #[rustc_box]
+                  ::alloc::boxed::Box::new([
+                      extendr_api::metadata::Arg {
+                          name: "x",
+                          arg_type: "Mat",
+                          default: None,
+                      },
+                  ]),
+              );
+              metadata
+                  .push(extendr_api::metadata::Func {
+                      doc: "",
+                      rust_name: "mat_to_rmat",
+                      r_name: "mat_to_rmat",
+                      mod_name: "mat_to_rmat",
+                      args: args,
+                      return_type: "RMatrix",
+                      func_ptr: wrap__mat_to_rmat as *const u8,
+                      hidden: false,
+                  })
+          }
+          fn mat_to_robj(x: Mat<f64>) -> Robj {
+              x.into_robj()
+          }
+          #[no_mangle]
+          #[allow(non_snake_case, clippy::not_unsafe_ptr_arg_deref)]
+          pub extern "C" fn wrap__mat_to_robj(x: extendr_api::SEXP) -> extendr_api::SEXP {
+              use extendr_api::robj::*;
+              let wrap_result_state: std::result::Result<
+                  std::result::Result<Robj, extendr_api::Error>,
+                  Box<dyn std::any::Any + Send>,
+              > = unsafe {
+                  let _x_robj = extendr_api::robj::Robj::from_sexp(x);
+                  std::panic::catch_unwind(
+                      std::panic::AssertUnwindSafe(|| -> std::result::Result<
+                          Robj,
+                          extendr_api::Error,
+                      > {
+                          Ok(
+                              extendr_api::Robj::from(
+                                  mat_to_robj(<Mat<f64>>::from_robj(&_x_robj)?),
+                              ),
+                          )
+                      }),
+                  )
+              };
+              match wrap_result_state {
+                  Ok(Ok(zz)) => {
+                      return unsafe { zz.get() };
+                  }
+                  Ok(Err(conversion_err)) => {
+                      let err_string = conversion_err.to_string();
+                      drop(conversion_err);
+                      extendr_api::throw_r_error(&err_string);
+                  }
+                  Err(unwind_err) => {
+                      drop(unwind_err);
+                      let err_string = {
+                          let res = ::alloc::fmt::format(
+                              format_args!("user function panicked: {0}", "mat_to_robj"),
+                          );
+                          res
+                      };
+                      extendr_api::handle_panic(
+                          err_string.as_str(),
+                          || {
+                              #[cold]
+                              #[track_caller]
+                              #[inline(never)]
+                              const fn panic_cold_explicit() -> ! {
+                                  ::core::panicking::panic_explicit()
+                              }
+                              panic_cold_explicit();
+                          },
+                      );
+                  }
+              }
+              {
+                  ::core::panicking::panic_fmt(
+                      format_args!(
+                          "internal error: entered unreachable code: {0}",
+                          format_args!("internal extendr error, this should never happen."),
+                      ),
+                  );
+              }
+          }
+          #[allow(non_snake_case)]
+          fn meta__mat_to_robj(metadata: &mut Vec<extendr_api::metadata::Func>) {
+              let mut args = <[_]>::into_vec(
+                  #[rustc_box]
+                  ::alloc::boxed::Box::new([
+                      extendr_api::metadata::Arg {
+                          name: "x",
+                          arg_type: "Mat",
+                          default: None,
+                      },
+                  ]),
+              );
+              metadata
+                  .push(extendr_api::metadata::Func {
+                      doc: "",
+                      rust_name: "mat_to_robj",
+                      r_name: "mat_to_robj",
+                      mod_name: "mat_to_robj",
+                      args: args,
+                      return_type: "Robj",
+                      func_ptr: wrap__mat_to_robj as *const u8,
+                      hidden: false,
+                  })
+          }
+          fn mat_to_rmatfloat(x: Mat<f64>) -> RMatrix<Rfloat> {
+              RMatrix::<Rfloat>::from(x)
+          }
+          #[no_mangle]
+          #[allow(non_snake_case, clippy::not_unsafe_ptr_arg_deref)]
+          pub extern "C" fn wrap__mat_to_rmatfloat(x: extendr_api::SEXP) -> extendr_api::SEXP {
+              use extendr_api::robj::*;
+              let wrap_result_state: std::result::Result<
+                  std::result::Result<Robj, extendr_api::Error>,
+                  Box<dyn std::any::Any + Send>,
+              > = unsafe {
+                  let _x_robj = extendr_api::robj::Robj::from_sexp(x);
+                  std::panic::catch_unwind(
+                      std::panic::AssertUnwindSafe(|| -> std::result::Result<
+                          Robj,
+                          extendr_api::Error,
+                      > {
+                          Ok(
+                              extendr_api::Robj::from(
+                                  mat_to_rmatfloat(<Mat<f64>>::from_robj(&_x_robj)?),
+                              ),
+                          )
+                      }),
+                  )
+              };
+              match wrap_result_state {
+                  Ok(Ok(zz)) => {
+                      return unsafe { zz.get() };
+                  }
+                  Ok(Err(conversion_err)) => {
+                      let err_string = conversion_err.to_string();
+                      drop(conversion_err);
+                      extendr_api::throw_r_error(&err_string);
+                  }
+                  Err(unwind_err) => {
+                      drop(unwind_err);
+                      let err_string = {
+                          let res = ::alloc::fmt::format(
+                              format_args!("user function panicked: {0}", "mat_to_rmatfloat"),
+                          );
+                          res
+                      };
+                      extendr_api::handle_panic(
+                          err_string.as_str(),
+                          || {
+                              #[cold]
+                              #[track_caller]
+                              #[inline(never)]
+                              const fn panic_cold_explicit() -> ! {
+                                  ::core::panicking::panic_explicit()
+                              }
+                              panic_cold_explicit();
+                          },
+                      );
+                  }
+              }
+              {
+                  ::core::panicking::panic_fmt(
+                      format_args!(
+                          "internal error: entered unreachable code: {0}",
+                          format_args!("internal extendr error, this should never happen."),
+                      ),
+                  );
+              }
+          }
+          #[allow(non_snake_case)]
+          fn meta__mat_to_rmatfloat(metadata: &mut Vec<extendr_api::metadata::Func>) {
+              let mut args = <[_]>::into_vec(
+                  #[rustc_box]
+                  ::alloc::boxed::Box::new([
+                      extendr_api::metadata::Arg {
+                          name: "x",
+                          arg_type: "Mat",
+                          default: None,
+                      },
+                  ]),
+              );
+              metadata
+                  .push(extendr_api::metadata::Func {
+                      doc: "",
+                      rust_name: "mat_to_rmatfloat",
+                      r_name: "mat_to_rmatfloat",
+                      mod_name: "mat_to_rmatfloat",
+                      args: args,
+                      return_type: "RMatrix",
+                      func_ptr: wrap__mat_to_rmatfloat as *const u8,
+                      hidden: false,
+                  })
+          }
+          fn rmat_to_mat(x: RMatrix<f64>) -> Mat<f64> {
+              Mat::<f64>::from(x)
+          }
+          #[no_mangle]
+          #[allow(non_snake_case, clippy::not_unsafe_ptr_arg_deref)]
+          pub extern "C" fn wrap__rmat_to_mat(x: extendr_api::SEXP) -> extendr_api::SEXP {
+              use extendr_api::robj::*;
+              let wrap_result_state: std::result::Result<
+                  std::result::Result<Robj, extendr_api::Error>,
+                  Box<dyn std::any::Any + Send>,
+              > = unsafe {
+                  let _x_robj = extendr_api::robj::Robj::from_sexp(x);
+                  std::panic::catch_unwind(
+                      std::panic::AssertUnwindSafe(|| -> std::result::Result<
+                          Robj,
+                          extendr_api::Error,
+                      > {
+                          Ok(
+                              extendr_api::Robj::from(
+                                  rmat_to_mat(<RMatrix<f64>>::from_robj(&_x_robj)?),
+                              ),
+                          )
+                      }),
+                  )
+              };
+              match wrap_result_state {
+                  Ok(Ok(zz)) => {
+                      return unsafe { zz.get() };
+                  }
+                  Ok(Err(conversion_err)) => {
+                      let err_string = conversion_err.to_string();
+                      drop(conversion_err);
+                      extendr_api::throw_r_error(&err_string);
+                  }
+                  Err(unwind_err) => {
+                      drop(unwind_err);
+                      let err_string = {
+                          let res = ::alloc::fmt::format(
+                              format_args!("user function panicked: {0}", "rmat_to_mat"),
+                          );
+                          res
+                      };
+                      extendr_api::handle_panic(
+                          err_string.as_str(),
+                          || {
+                              #[cold]
+                              #[track_caller]
+                              #[inline(never)]
+                              const fn panic_cold_explicit() -> ! {
+                                  ::core::panicking::panic_explicit()
+                              }
+                              panic_cold_explicit();
+                          },
+                      );
+                  }
+              }
+              {
+                  ::core::panicking::panic_fmt(
+                      format_args!(
+                          "internal error: entered unreachable code: {0}",
+                          format_args!("internal extendr error, this should never happen."),
+                      ),
+                  );
+              }
+          }
+          #[allow(non_snake_case)]
+          fn meta__rmat_to_mat(metadata: &mut Vec<extendr_api::metadata::Func>) {
+              let mut args = <[_]>::into_vec(
+                  #[rustc_box]
+                  ::alloc::boxed::Box::new([
+                      extendr_api::metadata::Arg {
+                          name: "x",
+                          arg_type: "RMatrix",
+                          default: None,
+                      },
+                  ]),
+              );
+              metadata
+                  .push(extendr_api::metadata::Func {
+                      doc: "",
+                      rust_name: "rmat_to_mat",
+                      r_name: "rmat_to_mat",
+                      mod_name: "rmat_to_mat",
+                      args: args,
+                      return_type: "Mat",
+                      func_ptr: wrap__rmat_to_mat as *const u8,
+                      hidden: false,
+                  })
+          }
+          fn robj_to_mat(x: Robj) -> Mat<f64> {
+              Mat::<f64>::try_from(x).unwrap()
+          }
+          #[no_mangle]
+          #[allow(non_snake_case, clippy::not_unsafe_ptr_arg_deref)]
+          pub extern "C" fn wrap__robj_to_mat(x: extendr_api::SEXP) -> extendr_api::SEXP {
+              use extendr_api::robj::*;
+              let wrap_result_state: std::result::Result<
+                  std::result::Result<Robj, extendr_api::Error>,
+                  Box<dyn std::any::Any + Send>,
+              > = unsafe {
+                  let _x_robj = extendr_api::robj::Robj::from_sexp(x);
+                  std::panic::catch_unwind(
+                      std::panic::AssertUnwindSafe(|| -> std::result::Result<
+                          Robj,
+                          extendr_api::Error,
+                      > {
+                          Ok(
+                              extendr_api::Robj::from(
+                                  robj_to_mat(<Robj>::from_robj(&_x_robj)?),
+                              ),
+                          )
+                      }),
+                  )
+              };
+              match wrap_result_state {
+                  Ok(Ok(zz)) => {
+                      return unsafe { zz.get() };
+                  }
+                  Ok(Err(conversion_err)) => {
+                      let err_string = conversion_err.to_string();
+                      drop(conversion_err);
+                      extendr_api::throw_r_error(&err_string);
+                  }
+                  Err(unwind_err) => {
+                      drop(unwind_err);
+                      let err_string = {
+                          let res = ::alloc::fmt::format(
+                              format_args!("user function panicked: {0}", "robj_to_mat"),
+                          );
+                          res
+                      };
+                      extendr_api::handle_panic(
+                          err_string.as_str(),
+                          || {
+                              #[cold]
+                              #[track_caller]
+                              #[inline(never)]
+                              const fn panic_cold_explicit() -> ! {
+                                  ::core::panicking::panic_explicit()
+                              }
+                              panic_cold_explicit();
+                          },
+                      );
+                  }
+              }
+              {
+                  ::core::panicking::panic_fmt(
+                      format_args!(
+                          "internal error: entered unreachable code: {0}",
+                          format_args!("internal extendr error, this should never happen."),
+                      ),
+                  );
+              }
+          }
+          #[allow(non_snake_case)]
+          fn meta__robj_to_mat(metadata: &mut Vec<extendr_api::metadata::Func>) {
+              let mut args = <[_]>::into_vec(
+                  #[rustc_box]
+                  ::alloc::boxed::Box::new([
+                      extendr_api::metadata::Arg {
+                          name: "x",
+                          arg_type: "Robj",
+                          default: None,
+                      },
+                  ]),
+              );
+              metadata
+                  .push(extendr_api::metadata::Func {
+                      doc: "",
+                      rust_name: "robj_to_mat",
+                      r_name: "robj_to_mat",
+                      mod_name: "robj_to_mat",
+                      args: args,
+                      return_type: "Mat",
+                      func_ptr: wrap__robj_to_mat as *const u8,
+                      hidden: false,
+                  })
+          }
+          fn matref_to_mat(x: MatRef<'_, f64>) -> Robj {
+              RMatrix::<f64>::from(x).into()
+          }
+          #[no_mangle]
+          #[allow(non_snake_case, clippy::not_unsafe_ptr_arg_deref)]
+          pub extern "C" fn wrap__matref_to_mat(x: extendr_api::SEXP) -> extendr_api::SEXP {
+              use extendr_api::robj::*;
+              let wrap_result_state: std::result::Result<
+                  std::result::Result<Robj, extendr_api::Error>,
+                  Box<dyn std::any::Any + Send>,
+              > = unsafe {
+                  let _x_robj = extendr_api::robj::Robj::from_sexp(x);
+                  std::panic::catch_unwind(
+                      std::panic::AssertUnwindSafe(|| -> std::result::Result<
+                          Robj,
+                          extendr_api::Error,
+                      > {
+                          Ok(
+                              extendr_api::Robj::from(
+                                  matref_to_mat(<MatRef<'_, f64>>::from_robj(&_x_robj)?),
+                              ),
+                          )
+                      }),
+                  )
+              };
+              match wrap_result_state {
+                  Ok(Ok(zz)) => {
+                      return unsafe { zz.get() };
+                  }
+                  Ok(Err(conversion_err)) => {
+                      let err_string = conversion_err.to_string();
+                      drop(conversion_err);
+                      extendr_api::throw_r_error(&err_string);
+                  }
+                  Err(unwind_err) => {
+                      drop(unwind_err);
+                      let err_string = {
+                          let res = ::alloc::fmt::format(
+                              format_args!("user function panicked: {0}", "matref_to_mat"),
+                          );
+                          res
+                      };
+                      extendr_api::handle_panic(
+                          err_string.as_str(),
+                          || {
+                              #[cold]
+                              #[track_caller]
+                              #[inline(never)]
+                              const fn panic_cold_explicit() -> ! {
+                                  ::core::panicking::panic_explicit()
+                              }
+                              panic_cold_explicit();
+                          },
+                      );
+                  }
+              }
+              {
+                  ::core::panicking::panic_fmt(
+                      format_args!(
+                          "internal error: entered unreachable code: {0}",
+                          format_args!("internal extendr error, this should never happen."),
+                      ),
+                  );
+              }
+          }
+          #[allow(non_snake_case)]
+          fn meta__matref_to_mat(metadata: &mut Vec<extendr_api::metadata::Func>) {
+              let mut args = <[_]>::into_vec(
+                  #[rustc_box]
+                  ::alloc::boxed::Box::new([
+                      extendr_api::metadata::Arg {
+                          name: "x",
+                          arg_type: "MatRef",
+                          default: None,
+                      },
+                  ]),
+              );
+              metadata
+                  .push(extendr_api::metadata::Func {
+                      doc: "",
+                      rust_name: "matref_to_mat",
+                      r_name: "matref_to_mat",
+                      mod_name: "matref_to_mat",
+                      args: args,
+                      return_type: "Robj",
+                      func_ptr: wrap__matref_to_mat as *const u8,
+                      hidden: false,
+                  })
+          }
+          #[no_mangle]
+          #[allow(non_snake_case)]
+          pub fn get_optional_faer_metadata() -> extendr_api::metadata::Metadata {
+              let mut functions = Vec::new();
+              let mut impls = Vec::new();
+              meta__mat_to_mat(&mut functions);
+              meta__mat_to_rmat(&mut functions);
+              meta__mat_to_robj(&mut functions);
+              meta__mat_to_rmatfloat(&mut functions);
+              meta__rmat_to_mat(&mut functions);
+              meta__robj_to_mat(&mut functions);
+              meta__matref_to_mat(&mut functions);
+              functions
+                  .push(extendr_api::metadata::Func {
+                      doc: "Metadata access function.",
+                      rust_name: "get_optional_faer_metadata",
+                      mod_name: "get_optional_faer_metadata",
+                      r_name: "get_optional_faer_metadata",
+                      args: Vec::new(),
+                      return_type: "Metadata",
+                      func_ptr: wrap__get_optional_faer_metadata as *const u8,
+                      hidden: true,
+                  });
+              functions
+                  .push(extendr_api::metadata::Func {
+                      doc: "Wrapper generator.",
+                      rust_name: "make_optional_faer_wrappers",
+                      mod_name: "make_optional_faer_wrappers",
+                      r_name: "make_optional_faer_wrappers",
+                      args: <[_]>::into_vec(
+                          #[rustc_box]
+                          ::alloc::boxed::Box::new([
+                              extendr_api::metadata::Arg {
+                                  name: "use_symbols",
+                                  arg_type: "bool",
+                                  default: None,
+                              },
+                              extendr_api::metadata::Arg {
+                                  name: "package_name",
+                                  arg_type: "&str",
+                                  default: None,
+                              },
+                          ]),
+                      ),
+                      return_type: "String",
+                      func_ptr: wrap__make_optional_faer_wrappers as *const u8,
+                      hidden: true,
+                  });
+              extendr_api::metadata::Metadata {
+                  name: "optional_faer",
+                  functions,
+                  impls,
+              }
+          }
+          #[no_mangle]
+          #[allow(non_snake_case)]
+          pub extern "C" fn wrap__get_optional_faer_metadata() -> extendr_api::SEXP {
+              use extendr_api::GetSexp;
+              unsafe { extendr_api::Robj::from(get_optional_faer_metadata()).get() }
+          }
+          #[no_mangle]
+          #[allow(non_snake_case, clippy::not_unsafe_ptr_arg_deref)]
+          pub extern "C" fn wrap__make_optional_faer_wrappers(
+              use_symbols_sexp: extendr_api::SEXP,
+              package_name_sexp: extendr_api::SEXP,
+          ) -> extendr_api::SEXP {
+              unsafe {
+                  use extendr_api::robj::*;
+                  use extendr_api::GetSexp;
+                  let robj = Robj::from_sexp(use_symbols_sexp);
+                  let use_symbols: bool = <bool>::from_robj(&robj).unwrap();
+                  let robj = Robj::from_sexp(package_name_sexp);
+                  let package_name: &str = <&str>::from_robj(&robj).unwrap();
+                  extendr_api::Robj::from(
+                          get_optional_faer_metadata()
+                              .make_r_wrappers(use_symbols, package_name)
+                              .unwrap(),
+                      )
+                      .get()
+              }
+          }
+          #[no_mangle]
+          #[allow(non_snake_case, clippy::not_unsafe_ptr_arg_deref)]
+          pub extern "C" fn R_init_optional_faer_extendr(info: *mut extendr_api::DllInfo) {
+              unsafe {
+                  extendr_api::register_call_methods(info, get_optional_faer_metadata())
+              };
+          }
+      }
       mod optional_ndarray {
           use extendr_api::prelude::*;
           /// Calculate Euclidean distance matrix
@@ -9073,6 +9788,7 @@
           functions.extend(memory_leaks::get_memory_leaks_metadata().functions);
           functions.extend(optional_either::get_optional_either_metadata().functions);
           functions.extend(optional_ndarray::get_optional_ndarray_metadata().functions);
+          functions.extend(optional_faer::get_optional_faer_metadata().functions);
           functions.extend(raw_identifiers::get_raw_identifiers_metadata().functions);
           functions.extend(submodule::get_submodule_metadata().functions);
           impls.extend(altrep::get_altrep_metadata().impls);
@@ -9080,6 +9796,7 @@
           impls.extend(memory_leaks::get_memory_leaks_metadata().impls);
           impls.extend(optional_either::get_optional_either_metadata().impls);
           impls.extend(optional_ndarray::get_optional_ndarray_metadata().impls);
+          impls.extend(optional_faer::get_optional_faer_metadata().impls);
           impls.extend(raw_identifiers::get_raw_identifiers_metadata().impls);
           impls.extend(submodule::get_submodule_metadata().impls);
           functions
diff --git a/tests/extendrtests/tests/testthat/test-faer.R b/tests/extendrtests/tests/testthat/test-faer.R
new file mode 100644
index 000000000..19797e31a
--- /dev/null
+++ b/tests/extendrtests/tests/testthat/test-faer.R
@@ -0,0 +1,18 @@
+test_that("[faer] Dimensions are provided", {
+    m <- matrix(rnorm(9), ncol = 3)
+    m_faer <- rmat_to_mat(m)
+
+    expect_equal(nrow(m), nrow(m_faer))
+    expect_equal(ncol(m), ncol(m_faer))
+})
+
+library(testthat)
+
+m <- matrix(rnorm(100), ncol = 10)
+expect_identical(m, mat_to_mat(m))
+expect_identical(m, mat_to_rmat(m))
+expect_identical(m, mat_to_robj(m))
+expect_identical(m, mat_to_rmatfloat(m))
+expect_identical(m, rmat_to_mat(m))
+expect_identical(m, robj_to_mat(m))
+expect_identical(m, matref_to_mat(m))