Repository: extendr/extendr
Pull request #854 (2024-11-26 16:39:47)

Generalize `HashMap` conversion
A named list can be thought of as a `HashMap<S, Robj>` where `S` is a string type, and `Robj` is an opaque type representing arbitrary R data. One may liken a `list()` to only having say `Doubles`s, where in this case, the desired representation would be `HashMap<S, Doubles>`. While this may seem as a limited example, it probably covers a lot of use cases for `list()`. This PR provides this conversion on `Robj` and `List` through the `TryFrom`-interface. It can also be used in `#[extendr]`-`fn`. 

CGMossa: @JosiahParry This one is also seems to be lost to CI mania.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6da9d4285..4ce6be55a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@
   `i32`, and `f64`. [[#856]](https://github.com/extendr/extendr/pull/856)
 - `FromIterator<T>` and `FromIterator<&T>` for vector wrapper where `T` represents a matching underlying type (`bool`,
   `i32`, `f64`, `c64`) [[#879]](https://github.com/extendr/extendr/pull/879)
+- Conversion from `Robj`/`List` to `HashMap<_, T>` for `T: TryFrom<Robj>` [[#854]](https://github.com/extendr/extendr/pull/854)
 
 ### Changed
 
diff --git a/extendr-api/src/robj/try_from_robj.rs b/extendr-api/src/robj/try_from_robj.rs
index d0e3bd5e4..7756c253a 100644
--- a/extendr-api/src/robj/try_from_robj.rs
+++ b/extendr-api/src/robj/try_from_robj.rs
@@ -6,9 +6,9 @@
 //!
 //!
 //! [`ownership`]: crate::ownership
-use crate::conversions::try_into_int::FloatToInt;
-
 use super::*;
+use crate as extendr_api;
+use crate::conversions::try_into_int::FloatToInt;
 
 macro_rules! impl_try_from_scalar_integer {
     ($t:ty) => {
@@ -468,15 +468,6 @@ impl TryFrom<&Robj> for Rcplx {
 // 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 {
-    () => {};
-    (&mut [$type:ty], $($rest:tt)*) => {
-        impl_try_from_robj!(&mut [$type]);
-        impl_try_from_robj!($($rest)*);
-    };
-    ($type:ty, $($rest:tt)*) => {
-        impl_try_from_robj!($type);
-        impl_try_from_robj!($($rest)*);
-    };
     (&mut [$type:ty]) => {
         impl TryFrom<Robj> for &mut [$type] {
             type Error = Error;
@@ -506,9 +497,8 @@ macro_rules! impl_try_from_robj {
             }
         }
     };
-
-    ($type:ty) => {
-        impl TryFrom<Robj> for $type {
+    ($(@generics<$generics:tt>)? $type:ty $(where $($where_clause:tt)*)?) => {
+        impl$(<$generics>)? TryFrom<Robj> for $type $(where $($where_clause)*)? {
             type Error = Error;
 
             fn try_from(robj: Robj) -> Result<Self> {
@@ -516,7 +506,7 @@ macro_rules! impl_try_from_robj {
             }
         }
 
-        impl TryFrom<&Robj> for Option<$type> {
+        impl$(<$generics>)? TryFrom<&Robj> for Option<$type> $(where $($where_clause)*)? {
             type Error = Error;
 
             fn try_from(robj: &Robj) -> Result<Self> {
@@ -528,7 +518,7 @@ macro_rules! impl_try_from_robj {
             }
         }
 
-        impl TryFrom<Robj> for Option<$type> {
+        impl$(<$generics>)? TryFrom<Robj> for Option<$type> $(where $($where_clause)*)? {
             type Error = Error;
 
             fn try_from(robj: Robj) -> Result<Self> {
@@ -537,46 +527,93 @@ macro_rules! impl_try_from_robj {
         }
     };
 }
-
 #[rustfmt::skip]
-impl_try_from_robj!(
-    u8, u16, u32, u64, usize,
-    i8, i16, i32, i64, isize,
-    bool,
-    Rint, Rfloat, Rbool, Rcplx,
-    f32, f64,
-    Vec::<String>,
-    HashMap::<String, Robj>, HashMap::<&str, Robj>,
-    Vec::<Rint>, Vec::<Rfloat>, Vec::<Rbool>, Vec::<Rcplx>, Vec::<u8>, Vec::<i32>, Vec::<f64>,
-    &[Rint], &[Rfloat], &[Rbool], &[Rcplx], &[u8], &[i32], &[f64],
-    &mut [Rint], &mut [Rfloat], &mut [Rbool], &mut [Rcplx], &mut [u8], &mut [i32], &mut [f64],
-    &str, String,
-);
+impl_try_from_robj!(u8);
+impl_try_from_robj!(u16);
+impl_try_from_robj!(u32);
+impl_try_from_robj!(u64);
+impl_try_from_robj!(usize);
+
+impl_try_from_robj!(i8);
+impl_try_from_robj!(i16);
+impl_try_from_robj!(i32);
+impl_try_from_robj!(i64);
+impl_try_from_robj!(isize);
+
+impl_try_from_robj!(bool);
+
+impl_try_from_robj!(Rint);
+impl_try_from_robj!(Rfloat);
+impl_try_from_robj!(Rbool);
+impl_try_from_robj!(Rcplx);
+
+impl_try_from_robj!(f32);
+impl_try_from_robj!(f64);
+
+impl_try_from_robj!(Vec::<String>);
+impl_try_from_robj!(Vec::<Rint>);
+impl_try_from_robj!(Vec::<Rfloat>);
+impl_try_from_robj!(Vec::<Rbool>);
+impl_try_from_robj!(Vec::<Rcplx>);
+impl_try_from_robj!(Vec::<u8>);
+impl_try_from_robj!(Vec::<i32>);
+impl_try_from_robj!(Vec::<f64>);
+
+impl_try_from_robj!(&[Rint]);
+impl_try_from_robj!(&[Rfloat]);
+impl_try_from_robj!(&[Rbool]);
+impl_try_from_robj!(&[Rcplx]);
+impl_try_from_robj!(&[u8]);
+impl_try_from_robj!(&[i32]);
+impl_try_from_robj!(&[f64]);
+
+impl_try_from_robj!(&mut [Rint]);
+impl_try_from_robj!(&mut [Rfloat]);
+impl_try_from_robj!(&mut [Rbool]);
+impl_try_from_robj!(&mut [Rcplx]);
+impl_try_from_robj!(&mut [u8]);
+impl_try_from_robj!(&mut [i32]);
+impl_try_from_robj!(&mut [f64]);
+
+impl_try_from_robj!(&str);
+impl_try_from_robj!(String);
+
+impl_try_from_robj!(@generics<T> HashMap::<&str, T> where T: TryFrom<Robj, Error = error::Error>);
+impl_try_from_robj!(@generics<T> HashMap::<String,T> where T: TryFrom<Robj, Error = error::Error>);
+
+impl_try_from_robj!(HashMap::<&str, Robj>);
+impl_try_from_robj!(HashMap::<String, Robj>);
 
 // NOTE: this is included for compatibility with previously defined `FromRobj`
 // One should prefer `List::from_hashmap` instead,
 // and this `impl` should be deprecated next.
 
-impl TryFrom<&Robj> for HashMap<String, Robj> {
+impl<T> TryFrom<&Robj> for HashMap<&str, T>
+where
+    T: TryFrom<Robj, Error = error::Error>,
+{
     type Error = Error;
-    fn try_from(robj: &Robj) -> Result<Self> {
-        Ok(robj
-            .as_list()
-            .map(|l| l.iter())
-            .ok_or_else(|| Error::ExpectedList(robj.clone()))?
-            .map(|(k, v)| (k.to_string(), v))
-            .collect::<HashMap<String, Robj>>())
+
+    fn try_from(value: &Robj) -> Result<Self> {
+        let value: List = value.try_into()?;
+
+        let value = value
+            .iter()
+            .map(|(name, value)| -> Result<(&str, T)> { value.try_into().map(|x| (name, x)) })
+            .collect::<Result<HashMap<_, _>>>()?;
+
+        Ok(value)
     }
 }
 
-impl TryFrom<&Robj> for HashMap<&str, Robj> {
+impl<T> TryFrom<&Robj> for HashMap<String, T>
+where
+    T: TryFrom<Robj, Error = error::Error>,
+{
     type Error = Error;
-    fn try_from(robj: &Robj) -> Result<Self> {
-        Ok(robj
-            .as_list()
-            .map(|l| l.iter())
-            .ok_or_else(|| Error::ExpectedList(robj.clone()))?
-            .collect::<HashMap<&str, Robj>>())
+    fn try_from(value: &Robj) -> Result<Self> {
+        let value: HashMap<&str, _> = value.try_into()?;
+        Ok(value.into_iter().map(|(k, v)| (k.to_string(), v)).collect())
     }
 }
 
@@ -639,3 +676,23 @@ impl_try_from_robj_for_arrays!(f64);
 
 // Choosing arity 12.. As the Rust compiler did for these [Tuple to array conversion](https://doc.rust-lang.org/stable/std/primitive.tuple.html#trait-implementations-1)
 impl_try_from_robj_tuples!((1, 12));
+
+// The following is necessary because it is impossible to define `TryFrom<Robj> for &Robj` as
+// it requires returning a reference to a owned (moved) value
+
+impl TryFrom<&Robj> for HashMap<&str, Robj> {
+    type Error = Error;
+
+    fn try_from(value: &Robj) -> Result<Self> {
+        let value: List = value.try_into()?;
+        Ok(value.into_iter().collect())
+    }
+}
+
+impl TryFrom<&Robj> for HashMap<String, Robj> {
+    type Error = Error;
+    fn try_from(value: &Robj) -> Result<Self> {
+        let value: HashMap<&str, _> = value.try_into()?;
+        Ok(value.into_iter().map(|(k, v)| (k.to_string(), v)).collect())
+    }
+}
diff --git a/extendr-api/src/wrapper/list.rs b/extendr-api/src/wrapper/list.rs
index 88ab8a343..4d8915609 100644
--- a/extendr-api/src/wrapper/list.rs
+++ b/extendr-api/src/wrapper/list.rs
@@ -88,12 +88,17 @@ impl List {
     ///     assert_eq!(names, vec!["a", "b"]);
     /// }
     /// ```
-    pub fn from_hashmap<K>(val: HashMap<K, Robj>) -> Result<Self>
+    pub fn from_hashmap<K, V>(val: HashMap<K, V>) -> Result<Self>
     where
+        V: IntoRobj,
         K: Into<String>,
     {
-        let mut res: Self = Self::from_values(val.values());
-        res.set_names(val.into_keys().map(|k| k.into()))?;
+        let (names, values): (Vec<_>, Vec<_>) = val
+            .into_iter()
+            .map(|(k, v)| (k.into(), v.into_robj()))
+            .unzip();
+        let mut res: Self = Self::from_values(values);
+        res.set_names(names)?;
         Ok(res)
     }
 
@@ -175,7 +180,7 @@ impl List {
         })
     }
 
-    /// Convert a List into a HashMap, consuming the list.
+    /// Convert a `List` into a `HashMap`, consuming the list.
     ///
     /// - If an element doesn't have a name, an empty string (i.e. `""`) will be used as the key.
     /// - If there are some duplicated names (including no name, which will be translated as `""`) of elements, only one of those will be preserved.
@@ -189,7 +194,53 @@ impl List {
     /// }
     /// ```
     pub fn into_hashmap(self) -> HashMap<&'static str, Robj> {
-        self.iter().collect::<HashMap<&str, Robj>>()
+        self.as_robj().try_into().unwrap()
+    }
+}
+
+impl<T> TryFrom<&List> for HashMap<&str, T>
+where
+    T: TryFrom<Robj, Error = error::Error>,
+{
+    type Error = Error;
+
+    fn try_from(value: &List) -> Result<Self> {
+        let value = value
+            .iter()
+            .map(|(name, value)| -> Result<(&str, T)> { value.try_into().map(|x| (name, x)) })
+            .collect::<Result<HashMap<_, _>>>()?;
+
+        Ok(value)
+    }
+}
+
+impl<T> TryFrom<&List> for HashMap<String, T>
+where
+    T: TryFrom<Robj, Error = error::Error>,
+{
+    type Error = Error;
+    fn try_from(value: &List) -> Result<Self> {
+        let value: HashMap<&str, _> = value.try_into()?;
+        Ok(value.into_iter().map(|(k, v)| (k.to_string(), v)).collect())
+    }
+}
+
+// The following is necessary because it is impossible to define `TryFrom<Robj> for &Robj` as
+// it requires returning a reference to a owned (moved) value
+
+impl TryFrom<&List> for HashMap<&str, Robj> {
+    type Error = Error;
+
+    fn try_from(value: &List) -> Result<Self> {
+        Ok(value.iter().collect())
+    }
+}
+
+impl TryFrom<&List> for HashMap<String, Robj> {
+    type Error = Error;
+    fn try_from(value: &List) -> Result<Self> {
+        let value: HashMap<&str, _> = value.try_into()?;
+        Ok(value.into_iter().map(|(k, v)| (k.to_string(), v)).collect())
     }
 }
 
diff --git a/extendr-macros/src/dataframe.rs b/extendr-macros/src/dataframe.rs
index b620ad68f..e586319c5 100644
--- a/extendr-macros/src/dataframe.rs
+++ b/extendr-macros/src/dataframe.rs
@@ -11,7 +11,7 @@ fn parse_struct(input: &DeriveInput, datastruct: &DataStruct) -> TokenStream {
     quote! {
         impl extendr_api::wrapper::IntoDataFrameRow<#structname> for Vec<#structname>
         {
-            fn into_dataframe(self) -> Result<extendr_api::wrapper::Dataframe<#structname>> {
+            fn into_dataframe(self) -> extendr_api::Result<extendr_api::wrapper::Dataframe<#structname>> {
                 #(let mut #a = Vec::with_capacity(self.len());)*
                 for val in self {
                     #(#a.push(val.#a);)*
@@ -29,7 +29,7 @@ fn parse_struct(input: &DeriveInput, datastruct: &DataStruct) -> TokenStream {
             I : ExactSizeIterator<Item=#structname>,
         {
             /// Thanks to RFC 2451, we need to wrap a generic iterator in a tuple!
-            fn into_dataframe(self) -> Result<extendr_api::wrapper::Dataframe<#structname>> {
+            fn into_dataframe(self) -> extendr_api::Result<extendr_api::wrapper::Dataframe<#structname>> {
                 #(let mut #a = Vec::with_capacity(self.0.len());)*
                 for val in self.0 {
                     #(#a.push(val.#a);)*
diff --git a/extendr-macros/src/lib.rs b/extendr-macros/src/lib.rs
index bec14776e..45d7b2720 100644
--- a/extendr-macros/src/lib.rs
+++ b/extendr-macros/src/lib.rs
@@ -339,11 +339,11 @@ pub fn impl_try_from_robj_tuples(input: TokenStream) -> TokenStream {
         TokenStream::from(quote! {
             impl<#(#types),*> TryFrom<&Robj> for (#(#types,)*)
             where
-                #(#types: for<'a> TryFrom<&'a Robj, Error = crate::error::Error>),*
+                #(#types: for<'a> TryFrom<&'a Robj, Error = extendr_api::Error>),*
             {
                 type Error = Error;
 
-                fn try_from(robj: &Robj) -> Result<Self> {
+                fn try_from(robj: &Robj) -> extendr_api::Result<Self> {
                     let list: List = robj.try_into()?;
                     if list.len() != #n {
                         return Err(Error::ExpectedLength(#n));
@@ -359,20 +359,20 @@ pub fn impl_try_from_robj_tuples(input: TokenStream) -> TokenStream {
 
             impl<#(#types),*> TryFrom<Robj> for (#(#types,)*)
             where
-                #(#types: for<'a> TryFrom<&'a Robj, Error = crate::error::Error>),* {
+                #(#types: for<'a> TryFrom<&'a Robj, Error = extendr_api::Error>),* {
                 type Error = Error;
 
-                fn try_from(robj: Robj) -> Result<Self> {
+                fn try_from(robj: Robj) -> extendr_api::Result<Self> {
                     Self::try_from(&robj)
                 }
             }
 
             impl<#(#types),*> TryFrom<&Robj> for Option<(#(#types,)*)>
             where
-            #(#types: for<'a> TryFrom<&'a Robj, Error = crate::error::Error>),*{
+            #(#types: for<'a> TryFrom<&'a Robj, Error = extendr_api::Error>),*{
                 type Error = Error;
 
-                fn try_from(robj: &Robj) -> Result<Self> {
+                fn try_from(robj: &Robj) -> extendr_api::Result<Self> {
                     if robj.is_null() || robj.is_na() {
                         Ok(None)
                     } else {
@@ -383,10 +383,10 @@ pub fn impl_try_from_robj_tuples(input: TokenStream) -> TokenStream {
 
             impl<#(#types),*> TryFrom<Robj> for Option<(#(#types,)*)>
             where
-            #(#types: for<'a> TryFrom<&'a Robj, Error = crate::error::Error>),*{
+            #(#types: for<'a> TryFrom<&'a Robj, Error = extendr_api::Error>),*{
                 type Error = Error;
 
-                fn try_from(robj: Robj) -> Result<Self> {
+                fn try_from(robj: Robj) -> extendr_api::Result<Self> {
                     Self::try_from(&robj)
                 }
             }


Test patch:
diff --git a/tests/extendrtests/R/extendr-wrappers.R b/tests/extendrtests/R/extendr-wrappers.R
index b2c0dc8e1..2b3b8b004 100644
--- a/tests/extendrtests/R/extendr-wrappers.R
+++ b/tests/extendrtests/R/extendr-wrappers.R
@@ -90,6 +90,12 @@ test_derive_into_dataframe <- function() .Call(wrap__test_derive_into_dataframe)
 
 test_into_robj_dataframe <- function() .Call(wrap__test_into_robj_dataframe)
 
+test_hm_string <- function(x) .Call(wrap__test_hm_string, x)
+
+test_hm_i32 <- function(x) .Call(wrap__test_hm_i32, x)
+
+test_hm_custom_try_from <- function(x) .Call(wrap__test_hm_custom_try_from, x)
+
 leak_arg2_try_implicit_strings <- function(`_y`, x) .Call(wrap__leak_arg2_try_implicit_strings, `_y`, x)
 
 leak_arg2_try_implicit_doubles <- function(`_y`, x) .Call(wrap__leak_arg2_try_implicit_doubles, `_y`, x)
diff --git a/tests/extendrtests/src/rust/src/hashmap.rs b/tests/extendrtests/src/rust/src/hashmap.rs
new file mode 100644
index 000000000..7c254d904
--- /dev/null
+++ b/tests/extendrtests/src/rust/src/hashmap.rs
@@ -0,0 +1,54 @@
+use extendr_api::prelude::*;
+use std::collections::HashMap;
+
+#[extendr]
+fn test_hm_string(mut x: HashMap<String, Robj>) -> List {
+    x.insert("inserted_value".to_string(), List::new(0).into());
+    List::from_hashmap(x).unwrap()
+}
+
+#[extendr]
+fn test_hm_i32(mut x: HashMap<String, i32>) -> List {
+    x.insert("inserted_value".to_string(), 314);
+    List::from_hashmap(x).unwrap()
+}
+
+struct Point {
+    x: f64,
+    y: f64,
+}
+
+impl TryFrom<Robj> for Point {
+    type Error = Error;
+
+    fn try_from(value: Robj) -> std::result::Result<Self, Self::Error> {
+        let inner_vec = Doubles::try_from(value)?;
+        let x = inner_vec[0].inner();
+        let y = inner_vec[1].inner();
+        Ok(Point { x, y })
+    }
+}
+
+impl From<Point> for Doubles {
+    fn from(value: Point) -> Self {
+        Doubles::from_values([value.x, value.y])
+    }
+}
+
+impl From<Point> for Robj {
+    fn from(value: Point) -> Self {
+        Robj::from(Doubles::from(value))
+    }
+}
+#[extendr]
+fn test_hm_custom_try_from(mut x: HashMap<&str, Point>) -> List {
+    x.insert("inserted_value", Point {x : 3.0, y: 0.1415});
+    List::from_hashmap(x).unwrap()
+}
+
+extendr_module! {
+    mod hashmap;
+    fn test_hm_string;
+    fn test_hm_i32;
+    fn test_hm_custom_try_from;
+}
diff --git a/tests/extendrtests/src/rust/src/lib.rs b/tests/extendrtests/src/rust/src/lib.rs
index 2ed7054ef..fb8c845b7 100644
--- a/tests/extendrtests/src/rust/src/lib.rs
+++ b/tests/extendrtests/src/rust/src/lib.rs
@@ -5,6 +5,7 @@ mod attributes;
 mod dataframe;
 mod externalptr;
 mod graphic_device;
+mod hashmap;
 mod matrix;
 mod memory_leaks;
 mod optional_either;
@@ -352,6 +353,7 @@ extendr_module! {
     use altrep;
     use attributes;
     use dataframe;
+    use hashmap;
     use memory_leaks;
     use optional_either;
     use optional_ndarray;
diff --git a/tests/extendrtests/tests/testthat/_snaps/macro-snapshot.md b/tests/extendrtests/tests/testthat/_snaps/macro-snapshot.md
index 97d867fd4..288611bce 100644
--- a/tests/extendrtests/tests/testthat/_snaps/macro-snapshot.md
+++ b/tests/extendrtests/tests/testthat/_snaps/macro-snapshot.md
@@ -899,7 +899,9 @@
               }
           }
           impl extendr_api::wrapper::IntoDataFrameRow<MyStruct> for Vec<MyStruct> {
-              fn into_dataframe(self) -> Result<extendr_api::wrapper::Dataframe<MyStruct>> {
+              fn into_dataframe(
+                  self,
+              ) -> extendr_api::Result<extendr_api::wrapper::Dataframe<MyStruct>> {
                   let mut x = Vec::with_capacity(self.len());
                   let mut y = Vec::with_capacity(self.len());
                   for val in self {
@@ -924,7 +926,9 @@
               I: ExactSizeIterator<Item = MyStruct>,
           {
               /// Thanks to RFC 2451, we need to wrap a generic iterator in a tuple!
-              fn into_dataframe(self) -> Result<extendr_api::wrapper::Dataframe<MyStruct>> {
+              fn into_dataframe(
+                  self,
+              ) -> extendr_api::Result<extendr_api::wrapper::Dataframe<MyStruct>> {
                   let mut x = Vec::with_capacity(self.0.len());
                   let mut y = Vec::with_capacity(self.0.len());
                   for val in self.0 {
@@ -3364,6 +3368,382 @@
               }
           }
       }
+      mod hashmap {
+          use extendr_api::prelude::*;
+          use std::collections::HashMap;
+          fn test_hm_string(mut x: HashMap<String, Robj>) -> List {
+              x.insert("inserted_value".to_string(), List::new(0).into());
+              List::from_hashmap(x).unwrap()
+          }
+          #[no_mangle]
+          #[allow(non_snake_case, clippy::not_unsafe_ptr_arg_deref)]
+          pub extern "C" fn wrap__test_hm_string(x: extendr_api::SEXP) -> extendr_api::SEXP {
+              use extendr_api::robj::*;
+              let wrap_result_state: std::result::Result<
+                  std::result::Result<extendr_api::Robj, extendr_api::Error>,
+                  Box<dyn std::any::Any + Send>,
+              > = unsafe {
+                  std::panic::catch_unwind(
+                      std::panic::AssertUnwindSafe(move || -> std::result::Result<
+                          extendr_api::Robj,
+                          extendr_api::Error,
+                      > {
+                          let _x_robj = extendr_api::robj::Robj::from_sexp(x);
+                          Ok(extendr_api::Robj::from(test_hm_string(_x_robj.try_into()?)))
+                      }),
+                  )
+              };
+              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 = ::alloc::__export::must_use({
+                          let res = ::alloc::fmt::format(
+                              format_args!("User function panicked: {0}", "test_hm_string"),
+                          );
+                          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__test_hm_string(metadata: &mut Vec<extendr_api::metadata::Func>) {
+              let args = <[_]>::into_vec(
+                  #[rustc_box]
+                  ::alloc::boxed::Box::new([
+                      extendr_api::metadata::Arg {
+                          name: "x",
+                          arg_type: "HashMap",
+                          default: None,
+                      },
+                  ]),
+              );
+              metadata
+                  .push(extendr_api::metadata::Func {
+                      doc: "",
+                      rust_name: "test_hm_string",
+                      r_name: "test_hm_string",
+                      mod_name: "test_hm_string",
+                      args: args,
+                      return_type: "List",
+                      func_ptr: wrap__test_hm_string as *const u8,
+                      hidden: false,
+                  })
+          }
+          fn test_hm_i32(mut x: HashMap<String, i32>) -> List {
+              x.insert("inserted_value".to_string(), 314);
+              List::from_hashmap(x).unwrap()
+          }
+          #[no_mangle]
+          #[allow(non_snake_case, clippy::not_unsafe_ptr_arg_deref)]
+          pub extern "C" fn wrap__test_hm_i32(x: extendr_api::SEXP) -> extendr_api::SEXP {
+              use extendr_api::robj::*;
+              let wrap_result_state: std::result::Result<
+                  std::result::Result<extendr_api::Robj, extendr_api::Error>,
+                  Box<dyn std::any::Any + Send>,
+              > = unsafe {
+                  std::panic::catch_unwind(
+                      std::panic::AssertUnwindSafe(move || -> std::result::Result<
+                          extendr_api::Robj,
+                          extendr_api::Error,
+                      > {
+                          let _x_robj = extendr_api::robj::Robj::from_sexp(x);
+                          Ok(extendr_api::Robj::from(test_hm_i32(_x_robj.try_into()?)))
+                      }),
+                  )
+              };
+              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 = ::alloc::__export::must_use({
+                          let res = ::alloc::fmt::format(
+                              format_args!("User function panicked: {0}", "test_hm_i32"),
+                          );
+                          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__test_hm_i32(metadata: &mut Vec<extendr_api::metadata::Func>) {
+              let args = <[_]>::into_vec(
+                  #[rustc_box]
+                  ::alloc::boxed::Box::new([
+                      extendr_api::metadata::Arg {
+                          name: "x",
+                          arg_type: "HashMap",
+                          default: None,
+                      },
+                  ]),
+              );
+              metadata
+                  .push(extendr_api::metadata::Func {
+                      doc: "",
+                      rust_name: "test_hm_i32",
+                      r_name: "test_hm_i32",
+                      mod_name: "test_hm_i32",
+                      args: args,
+                      return_type: "List",
+                      func_ptr: wrap__test_hm_i32 as *const u8,
+                      hidden: false,
+                  })
+          }
+          struct Point {
+              x: f64,
+              y: f64,
+          }
+          impl TryFrom<Robj> for Point {
+              type Error = Error;
+              fn try_from(value: Robj) -> std::result::Result<Self, Self::Error> {
+                  let inner_vec = Doubles::try_from(value)?;
+                  let x = inner_vec[0].inner();
+                  let y = inner_vec[1].inner();
+                  Ok(Point { x, y })
+              }
+          }
+          impl From<Point> for Doubles {
+              fn from(value: Point) -> Self {
+                  Doubles::from_values([value.x, value.y])
+              }
+          }
+          impl From<Point> for Robj {
+              fn from(value: Point) -> Self {
+                  Robj::from(Doubles::from(value))
+              }
+          }
+          fn test_hm_custom_try_from(mut x: HashMap<&str, Point>) -> List {
+              x.insert("inserted_value", Point { x: 3.0, y: 0.1415 });
+              List::from_hashmap(x).unwrap()
+          }
+          #[no_mangle]
+          #[allow(non_snake_case, clippy::not_unsafe_ptr_arg_deref)]
+          pub extern "C" fn wrap__test_hm_custom_try_from(
+              x: extendr_api::SEXP,
+          ) -> extendr_api::SEXP {
+              use extendr_api::robj::*;
+              let wrap_result_state: std::result::Result<
+                  std::result::Result<extendr_api::Robj, extendr_api::Error>,
+                  Box<dyn std::any::Any + Send>,
+              > = unsafe {
+                  std::panic::catch_unwind(
+                      std::panic::AssertUnwindSafe(move || -> std::result::Result<
+                          extendr_api::Robj,
+                          extendr_api::Error,
+                      > {
+                          let _x_robj = extendr_api::robj::Robj::from_sexp(x);
+                          Ok(
+                              extendr_api::Robj::from(
+                                  test_hm_custom_try_from(_x_robj.try_into()?),
+                              ),
+                          )
+                      }),
+                  )
+              };
+              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 = ::alloc::__export::must_use({
+                          let res = ::alloc::fmt::format(
+                              format_args!(
+                                  "User function panicked: {0}",
+                                  "test_hm_custom_try_from",
+                              ),
+                          );
+                          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__test_hm_custom_try_from(metadata: &mut Vec<extendr_api::metadata::Func>) {
+              let args = <[_]>::into_vec(
+                  #[rustc_box]
+                  ::alloc::boxed::Box::new([
+                      extendr_api::metadata::Arg {
+                          name: "x",
+                          arg_type: "HashMap",
+                          default: None,
+                      },
+                  ]),
+              );
+              metadata
+                  .push(extendr_api::metadata::Func {
+                      doc: "",
+                      rust_name: "test_hm_custom_try_from",
+                      r_name: "test_hm_custom_try_from",
+                      mod_name: "test_hm_custom_try_from",
+                      args: args,
+                      return_type: "List",
+                      func_ptr: wrap__test_hm_custom_try_from as *const u8,
+                      hidden: false,
+                  })
+          }
+          #[no_mangle]
+          #[allow(non_snake_case)]
+          pub fn get_hashmap_metadata() -> extendr_api::metadata::Metadata {
+              let mut functions = Vec::new();
+              let mut impls = Vec::new();
+              meta__test_hm_string(&mut functions);
+              meta__test_hm_i32(&mut functions);
+              meta__test_hm_custom_try_from(&mut functions);
+              functions
+                  .push(extendr_api::metadata::Func {
+                      doc: "Metadata access function.",
+                      rust_name: "get_hashmap_metadata",
+                      mod_name: "get_hashmap_metadata",
+                      r_name: "get_hashmap_metadata",
+                      args: Vec::new(),
+                      return_type: "Metadata",
+                      func_ptr: wrap__get_hashmap_metadata as *const u8,
+                      hidden: true,
+                  });
+              functions
+                  .push(extendr_api::metadata::Func {
+                      doc: "Wrapper generator.",
+                      rust_name: "make_hashmap_wrappers",
+                      mod_name: "make_hashmap_wrappers",
+                      r_name: "make_hashmap_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_hashmap_wrappers as *const u8,
+                      hidden: true,
+                  });
+              extendr_api::metadata::Metadata {
+                  name: "hashmap",
+                  functions,
+                  impls,
+              }
+          }
+          #[no_mangle]
+          #[allow(non_snake_case)]
+          pub extern "C" fn wrap__get_hashmap_metadata() -> extendr_api::SEXP {
+              use extendr_api::GetSexp;
+              unsafe { extendr_api::Robj::from(get_hashmap_metadata()).get() }
+          }
+          #[no_mangle]
+          #[allow(non_snake_case, clippy::not_unsafe_ptr_arg_deref)]
+          pub extern "C" fn wrap__make_hashmap_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>::try_from(&robj).unwrap();
+                  let robj = Robj::from_sexp(package_name_sexp);
+                  let package_name: &str = <&str>::try_from(&robj).unwrap();
+                  extendr_api::Robj::from(
+                          get_hashmap_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_hashmap_extendr(info: *mut extendr_api::DllInfo) {
+              unsafe { extendr_api::register_call_methods(info, get_hashmap_metadata()) };
+          }
+      }
       mod matrix {
           use extendr_api::prelude::*;
           fn fetch_dimnames(x: RMatrix<f64>) -> List {
@@ -11747,6 +12127,7 @@
           functions.extend(altrep::get_altrep_metadata().functions);
           functions.extend(attributes::get_attributes_metadata().functions);
           functions.extend(dataframe::get_dataframe_metadata().functions);
+          functions.extend(hashmap::get_hashmap_metadata().functions);
           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);
@@ -11761,6 +12142,7 @@
           impls.extend(altrep::get_altrep_metadata().impls);
           impls.extend(attributes::get_attributes_metadata().impls);
           impls.extend(dataframe::get_dataframe_metadata().impls);
+          impls.extend(hashmap::get_hashmap_metadata().impls);
           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);
diff --git a/tests/extendrtests/tests/testthat/test-try-from-hashmap.R b/tests/extendrtests/tests/testthat/test-try-from-hashmap.R
new file mode 100644
index 000000000..159583981
--- /dev/null
+++ b/tests/extendrtests/tests/testthat/test-try-from-hashmap.R
@@ -0,0 +1,21 @@
+test_that("String as HashMap key & Robj T", {
+  expect_equal(
+    test_hm_string(list(x = 10))[c("inserted_value", "x")],
+    list(inserted_value = list(), x = 10)
+  )
+})
+
+test_that("String as Key and i32 as T", {
+  expect_identical(
+    test_hm_i32(list()), 
+    list(inserted_value = 314L)
+  )
+})
+
+test_that("HashMap with custom TryFrom<Robj> impl", {
+  expect_equal(
+    test_hm_custom_try_from(list(x = c(0, 1)))[c("x", "inserted_value")],
+    list(x = c(0, 1), inserted_value = c(3, 0.1415))
+  )
+})
+