Support for template specializations used outside of type aliases.

Before this CL, Crubit would generate bindings for template
instantiations used in type aliases: `using MyTypeAlias =
MyTemplate<int>`.  Template instantiations used elsewhere would
result in emitting an `UnsupportedItem`.

This CL moves that call to `Importer::ConvertTemplateSpecializationType`
from `TypedefNameDeclImporter::Import` to `Importer::ConvertType`.
After this CL, Crubit supports template specializations used in such
places as 1) function return types, 2) function parameter types, 3)
private and public fields of structs, etc.

PiperOrigin-RevId: 452551994
diff --git a/rs_bindings_from_cc/decl_importer.h b/rs_bindings_from_cc/decl_importer.h
index 27982a9..3641529 100644
--- a/rs_bindings_from_cc/decl_importer.h
+++ b/rs_bindings_from_cc/decl_importer.h
@@ -136,12 +136,7 @@
   virtual absl::StatusOr<MappedType> ConvertQualType(
       clang::QualType qual_type,
       std::optional<clang::tidy::lifetimes::ValueLifetimes>& lifetimes,
-      bool nullable = true) const = 0;
-
-  // Converts `type` into a MappedType, after first importing the Record behind
-  // the template instantiation.
-  virtual absl::StatusOr<MappedType> ConvertTemplateSpecializationType(
-      const clang::TemplateSpecializationType* type) = 0;
+      bool nullable = true) = 0;
 
   // Marks `decl` as successfully imported.  Other pieces of code can check
   // HasBeenAlreadySuccessfullyImported to avoid introducing dangling ItemIds
diff --git a/rs_bindings_from_cc/importer.cc b/rs_bindings_from_cc/importer.cc
index 6584556..ad48d3f 100644
--- a/rs_bindings_from_cc/importer.cc
+++ b/rs_bindings_from_cc/importer.cc
@@ -636,7 +636,7 @@
 absl::StatusOr<MappedType> Importer::ConvertType(
     const clang::Type* type,
     std::optional<clang::tidy::lifetimes::ValueLifetimes>& lifetimes,
-    bool nullable) const {
+    bool nullable) {
   // Qualifiers are handled separately in ConvertQualType().
   std::string type_string = clang::QualType(type, 0).getAsString();
 
@@ -741,6 +741,9 @@
   } else if (const auto* typedef_type =
                  type->getAsAdjusted<clang::TypedefType>()) {
     return ConvertTypeDecl(typedef_type->getDecl());
+  } else if (const auto* tst_type =
+                 type->getAs<clang::TemplateSpecializationType>()) {
+    return ConvertTemplateSpecializationType(tst_type);
   } else if (const auto* subst_type =
                  type->getAs<clang::SubstTemplateTypeParmType>()) {
     return ConvertQualType(subst_type->getReplacementType(), lifetimes);
@@ -758,7 +761,7 @@
 absl::StatusOr<MappedType> Importer::ConvertQualType(
     clang::QualType qual_type,
     std::optional<clang::tidy::lifetimes::ValueLifetimes>& lifetimes,
-    bool nullable) const {
+    bool nullable) {
   std::string type_string = qual_type.getAsString();
   absl::StatusOr<MappedType> type =
       ConvertType(qual_type.getTypePtr(), lifetimes, nullable);
diff --git a/rs_bindings_from_cc/importer.h b/rs_bindings_from_cc/importer.h
index cee33f7..bc495ac 100644
--- a/rs_bindings_from_cc/importer.h
+++ b/rs_bindings_from_cc/importer.h
@@ -76,9 +76,7 @@
   absl::StatusOr<MappedType> ConvertQualType(
       clang::QualType qual_type,
       std::optional<clang::tidy::lifetimes::ValueLifetimes>& lifetimes,
-      bool nullable = true) const override;
-  absl::StatusOr<MappedType> ConvertTemplateSpecializationType(
-      const clang::TemplateSpecializationType* type) override;
+      bool nullable = true) override;
   void MarkAsSuccessfullyImported(const clang::TypeDecl* decl) override;
   bool HasBeenAlreadySuccessfullyImported(
       const clang::TypeDecl* decl) const override;
@@ -102,9 +100,14 @@
   absl::StatusOr<MappedType> ConvertType(
       const clang::Type* type,
       std::optional<clang::tidy::lifetimes::ValueLifetimes>& lifetimes,
-      bool nullable) const;
+      bool nullable);
   absl::StatusOr<MappedType> ConvertTypeDecl(const clang::TypeDecl* decl) const;
 
