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());
+ }
+}