+  // Converts `type` into a MappedType, after first importing the Record behind
+  // the template instantiation.
+  absl::StatusOr<MappedType> ConvertTemplateSpecializationType(
+      const clang::TemplateSpecializationType* type);
+
   std::vector<std::unique_ptr<DeclImporter>> decl_importers_;
   std::unique_ptr<clang::MangleContext> mangler_;
   absl::flat_hash_map<const clang::Decl*, std::optional<IR::Item>>
diff --git a/rs_bindings_from_cc/importers/typedef_name.cc b/rs_bindings_from_cc/importers/typedef_name.cc
index e0f39b8..d055764 100644
--- a/rs_bindings_from_cc/importers/typedef_name.cc
+++ b/rs_bindings_from_cc/importers/typedef_name.cc
@@ -32,17 +32,9 @@
       ictx_.GetTranslatedIdentifier(typedef_name_decl);
   CRUBIT_CHECK(identifier.has_value());  // This should never happen.
 
-  absl::StatusOr<MappedType> underlying_type;
-  // TODO(b/228868369): Move this "if" into the generic TypeMapper::ConvertType.
-  // This will extend support for template instantiations outside type aliases.
-  if (const auto* tst_type = typedef_name_decl->getUnderlyingType()
-                                 ->getAs<clang::TemplateSpecializationType>()) {
-    underlying_type = ictx_.ConvertTemplateSpecializationType(tst_type);
-  } else {
-    std::optional<clang::tidy::lifetimes::ValueLifetimes> no_lifetimes;
-    underlying_type = ictx_.ConvertQualType(
-        typedef_name_decl->getUnderlyingType(), no_lifetimes);
-  }
+  std::optional<clang::tidy::lifetimes::ValueLifetimes> no_lifetimes;
+  absl::StatusOr<MappedType> underlying_type = ictx_.ConvertQualType(
+      typedef_name_decl->getUnderlyingType(), no_lifetimes);
   if (underlying_type.ok()) {
     if (const auto* tag_decl = type->getAsTagDecl();
         tag_decl && tag_decl->getDeclContext() == decl_context &&
diff --git a/rs_bindings_from_cc/ir.rs b/rs_bindings_from_cc/ir.rs
index 4b003f6..18f5831 100644
--- a/rs_bindings_from_cc/ir.rs
+++ b/rs_bindings_from_cc/ir.rs
@@ -218,7 +218,19 @@
 
 #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Deserialize)]
 #[serde(transparent)]
-pub struct ItemId(pub usize);
+pub struct ItemId(usize);
+
+impl ItemId {
+    pub fn new_for_testing(value: usize) -> Self {
+        Self(value)
+    }
+}
+
+impl ToTokens for ItemId {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        proc_macro2::Literal::usize_unsuffixed(self.0).to_tokens(tokens)
+    }
+}
 
 #[derive(Debug, PartialEq, Eq, Hash, Clone, Deserialize)]
 #[serde(transparent)]
diff --git a/rs_bindings_from_cc/ir_from_cc_test.rs b/rs_bindings_from_cc/ir_from_cc_test.rs
index 9bf2a2c..69adc99 100644
--- a/rs_bindings_from_cc/ir_from_cc_test.rs
+++ b/rs_bindings_from_cc/ir_from_cc_test.rs
@@ -907,10 +907,10 @@
           }
         }
     );
+    let record_id = retrieve_record(&ir, "MyStruct<int>").id;
     // Make sure the instantiation of the class template appears exactly once in the
     // `top_level_item_ids`.
-    let record = ir.records().find(|r| r.cc_name == "MyStruct<int>").unwrap();
-    assert_eq!(1, ir.top_level_item_ids().filter(|&&id| id == record.id).count());
+    assert_eq!(1, ir.top_level_item_ids().filter(|&&id| id == record_id).count());
     // Type alias for the class template specialization.
     assert_ir_matches!(
         ir,
@@ -924,13 +924,13 @@
                     name: None,
                     lifetime_args: [],
                     type_args: [],
-                    decl_id: Some(ItemId(...)),
+                    decl_id: Some(ItemId(#record_id)),
                 },
                 cc_type: CcType {
                     name: None,
                     is_const: false,
                     type_args: [],
-                    decl_id: Some(ItemId(...)),
+                    decl_id: Some(ItemId(#record_id)),
                 },
             } ...
           }
@@ -947,7 +947,7 @@
             doc_comment: Some("Doc comment of GetValue method."), ...
             is_inline: true, ...
             member_func_metadata: Some(MemberFuncMetadata {
-                record_id: ItemId(...),
+                record_id: ItemId(#record_id),
                 instance_method_metadata: Some(InstanceMethodMetadata { ... }), ...
             }), ...
           }
@@ -992,10 +992,6 @@
             // Doc comment of MyTypeAlias.
             using MyTypeAlias = MyStruct<int>; "#,
     )?;
-    // Make sure the explicit specialization of the struct template appears exactly
-    // once in the `top_level_item_ids`.
-    let record = ir.records().find(|r| r.cc_name == "MyStruct<int>").unwrap();
-    assert_eq!(1, ir.top_level_item_ids().filter(|&&id| id == record.id).count());
     // Instantiation of the struct template based on the specialization for T=int:
     assert_ir_matches!(
         ir,
@@ -1018,6 +1014,10 @@
           }
         }
     );
+    let record_id = retrieve_record(&ir, "MyStruct<int>").id;
+    // Make sure the explicit specialization of the struct template appears exactly
+    // once in the `top_level_item_ids`.
+    assert_eq!(1, ir.top_level_item_ids().filter(|&&id| id == record_id).count());
     // Instance method inside the struct template:
     assert_ir_matches!(
         ir,
@@ -1029,7 +1029,7 @@
             doc_comment: Some("Doc comment of the GetValue method specialization for T=int."), ...
             is_inline: true, ...
             member_func_metadata: Some(MemberFuncMetadata {
-                record_id: ItemId(...),
+                record_id: ItemId(#record_id),
                 instance_method_metadata: Some(InstanceMethodMetadata { ... }), ...
             }), ...
           }
@@ -1231,7 +1231,187 @@
 }
 
 #[test]
-fn test_no_instantiation_of_template_only_used_in_private_field() -> Result<()> {
+fn test_fully_instantiated_template_in_function_return_type() -> Result<()> {
+    let ir = ir_from_cc(
+        r#" #pragma clang lifetime_elision
+
+            template <typename T>
+            struct MyStruct { T value; };
+
+            MyStruct<int> MyFunction(); "#,
+    )?;
+    // Instantiation of the struct template:
+    assert_ir_matches!(
+        ir,
+        quote! {
+          Record {
+            rs_name: "__CcTemplateInst8MyStructIiE", ...
+            cc_name: "MyStruct<int>", ...
+            owning_target: BazelLabel("//test:testing_target"), ...
+          }
+        }
+    );
+    let record_id = retrieve_record(&ir, "MyStruct<int>").id;
+    // Function that used the class template as a return type.
+    assert_ir_matches!(
+        ir,
+        quote! {
+          Func {
+            name: "MyFunction",
+            owning_target: BazelLabel("//test:testing_target"), ...
+            return_type: MappedType {
+                rs_type: RsType {
+                    name: None,
+                    lifetime_args: [],
+                    type_args: [],
+                    decl_id: Some(ItemId(#record_id)),
+                },
+                cc_type: CcType {
+                    name: None,
+                    is_const: false,
+                    type_args: [],
+                    decl_id: Some(ItemId(#record_id)),
+                },
+            }, ...
+            params: [], ...
+            is_inline: false, ...
+            member_func_metadata: None, ...
+            has_c_calling_convention: true, ...
+            is_member_or_descendant_of_class_template: false, ...
+          }
+        }
+    );
+    Ok(())
+}
+
+#[test]
+fn test_fully_instantiated_template_in_function_param_type() -> Result<()> {
+    let ir = ir_from_cc(
+        r#" #pragma clang lifetime_elision
+
+            template <typename T>
+            struct MyStruct { T value; };
+
+            void MyFunction(const MyStruct<int>& my_param); "#,
+    )?;
+    // Instantiation of the struct template:
+    assert_ir_matches!(
+        ir,
+        quote! {
+          Record {
+            rs_name: "__CcTemplateInst8MyStructIiE", ...
+            cc_name: "MyStruct<int>", ...
+            owning_target: BazelLabel("//test:testing_target"), ...
+          }
+        }
+    );
+    let record_id = retrieve_record(&ir, "MyStruct<int>").id;
+    // Function that used the class template as a param type:
+    assert_ir_matches!(
+        ir,
+        quote! {
+          Func {
+            name: "MyFunction",
+            owning_target: BazelLabel("//test:testing_target"), ...
+            params: [FuncParam {
+                type_: MappedType {
+                    rs_type: RsType {
+                        name: Some("&"),
+                        lifetime_args: [LifetimeId(...)],
+                        type_args: [RsType {
+                            name: None,
+                            lifetime_args: [],
+                            type_args: [],
+                            decl_id: Some(ItemId(#record_id)),
+                        }],
+                        decl_id: None,
+                    },
+                    cc_type: CcType {
+                        name: Some("&"),
+                        is_const: false,
+                        type_args: [CcType {
+                            name: None,
+                            is_const: true,
+                            type_args: [],
+                            decl_id: Some(ItemId(#record_id)),
+                        }],
+                        decl_id: None,
+                    },
+                },
+                identifier: "my_param",
+            }], ...
+            is_inline: false, ...
+            member_func_metadata: None, ...
+            has_c_calling_convention: true, ...
+            is_member_or_descendant_of_class_template: false, ...
+          }
+        }
+    );
+    Ok(())
+}
+
+#[test]
+fn test_fully_instantiated_template_in_public_field() -> Result<()> {
+    let ir = ir_from_cc(
+        r#" #pragma clang lifetime_elision
+            template <typename T>
+            struct MyTemplate { T field; };
+
+            class MyStruct {
+             public:
+              MyTemplate<int> public_field;
+            }; "#,
+    )?;
+    // Instantiation of the struct template:
+    assert_ir_matches!(
+        ir,
+        quote! {
+          Record {
+            rs_name: "__CcTemplateInst10MyTemplateIiE", ...
+            cc_name: "MyTemplate<int>", ...
+            owning_target: BazelLabel("//test:testing_target"), ...
+          }
+        }
+    );
+    let record_id = retrieve_record(&ir, "MyTemplate<int>").id;
+    // Struct that used the class template as a type of a public field:
+    assert_ir_matches!(
+        ir,
+        quote! {
+               Record {
+                   rs_name: "MyStruct",
+                   cc_name: "MyStruct", ...
+                   owning_target: BazelLabel("//test:testing_target"), ...
+                   fields: [Field {
+                       identifier: Some("public_field"), ...
+                       type_: Ok(MappedType {
+                           rs_type: RsType {
+                               name: None,
+                               lifetime_args: [],
+                               type_args: [],
+                               decl_id: Some(ItemId(#record_id)),
+                           },
+                           cc_type: CcType {
+                               name: None,
+                               is_const: false,
+                               type_args: [],
+                               decl_id: Some(ItemId(#record_id)),
+                           },
+                       }),
+                       access: Public,
+                       offset: 0,
+                       size: 32,
+                       is_no_unique_address: false,
+                       is_bitfield: false,
+                   }], ...
+               }
+        }
+    );
+    Ok(())
+}
+
+#[test]
+fn test_fully_instantiated_template_in_private_field() -> Result<()> {
     let ir = ir_from_cc(
         r#" #pragma clang lifetime_elision
             template <typename T>
@@ -1245,7 +1425,11 @@
     // There should be no instantiated template, just because of the private field.
     // To some extent this test is an early enforcement of the long-term plan for
     // b/226580208 and <internal link>.
-    assert_ir_not_matches!(ir, quote! { "field" });
+    //
+    // TODO(b/228868369): All private fields should be emitted as opaque blobs of bytes.
+    // After this is fixed, we should change the undesired test assertion below to the
+    // desirable `assert_ir_not_matches`.
+    assert_ir_matches!(ir, quote! { "field" });
     Ok(())
 }
 
@@ -2531,14 +2715,14 @@
     let ir = ir_from_cc("struct SomeStruct { struct NestedStruct {}; };").unwrap();
     let unsupported =
         ir.unsupported_items().find(|i| i.name == "SomeStruct::NestedStruct").unwrap();
-    assert_ne!(unsupported.id, ItemId(0));
+    assert_ne!(unsupported.id, ItemId::new_for_testing(0));
 }
 
 #[test]
 fn test_comment_has_item_id() {
     let ir = ir_from_cc("// Comment").unwrap();
     let comment = ir.comments().find(|i| i.text == "Comment").unwrap();
-    assert_ne!(comment.id, ItemId(0));
+    assert_ne!(comment.id, ItemId::new_for_testing(0));
 }
 
 #[test]
@@ -2546,7 +2730,7 @@
     let ir = ir_from_cc("int foo();").unwrap();
     let function =
         ir.functions().find(|i| i.name == UnqualifiedIdentifier::Identifier(ir_id("foo"))).unwrap();
-    assert_ne!(function.id, ItemId(0));
+    assert_ne!(function.id, ItemId::new_for_testing(0));
 }
 
 #[test]
@@ -3010,7 +3194,7 @@
     }"#,
     )
     .unwrap();
-    let item_id = proc_macro2::Literal::usize_unsuffixed(ir.top_level_item_ids().next().unwrap().0);
+    let item_id = ir.top_level_item_ids().next().unwrap();
 
     assert_ir_matches!(
         ir,
diff --git a/rs_bindings_from_cc/ir_testing.rs b/rs_bindings_from_cc/ir_testing.rs
index 3549ab3..ff36e3e 100644
--- a/rs_bindings_from_cc/ir_testing.rs
+++ b/rs_bindings_from_cc/ir_testing.rs
@@ -81,12 +81,21 @@
 /// Retrieves the function with the given name.
 /// Panics if no such function could be found.
 pub fn retrieve_func<'a>(ir: &'a IR, name: &str) -> &'a Func {
-    for item in ir.items() {
-        if let Item::Func(func) = item {
-            if func.name == ir::UnqualifiedIdentifier::Identifier(ir_id(name)) {
-                return func;
-            }
+    for func in ir.functions() {
+        if func.name == ir::UnqualifiedIdentifier::Identifier(ir_id(name)) {
+            return func;
         }
     }
     panic!("Didn't find function with name {}", name);
 }
+
+/// Retrieves the `Record` with the given name.
+/// Panics if no such record could be found.
+pub fn retrieve_record<'a>(ir: &'a IR, cc_name: &str) -> &'a Record {
+    for record in ir.records() {
+        if &record.cc_name == cc_name {
+            return record;
+        }
+    }
+    panic!("Didn't find record with cc_name {}", cc_name);
+}
diff --git a/rs_bindings_from_cc/src_code_gen.rs b/rs_bindings_from_cc/src_code_gen.rs
index 34f04c5..0880d1b 100644
--- a/rs_bindings_from_cc/src_code_gen.rs
+++ b/rs_bindings_from_cc/src_code_gen.rs
@@ -2654,9 +2654,9 @@
     // `ir_testing`.
     fn test_duplicate_decl_ids_err() {
         let mut r1 = ir_record("R1");
-        r1.id = ItemId(42);
+        r1.id = ItemId::new_for_testing(42);
         let mut r2 = ir_record("R2");
-        r2.id = ItemId(42);
+        r2.id = ItemId::new_for_testing(42);
         let result = make_ir_from_items([r1.into(), r2.into()]);
         assert!(result.is_err());
         assert!(result.unwrap_err().to_string().contains("Duplicate decl_id found in"));
diff --git a/rs_bindings_from_cc/test/templates/func_return_and_param_types/BUILD b/rs_bindings_from_cc/test/templates/func_return_and_param_types/BUILD
new file mode 100644
index 0000000..f14f6f1
--- /dev/null
+++ b/rs_bindings_from_cc/test/templates/func_return_and_param_types/BUILD
@@ -0,0 +1,17 @@
+"""End-to-end example of using fully-instantiated templates as function return types."""
+
+load("@rules_rust//rust:defs.bzl", "rust_test")
+
+licenses(["notice"])
+
+cc_library(
+    name = "func_return_and_param_types",
+    srcs = ["func_return_and_param_types.cc"],
+    hdrs = ["func_return_and_param_types.h"],
+)
+
+rust_test(
+    name = "main",
+    srcs = ["test.rs"],
+    cc_deps = [":func_return_and_param_types"],
+)
diff --git a/rs_bindings_from_cc/test/templates/func_return_and_param_types/func_return_and_param_types.cc b/rs_bindings_from_cc/test/templates/func_return_and_param_types/func_return_and_param_types.cc
new file mode 100644
index 0000000..c74ca30
--- /dev/null
+++ b/rs_bindings_from_cc/test/templates/func_return_and_param_types/func_return_and_param_types.cc
@@ -0,0 +1,13 @@
+// Part of the Crubit project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#include "rs_bindings_from_cc/test/templates/func_return_and_param_types/func_return_and_param_types.h"
+
+MyTemplate<int> CreateInstanceOfMyTemplate(int value) {
+  return MyTemplate<int>::Create(value);
+}
+
+int DoubleInstanceOfMyTemplate(const MyTemplate<int>& my_template) {
+  return my_template.value() * 2;
+}
diff --git a/rs_bindings_from_cc/test/templates/func_return_and_param_types/func_return_and_param_types.h b/rs_bindings_from_cc/test/templates/func_return_and_param_types/func_return_and_param_types.h
new file mode 100644
index 0000000..eaccb95
--- /dev/null
+++ b/rs_bindings_from_cc/test/templates/func_return_and_param_types/func_return_and_param_types.h
@@ -0,0 +1,29 @@
+// Part of the Crubit project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#ifndef THIRD_PARTY_CRUBIT_RS_BINDINGS_FROM_CC_TEST_TEMPLATES_FUNC_RETURN_AND_PARAM_TYPES_FUNC_RETURN_AND_PARAM_TYPES_H_
+#define THIRD_PARTY_CRUBIT_RS_BINDINGS_FROM_CC_TEST_TEMPLATES_FUNC_RETURN_AND_PARAM_TYPES_FUNC_RETURN_AND_PARAM_TYPES_H_
+
+#pragma clang lifetime_elision
+
+template <typename T>
+class MyTemplate {
+ public:
+  static MyTemplate Create(T value) {
+    MyTemplate result;
+    result.value_ = value;
+    return result;
+  }
+
+  const T& value() const { return value_; }
+
+ private:
+  T value_;
+};
+
+MyTemplate<int> CreateInstanceOfMyTemplate(int value);
+
+int DoubleInstanceOfMyTemplate(const MyTemplate<int>& my_template);
+
+#endif  // THIRD_PARTY_CRUBIT_RS_BINDINGS_FROM_CC_TEST_TEMPLATES_FUNC_RETURN_AND_PARAM_TYPES_FUNC_RETURN_AND_PARAM_TYPES_H_
diff --git a/rs_bindings_from_cc/test/templates/func_return_and_param_types/test.rs b/rs_bindings_from_cc/test/templates/func_return_and_param_types/test.rs
new file mode 100644
index 0000000..8e5e3eb
--- /dev/null
+++ b/rs_bindings_from_cc/test/templates/func_return_and_param_types/test.rs
@@ -0,0 +1,25 @@
+// Part of the Crubit project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#[cfg(test)]
+mod tests {
+    use func_return_and_param_types::*;
+
+    // This tests whether Crubit supports template specialization/instantiation in a
+    // function return type, or in a function parameter type - see b/228868369.
+    #[test]
+    fn test_template_instantiation_in_return_value_and_parameter_type() {
+        // Note that the Rust code below never needs to refer to the
+        // mangled name of the Rust struct that the class template
+        // specialization/instantiation gets translated to.
+
+        // Class template instantiation used as a function return type.
+        let s = CreateInstanceOfMyTemplate(123);
+        assert_eq!(123, *s.value());
+
+        // Const-ref to class template instantiation used as a function parameter type.
+        let d = DoubleInstanceOfMyTemplate(&s);
+        assert_eq!(123 * 2, d);
+    }
+}
diff --git a/rs_bindings_from_cc/test/templates/struct_fields/BUILD b/rs_bindings_from_cc/test/templates/struct_fields/BUILD
new file mode 100644
index 0000000..8717b03
--- /dev/null
+++ b/rs_bindings_from_cc/test/templates/struct_fields/BUILD
@@ -0,0 +1,17 @@
+"""End-to-end example of using fully-instantiated templates as function return types."""
+
+load("@rules_rust//rust:defs.bzl", "rust_test")
+
+licenses(["notice"])
+
+cc_library(
+    name = "struct_fields",
+    hdrs = ["struct_fields.h"],
+)
+
+rust_test(
+    name = "main",
+    srcs = ["test.rs"],
+    cc_deps = [":struct_fields"],
+    deps = ["//rs_bindings_from_cc/support:ctor"],
+)
diff --git a/rs_bindings_from_cc/test/templates/struct_fields/struct_fields.h b/rs_bindings_from_cc/test/templates/struct_fields/struct_fields.h
new file mode 100644
index 0000000..3dc03c0
--- /dev/null
+++ b/rs_bindings_from_cc/test/templates/struct_fields/struct_fields.h
@@ -0,0 +1,25 @@
+// Part of the Crubit project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#ifndef THIRD_PARTY_CRUBIT_RS_BINDINGS_FROM_CC_TEST_TEMPLATES_STRUCT_FIELDS_STRUCT_FIELDS_H_
+#define THIRD_PARTY_CRUBIT_RS_BINDINGS_FROM_CC_TEST_TEMPLATES_STRUCT_FIELDS_STRUCT_FIELDS_H_
+
+#pragma clang lifetime_elision
+
+template <typename T>
+class MyTemplate {
+ public:
+  explicit MyTemplate(T value) : value_(value) {}
+  const T& value() const { return value_; }
+
+ private:
+  T value_;
+};
+
+struct MyStruct {
+  MyStruct(int i) : public_field(i) {}
+  MyTemplate<int> public_field;
+};
+
+#endif  // THIRD_PARTY_CRUBIT_RS_BINDINGS_FROM_CC_TEST_TEMPLATES_STRUCT_FIELDS_STRUCT_FIELDS_H_
diff --git a/rs_bindings_from_cc/test/templates/struct_fields/test.rs b/rs_bindings_from_cc/test/templates/struct_fields/test.rs
new file mode 100644
index 0000000..1179827
--- /dev/null
+++ b/rs_bindings_from_cc/test/templates/struct_fields/test.rs
@@ -0,0 +1,24 @@
+// Part of the Crubit project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#[cfg(test)]
+mod tests {
+    use ctor::CtorNew as _;
+    use struct_fields::*;
+
+    // This tests whether Crubit supports template specialization/instantiation in a
+    // struct field - see b/228868369.
+    #[test]
+    fn test_template_instantiation_in_return_value_and_parameter_type() {
+        // Note that the Rust code below never needs to refer to the
+        // mangled name of the Rust struct that the class template
+        // specialization/instantiation gets translated to.
+
+        // Class template instantiation used as a type of a public field.
+        ctor::emplace! {
+            let s = MyStruct::ctor_new(123);
+        }
+        assert_eq!(123, *s.public_field.value());
+    }
+}