Type aliases bound to fully-instantiated template

PiperOrigin-RevId: 449002160
diff --git a/rs_bindings_from_cc/BUILD b/rs_bindings_from_cc/BUILD
index 7468975..db38b32 100644
--- a/rs_bindings_from_cc/BUILD
+++ b/rs_bindings_from_cc/BUILD
@@ -108,9 +108,24 @@
 )
 
 cc_library(
+    name = "ast_util",
+    srcs = ["ast_util.cc"],
+    hdrs = ["ast_util.h"],
+    visibility = ["//third_party/crubit:__subpackages__"],
+    deps = [
+        "//common:check",
+        "@llvm///clang:ast",
+    ],
+)
+
+cc_library(
     name = "bazel_types",
+    srcs = ["bazel_types.cc"],
     hdrs = ["bazel_types.h"],
-    deps = ["//common:string_type"],
+    deps = [
+        "//common:string_type",
+        "@absl//strings",
+    ],
 )
 
 cc_library(
@@ -149,6 +164,7 @@
         "//lifetime_annotations",
         "@absl//container:flat_hash_map",
         "@absl//status:statusor",
+        "@llvm///clang:ast",
     ],
 )
 
@@ -189,6 +205,7 @@
         "//common:check",
         "//common:status_macros",
         "//lifetime_annotations:type_lifetimes",
+        "//rs_bindings_from_cc:ast_util",
         "//rs_bindings_from_cc/importers:class_template",
         "//rs_bindings_from_cc/importers:cxx_record",
         "//rs_bindings_from_cc/importers:enum",
diff --git a/rs_bindings_from_cc/ast_util.cc b/rs_bindings_from_cc/ast_util.cc
new file mode 100644
index 0000000..93fce47
--- /dev/null
+++ b/rs_bindings_from_cc/ast_util.cc
@@ -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
+
+#include "rs_bindings_from_cc/ast_util.h"
+
+#include "common/check.h"
+#include "clang/AST/DeclCXX.h"
+#include "clang/AST/DeclTemplate.h"
+
+namespace crubit {
+
+bool IsFullClassTemplateSpecializationOrChild(const clang::Decl* decl) {
+  if (clang::isa<clang::ClassTemplatePartialSpecializationDecl>(decl)) {
+    return false;
+  }
+  if (clang::isa<clang::ClassTemplateSpecializationDecl>(decl)) {
+    return true;
+  }
+
+  if (const auto* decl_context = decl->getDeclContext()) {
+    return IsFullClassTemplateSpecializationOrChild(
+        clang::dyn_cast<clang::Decl>(decl_context));
+  }
+
+  return false;
+}
+
+}  // namespace crubit
diff --git a/rs_bindings_from_cc/ast_util.h b/rs_bindings_from_cc/ast_util.h
new file mode 100644
index 0000000..576c9b2
--- /dev/null
+++ b/rs_bindings_from_cc/ast_util.h
@@ -0,0 +1,19 @@
+// 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 CRUBIT_RS_BINDINGS_FROM_CC_AST_UTIL_H_
+#define CRUBIT_RS_BINDINGS_FROM_CC_AST_UTIL_H_
+
+#include "clang/AST/DeclCXX.h"
+
+namespace crubit {
+
+// Returns true if `decl` is either 1) a ClassTemplateSpecializationDecl (but
+// not ClassTemplatePartialSpecializationDecl) or 2) a decl (e.g. a member
+// function decl) nested inside a ClassTemplateSpecializationDecl.
+bool IsFullClassTemplateSpecializationOrChild(const clang::Decl* decl);
+
+}  // namespace crubit
+
+#endif  // CRUBIT_RS_BINDINGS_FROM_CC_AST_UTIL_H_
diff --git a/rs_bindings_from_cc/bazel_types.cc b/rs_bindings_from_cc/bazel_types.cc
new file mode 100644
index 0000000..fa4cde8
--- /dev/null
+++ b/rs_bindings_from_cc/bazel_types.cc
@@ -0,0 +1,31 @@
+// 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/bazel_types.h"
+
+#include "absl/strings/ascii.h"
+
+namespace crubit {
+
+std::string ConvertToCcIdentifier(const BazelLabel& target) {
+  std::string result = target.value();
+
+  // TODO(b/222001243): The escaping below can arrive at the same result for 2
+  // distinct targets like //foo/bar:baz and //foo_bar:baz.  In the long-term
+  // this should be fixed, or alternatively ConvertToCcIdentifier should be
+  // removed (the latter is the current plan of record - see also "Handling
+  // thunks" section in <internal link>).
+  for (char& c : result) {
+    if (!absl::ascii_isalnum(c)) {
+      c = '_';
+    }
+  }
+  if (!result.empty() && !absl::ascii_isalpha(result[0])) {
+    result[0] = '_';
+  }
+
+  return result;
+}
+
+}  // namespace crubit
diff --git a/rs_bindings_from_cc/bazel_types.h b/rs_bindings_from_cc/bazel_types.h
index 1c87ae1..8d6d2fc 100644
--- a/rs_bindings_from_cc/bazel_types.h
+++ b/rs_bindings_from_cc/bazel_types.h
@@ -14,6 +14,10 @@
 // Representation of a Bazel label (for example //foo/bar:baz).
 CRUBIT_DEFINE_STRING_TYPE(BazelLabel);
 
+// Coverts the argument to a valid C++ identifier (e.g. replacing characters
+// like ":" or "/" with "_").
+std::string ConvertToCcIdentifier(const BazelLabel&);
+
 }  // namespace crubit
 
 #endif  // CRUBIT_RS_BINDINGS_FROM_CC_BAZEL_TYPES_H_
diff --git a/rs_bindings_from_cc/decl_importer.h b/rs_bindings_from_cc/decl_importer.h
index c1e8809..8508ccd 100644
--- a/rs_bindings_from_cc/decl_importer.h
+++ b/rs_bindings_from_cc/decl_importer.h
@@ -10,6 +10,7 @@
 #include "lifetime_annotations/lifetime_annotations.h"
 #include "rs_bindings_from_cc/bazel_types.h"
 #include "rs_bindings_from_cc/ir.h"
+#include "clang/AST/DeclTemplate.h"
 
 namespace crubit {
 
@@ -163,6 +164,11 @@
   // Converts a Clang source location to IR.
   virtual SourceLoc ConvertSourceLocation(clang::SourceLocation loc) 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;
+
   Invocation& invocation_;
   clang::ASTContext& ctx_;
   clang::Sema& sema_;
diff --git a/rs_bindings_from_cc/importer.cc b/rs_bindings_from_cc/importer.cc
index 9713fae..49c30cb 100644
--- a/rs_bindings_from_cc/importer.cc
+++ b/rs_bindings_from_cc/importer.cc
@@ -28,6 +28,7 @@
 #include "common/check.h"
 #include "common/status_macros.h"
 #include "lifetime_annotations/type_lifetimes.h"
+#include "rs_bindings_from_cc/ast_util.h"
 #include "rs_bindings_from_cc/bazel_types.h"
 #include "rs_bindings_from_cc/ir.h"
 #include "clang/AST/ASTContext.h"
@@ -41,6 +42,7 @@
 #include "clang/Basic/SourceLocation.h"
 #include "clang/Basic/SourceManager.h"
 #include "clang/Basic/Specifiers.h"
+#include "clang/Sema/Sema.h"
 #include "llvm/ADT/Optional.h"
 #include "llvm/ADT/STLExtras.h"
 #include "llvm/Support/Casting.h"
@@ -52,6 +54,17 @@
 constexpr absl::string_view kTypeStatusPayloadUrl =
     "type.googleapis.com/devtools.rust.cc_interop.rs_binding_from_cc.type";
 
+// Checks if the return value from `GetDeclItem` indicates that the import was
+// successful.
+absl::Status CheckImportStatus(const std::optional<IR::Item>& item) {
+  if (!item.has_value()) {
+    return absl::InvalidArgumentError("The import has been skipped");
+  }
+  if (auto* unsupported = std::get_if<UnsupportedItem>(&*item)) {
+    return absl::InvalidArgumentError(unsupported->message);
+  }
+  return absl::OkStatus();
+}
 }
 
 // A mapping of C++ standard types to their equivalent Rust types.
@@ -322,6 +335,27 @@
   return ordered_item_ids;
 }
 
+std::vector<ItemId> Importer::GetOrderedItemIdsOfTemplateInstantiations()
+    const {
+  std::vector<SourceLocationComparator::OrderedItemId> items;
+  items.reserve(class_template_instantiations_for_current_target_.size());
+  for (const auto* decl : class_template_instantiations_for_current_target_) {
+    items.push_back(
+        {decl->getSourceRange(), GetDeclOrder(decl), GenerateItemId(decl)});
+  }
+
+  clang::SourceManager& sm = ctx_.getSourceManager();
+  auto compare_locations = SourceLocationComparator(sm);
+  llvm::sort(items, compare_locations);
+
+  std::vector<ItemId> ordered_item_ids;
+  ordered_item_ids.reserve(items.size());
+  for (const auto& ordered_item : items) {
+    ordered_item_ids.push_back(std::get<2>(ordered_item));
+  }
+  return ordered_item_ids;
+}
+
 void Importer::ImportFreeComments() {
   clang::SourceManager& sm = ctx_.getSourceManager();
   for (const auto& header : invocation_.entry_headers_) {
@@ -369,6 +403,11 @@
   }
   invocation_.ir_.top_level_item_ids =
       GetItemIdsInSourceOrder(translation_unit_decl);
+
+  // TODO(b/222001243): Consider placing the generated template instantiations
+  // into a separate namespace (maybe `crubit::instantiated_templates` ?).
+  llvm::copy(GetOrderedItemIdsOfTemplateInstantiations(),
+             std::back_inserter(invocation_.ir_.top_level_item_ids));
 }
 
 void Importer::ImportDeclsFromDeclContext(
@@ -408,6 +447,12 @@
 }
 
 BazelLabel Importer::GetOwningTarget(const clang::Decl* decl) const {
+  // Template instantiations need to be generated in the target that triggered
+  // the instantiation (not in the target where the template is defined).
+  if (IsFullClassTemplateSpecializationOrChild(decl)) {
+    return invocation_.target_;
+  }
+
   clang::SourceManager& source_manager = ctx_.getSourceManager();
   auto source_location = decl->getLocation();
 
@@ -504,6 +549,53 @@
                    .column = sm.getSpellingColumnNumber(loc)};
 }
 
+absl::StatusOr<MappedType> Importer::ConvertTemplateSpecializationType(
+    const clang::TemplateSpecializationType* type) {
+  // Qualifiers are handled separately in TypeMapper::ConvertQualType().
+  std::string type_string = clang::QualType(type, 0).getAsString();
+
+  auto* specialization_decl =
+      clang::dyn_cast_or_null<clang::ClassTemplateSpecializationDecl>(
+          type->getAsCXXRecordDecl());
+  if (!specialization_decl) {
+    return absl::InvalidArgumentError(absl::Substitute(
+        "Template specialization '$0' without an associated record decl "
+        "is not supported.",
+        type_string));
+  }
+
+  if (IsFromCurrentTarget(specialization_decl) &&
+      !specialization_decl->isExplicitSpecialization()) {
+    // Store implicit `specialization_decl`s so that they will get included in
+    // IR::top_level_item_ids.
+    class_template_instantiations_for_current_target_.insert(
+        specialization_decl);
+  }
+
+  // `Sema::isCompleteType` will try to instantiate the class template as a
+  // side-effect and we rely on this here. `decl->getDefinition()` can
+  // return nullptr before the call to sema and return its definition
+  // afterwards.
+  if (!sema_.isCompleteType(specialization_decl->getLocation(),
+                            ctx_.getRecordType(specialization_decl))) {
+    return absl::InvalidArgumentError(absl::Substitute(
+        "'$0' template specialization is incomplete", type_string));
+  }
+
+  // TODO(lukasza): Limit specialization depth? (e.g. using
+  // `isSpecializationDepthGreaterThan` from earlier prototypes).
+
+  absl::Status import_status =
+      CheckImportStatus(GetDeclItem(specialization_decl));
+  if (!import_status.ok()) {
+    return absl::InvalidArgumentError(absl::Substitute(
+        "Failed to create bindings for template specialization type $0: $1",
+        type_string, import_status.message()));
+  }
+
+  return type_mapper_.ConvertTypeDecl(specialization_decl);
+}
+
 absl::StatusOr<MappedType> TypeMapper::ConvertTypeDecl(
     const clang::TypeDecl* decl) const {
   if (!known_type_decls_.contains(decl)) {
@@ -623,6 +715,9 @@
   } else if (const auto* typedef_type =
                  type->getAsAdjusted<clang::TypedefType>()) {
     return ConvertTypeDecl(typedef_type->getDecl());
+  } else if (const auto* subst_type =
+                 type->getAs<clang::SubstTemplateTypeParmType>()) {
+    return ConvertQualType(subst_type->getReplacementType(), lifetimes);
   }
 
   return absl::UnimplementedError(absl::StrCat(
@@ -654,6 +749,26 @@
 }
 
 std::string Importer::GetMangledName(const clang::NamedDecl* named_decl) const {
+  if (auto specialization_decl =
+          clang::dyn_cast<clang::ClassTemplateSpecializationDecl>(named_decl)) {
+    // Itanium mangler produces valid Rust identifiers, use it to generate a
+    // name for this instantiation.
+    llvm::SmallString<128> storage;
+    llvm::raw_svector_ostream buffer(storage);
+    mangler_->mangleTypeName(ctx_.getRecordType(specialization_decl), buffer);
+
+    // The Itanium mangler does not provide a way to get the mangled
+    // representation of a type. Instead, we call mangleTypeName() that
+    // returns the name of the RTTI typeinfo symbol, and remove the _ZTS
+    // prefix. Then we prepend __CcTemplateInst to reduce chances of conflict
+    // with regular C and C++ structs.
+    constexpr llvm::StringRef kZtsPrefix = "_ZTS";
+    constexpr llvm::StringRef kCcTemplatePrefix = "__CcTemplateInst";
+    CRUBIT_CHECK(buffer.str().take_front(4) == kZtsPrefix);
+    return llvm::formatv("{0}{1}", kCcTemplatePrefix,
+                         buffer.str().drop_front(kZtsPrefix.size()));
+  }
+
   clang::GlobalDecl decl;
 
   // There are only three named decl types that don't work with the GlobalDecl
diff --git a/rs_bindings_from_cc/importer.h b/rs_bindings_from_cc/importer.h
index aecf1a7..7509dec 100644
--- a/rs_bindings_from_cc/importer.h
+++ b/rs_bindings_from_cc/importer.h
@@ -73,8 +73,15 @@
   llvm::Optional<std::string> GetComment(
       const clang::Decl* decl) const override;
   SourceLoc ConvertSourceLocation(clang::SourceLocation loc) const override;
+  absl::StatusOr<MappedType> ConvertTemplateSpecializationType(
+      const clang::TemplateSpecializationType* type) override;
 
  private:
+  // Returns the item ids of template instantiations that have been triggered
+  // from the current target.  The returned items are in an arbitrary,
+  // deterministic/reproducible order.
+  std::vector<ItemId> GetOrderedItemIdsOfTemplateInstantiations() const;
+
   // Returns the Item of a Decl, importing it first if necessary.
   std::optional<IR::Item> GetDeclItem(clang::Decl* decl);
 
@@ -89,6 +96,8 @@
   std::unique_ptr<clang::MangleContext> mangler_;
   absl::flat_hash_map<const clang::Decl*, std::optional<IR::Item>>
       import_cache_;
+  absl::flat_hash_set<const clang::ClassTemplateSpecializationDecl*>
+      class_template_instantiations_for_current_target_;
   std::vector<const clang::RawComment*> comments_;
 };  // class Importer
 
diff --git a/rs_bindings_from_cc/importers/BUILD b/rs_bindings_from_cc/importers/BUILD
index a7ae2b4..7391473 100644
--- a/rs_bindings_from_cc/importers/BUILD
+++ b/rs_bindings_from_cc/importers/BUILD
@@ -19,6 +19,7 @@
     deps = [
         "@absl//strings",
         "//rs_bindings_from_cc:ast_convert",
+        "//rs_bindings_from_cc:ast_util",
         "//rs_bindings_from_cc:decl_importer",
         "@llvm///clang:ast",
         "@llvm///clang:sema",
@@ -41,6 +42,7 @@
     hdrs = ["function.h"],
     deps = [
         "@absl//strings",
+        "//rs_bindings_from_cc:ast_util",
         "//rs_bindings_from_cc:decl_importer",
     ],
 )
diff --git a/rs_bindings_from_cc/importers/cxx_record.cc b/rs_bindings_from_cc/importers/cxx_record.cc
index 5347ac1..024d643 100644
--- a/rs_bindings_from_cc/importers/cxx_record.cc
+++ b/rs_bindings_from_cc/importers/cxx_record.cc
@@ -13,6 +13,19 @@
 
 namespace crubit {
 
+namespace {
+
+std::string GetClassTemplateSpecializationCcName(
+    const clang::ASTContext& ast_context,
+    const clang::ClassTemplateSpecializationDecl* specialization_decl) {
+  clang::PrintingPolicy policy(ast_context.getLangOpts());
+  policy.IncludeTagDefinition = false;
+  return clang::QualType(specialization_decl->getTypeForDecl(), 0)
+      .getAsString(policy);
+}
+
+}  // namespace
+
 std::optional<IR::Item> CXXRecordDeclImporter::Import(
     clang::CXXRecordDecl* record_decl) {
   const clang::DeclContext* decl_context = record_decl->getDeclContext();
@@ -26,14 +39,35 @@
     return ictx_.ImportUnsupportedItem(record_decl,
                                        "Nested classes are not supported yet");
   }
+  if (clang::isa<clang::ClassTemplatePartialSpecializationDecl>(record_decl)) {
+    return ictx_.ImportUnsupportedItem(
+        record_decl, "Partially-specialized class templates are not supported");
+  }
   if (record_decl->isInvalidDecl()) {
     return std::nullopt;
   }
 
-  std::optional<Identifier> record_name =
-      ictx_.GetTranslatedIdentifier(record_decl);
-  if (!record_name.has_value()) {
-    return std::nullopt;
+  std::string rs_name, cc_name;
+  llvm::Optional<std::string> doc_comment;
+  if (auto* specialization_decl =
+          clang::dyn_cast<clang::ClassTemplateSpecializationDecl>(
+              record_decl)) {
+    rs_name = ictx_.GetMangledName(specialization_decl);
+    cc_name =
+        GetClassTemplateSpecializationCcName(ictx_.ctx_, specialization_decl);
+    doc_comment = ictx_.GetComment(specialization_decl);
+    if (!doc_comment.hasValue()) {
+      doc_comment =
+          ictx_.GetComment(specialization_decl->getSpecializedTemplate());
+    }
+  } else {
+    std::optional<Identifier> record_name =
+        ictx_.GetTranslatedIdentifier(record_decl);
+    if (!record_name.has_value()) {
+      return std::nullopt;
+    }
+    rs_name = cc_name = record_name->Ident();
+    doc_comment = ictx_.GetComment(record_decl);
   }
 
   if (clang::CXXRecordDecl* complete = record_decl->getDefinition()) {
@@ -42,20 +76,12 @@
     CRUBIT_CHECK(!record_decl->isCompleteDefinition());
     ictx_.type_mapper_.Insert(record_decl);
     return IncompleteRecord{
-        .cc_name = std::string(record_name->Ident()),
+        .cc_name = std::move(cc_name),
         .id = GenerateItemId(record_decl),
         .owning_target = ictx_.GetOwningTarget(record_decl),
         .enclosing_namespace_id = GetEnclosingNamespaceId(record_decl)};
   }
 
-  // To compute the memory layout of the record, it needs to be a concrete type,
-  // not a template.
-  if (record_decl->getDescribedClassTemplate() ||
-      clang::isa<clang::ClassTemplateSpecializationDecl>(record_decl)) {
-    return ictx_.ImportUnsupportedItem(record_decl,
-                                       "Class templates are not supported yet");
-  }
-
   ictx_.sema_.ForceDeclarationOfImplicitMembers(record_decl);
 
   const clang::ASTRecordLayout& layout =
@@ -81,11 +107,11 @@
 
   auto item_ids = ictx_.GetItemIdsInSourceOrder(record_decl);
   return Record{
-      .rs_name = std::string(record_name->Ident()),
-      .cc_name = std::string(record_name->Ident()),
+      .rs_name = std::move(rs_name),
+      .cc_name = std::move(cc_name),
       .id = GenerateItemId(record_decl),
       .owning_target = ictx_.GetOwningTarget(record_decl),
-      .doc_comment = ictx_.GetComment(record_decl),
+      .doc_comment = std::move(doc_comment),
       .unambiguous_public_bases = GetUnambiguousPublicBases(*record_decl),
       .fields = *std::move(fields),
       .size = layout.getSize().getQuantity(),
diff --git a/rs_bindings_from_cc/importers/function.cc b/rs_bindings_from_cc/importers/function.cc
index e09245b..1f1e363 100644
--- a/rs_bindings_from_cc/importers/function.cc
+++ b/rs_bindings_from_cc/importers/function.cc
@@ -5,6 +5,7 @@
 #include "rs_bindings_from_cc/importers/function.h"
 
 #include "absl/strings/substitute.h"
+#include "rs_bindings_from_cc/ast_util.h"
 
 namespace crubit {
 
@@ -187,9 +188,34 @@
   bool has_c_calling_convention =
       function_decl->getType()->getAs<clang::FunctionType>()->getCallConv() ==
       clang::CC_C;
+  bool is_member_or_descendant_of_class_template =
+      IsFullClassTemplateSpecializationOrChild(function_decl);
   std::optional<UnqualifiedIdentifier> translated_name =
       ictx_.GetTranslatedName(function_decl);
 
+  llvm::Optional<std::string> doc_comment = ictx_.GetComment(function_decl);
+  if (!doc_comment.hasValue() && is_member_or_descendant_of_class_template) {
+    // Despite `is_member_or_descendant_of_class_template` check above, we are
+    // not guaranteed that a `func_pattern` exists below.  For example, it may
+    // be missing when `function_decl` is an implicitly defined constructor of a
+    // class template -- such decls are generated, not instantiated.
+    if (clang::FunctionDecl* func_pattern =
+            function_decl->getTemplateInstantiationPattern()) {
+      doc_comment = ictx_.GetComment(func_pattern);
+    }
+  }
+
+  std::string mangled_name = ictx_.GetMangledName(function_decl);
+  if (is_member_or_descendant_of_class_template) {
+    // TODO(b/222001243): Avoid calling `ConvertToCcIdentifier(target)` to
+    // distinguish multiple definitions of a template instantiation.  Instead
+    // help the linker merge all the definitions into one, by defining the
+    // thunk via a function template - see "Handling thunks" section in
+    // <internal link>
+    mangled_name += '_';
+    mangled_name += ConvertToCcIdentifier(ictx_.GetOwningTarget(function_decl));
+  }
+
   // Silence ClangTidy, checked above: calling `add_error` if
   // `!return_type.ok()` and returning early if `!errors.empty()`.
   CRUBIT_CHECK(return_type.ok());
@@ -198,14 +224,16 @@
     return Func{
         .name = *translated_name,
         .owning_target = ictx_.GetOwningTarget(function_decl),
-        .doc_comment = ictx_.GetComment(function_decl),
-        .mangled_name = ictx_.GetMangledName(function_decl),
+        .doc_comment = std::move(doc_comment),
+        .mangled_name = std::move(mangled_name),
         .return_type = *return_type,
         .params = std::move(params),
         .lifetime_params = std::move(lifetime_params),
         .is_inline = function_decl->isInlined(),
         .member_func_metadata = std::move(member_func_metadata),
         .has_c_calling_convention = has_c_calling_convention,
+        .is_member_or_descendant_of_class_template =
+            is_member_or_descendant_of_class_template,
         .source_loc = ictx_.ConvertSourceLocation(function_decl->getBeginLoc()),
         .id = GenerateItemId(function_decl),
         .enclosing_namespace_id = GetEnclosingNamespaceId(function_decl),
diff --git a/rs_bindings_from_cc/importers/typedef_name.cc b/rs_bindings_from_cc/importers/typedef_name.cc
index ff27f16..e2b483a 100644
--- a/rs_bindings_from_cc/importers/typedef_name.cc
+++ b/rs_bindings_from_cc/importers/typedef_name.cc
@@ -32,10 +32,18 @@
   std::optional<Identifier> identifier =
       ictx_.GetTranslatedIdentifier(typedef_name_decl);
   CRUBIT_CHECK(identifier.has_value());  // This should never happen.
-  std::optional<clang::tidy::lifetimes::ValueLifetimes> no_lifetimes;
-  absl::StatusOr<MappedType> underlying_type =
-      ictx_.type_mapper_.ConvertQualType(typedef_name_decl->getUnderlyingType(),
-                                         no_lifetimes);
+
+  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_.type_mapper_.ConvertQualType(
+        typedef_name_decl->getUnderlyingType(), no_lifetimes);
+  }
   if (underlying_type.ok()) {
     ictx_.type_mapper_.Insert(typedef_name_decl);
     return TypeAlias{
diff --git a/rs_bindings_from_cc/ir.cc b/rs_bindings_from_cc/ir.cc
index 10ec1e9..7e2730e 100644
--- a/rs_bindings_from_cc/ir.cc
+++ b/rs_bindings_from_cc/ir.cc
@@ -291,6 +291,8 @@
       {"is_inline", is_inline},
       {"member_func_metadata", member_func_metadata},
       {"has_c_calling_convention", has_c_calling_convention},
+      {"is_member_or_descendant_of_class_template",
+       is_member_or_descendant_of_class_template},
       {"source_loc", source_loc},
       {"id", id},
       {"enclosing_namespace_id", enclosing_namespace_id},
diff --git a/rs_bindings_from_cc/ir.h b/rs_bindings_from_cc/ir.h
index b0c35e3..f4027e5 100644
--- a/rs_bindings_from_cc/ir.h
+++ b/rs_bindings_from_cc/ir.h
@@ -454,6 +454,7 @@
   // If null, this is not a member function.
   llvm::Optional<MemberFuncMetadata> member_func_metadata;
   bool has_c_calling_convention = true;
+  bool is_member_or_descendant_of_class_template = false;
   SourceLoc source_loc;
   ItemId id;
   llvm::Optional<ItemId> enclosing_namespace_id;
diff --git a/rs_bindings_from_cc/ir.rs b/rs_bindings_from_cc/ir.rs
index e91e099..1209f0e 100644
--- a/rs_bindings_from_cc/ir.rs
+++ b/rs_bindings_from_cc/ir.rs
@@ -282,6 +282,7 @@
     pub is_inline: bool,
     pub member_func_metadata: Option<MemberFuncMetadata>,
     pub has_c_calling_convention: bool,
+    pub is_member_or_descendant_of_class_template: bool,
     pub source_loc: SourceLoc,
     pub id: ItemId,
     pub enclosing_namespace_id: Option<ItemId>,
diff --git a/rs_bindings_from_cc/ir_from_cc_test.rs b/rs_bindings_from_cc/ir_from_cc_test.rs
index 986c5fa..35b5db6 100644
--- a/rs_bindings_from_cc/ir_from_cc_test.rs
+++ b/rs_bindings_from_cc/ir_from_cc_test.rs
@@ -77,6 +77,7 @@
                 is_inline: false,
                 member_func_metadata: None,
                 has_c_calling_convention: true,
+                is_member_or_descendant_of_class_template: false,
                 source_loc: SourceLoc {
                     filename: "ir_from_cc_virtual_header.h",
                     line: 3,
@@ -137,14 +138,14 @@
 #[test]
 fn test_dont_import_record_nested_in_func() {
     let ir = ir_from_cc("inline void f() { struct S{}; }").unwrap();
-    assert_ir_not_matches!(ir, quote! { Record { identifier: "S" ... } });
+    assert_ir_not_matches!(ir, quote! { Record { ... "S" ... } });
 }
 
 #[test]
-fn test_dont_import_class_template_or_specialization() {
+fn test_dont_import_unused_class_template_or_specialization() {
     let ir = ir_from_cc("template <class T> struct Template{}; template<> struct Template<int>{};")
         .unwrap();
-    assert_ir_not_matches!(ir, quote! { Record { identifier: "Template" ... } });
+    assert_ir_not_matches!(ir, quote! { Record { ... "Template" ... } });
 }
 
 #[test]
@@ -749,6 +750,697 @@
 }
 
 #[test]
+fn test_typedef_of_fully_instantiated_template() -> Result<()> {
+    let ir = ir_from_cc(
+        r#" #pragma clang lifetime_elision
+
+            // Doc comment of MyStruct template.
+            template <typename T>
+            struct MyStruct {
+              // Doc comment of GetValue method.
+              const T& GetValue() const { return value; }
+
+              // Doc comment of `value` field.
+              T value;
+            };
+
+            // Doc comment of MyTypeAlias.
+            using MyTypeAlias = MyStruct<int>; "#,
+    )?;
+    // Instantiation of the struct template:
+    assert_ir_matches!(
+        ir,
+        quote! {
+          Record {
+            rs_name: "__CcTemplateInst8MyStructIiE", ...
+            cc_name: "MyStruct<int>", ...
+            owning_target: BazelLabel("//test:testing_target"), ...
+            doc_comment: Some("Doc comment of MyStruct template."), ...
+            fields: [Field {
+                identifier: Some("value"), ...
+                doc_comment: Some("Doc comment of `value` field."), ...
+                type_: MappedType {
+                    rs_type: RsType { name: Some("i32"), ... },
+                    cc_type: CcType { name: Some("int"), ... },
+                },
+                access: Public,
+                offset: 0, ...
+            }], ...
+          }
+        }
+    );
+    // 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());
+    // Type alias for the class template specialization.
+    assert_ir_matches!(
+        ir,
+        quote! {
+          TypeAlias {
+            identifier: "MyTypeAlias", ...
+            owning_target: BazelLabel("//test:testing_target"), ...
+            doc_comment: Some("Doc comment of MyTypeAlias."), ...
+            underlying_type: MappedType {
+                rs_type: RsType {
+                    name: None,
+                    lifetime_args: [],
+                    type_args: [],
+                    decl_id: Some(ItemId(...)),
+                },
+                cc_type: CcType {
+                    name: None,
+                    is_const: false,
+                    type_args: [],
+                    decl_id: Some(ItemId(...)),
+                },
+            } ...
+          }
+        }
+    );
+    // Member function of the struct template instantiation:
+    assert_ir_matches!(
+        ir,
+        quote! {
+          Func {
+            name: "GetValue",
+            owning_target: BazelLabel("//test:testing_target"),
+            mangled_name: "_ZNK8MyStructIiE8GetValueEv___test_testing_target", ...
+            doc_comment: Some("Doc comment of GetValue method."), ...
+            is_inline: true, ...
+            member_func_metadata: Some(MemberFuncMetadata {
+                record_id: ItemId(...),
+                instance_method_metadata: Some(InstanceMethodMetadata { ... }), ...
+            }), ...
+          }
+        }
+    );
+    // Implicitly defined assignment operator inside the struct template is
+    // represented in the AST slightly differently (not marked as instantiated)
+    // because it is generated by the compiler for the complete, instantiated type
+    // according to general rules.
+    assert_ir_matches!(
+        ir,
+        quote! {
+          Func {
+              name: "operator=",
+              owning_target: BazelLabel("//test:testing_target"),
+              mangled_name: "_ZN8MyStructIiEaSERKS0____test_testing_target", ...
+              doc_comment: None, ...
+          }
+        }
+    );
+    Ok(())
+}
+
+#[test]
+fn test_typedef_for_explicit_template_specialization() -> Result<()> {
+    let ir = ir_from_cc(
+        r#" #pragma clang lifetime_elision
+
+            template <typename T>
+            struct MyStruct final {};
+
+            // Doc comment for template specialization for T=int.
+            template<>
+            struct MyStruct<int> final {
+              // Doc comment of the GetValue method specialization for T=int.
+              const int& GetValue() const { return value * 42; }
+
+              // Doc comment of the `value` field specialization for T=int.
+              int value;
+            };
+
+            // 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,
+        quote! {
+          Record {
+            rs_name: "__CcTemplateInst8MyStructIiE", ...
+            cc_name: "MyStruct<int>", ...
+            owning_target: BazelLabel("//test:testing_target"), ...
+            doc_comment: Some("Doc comment for template specialization for T=int."), ...
+            fields: [Field {
+                identifier: Some("value"), ...
+                doc_comment: Some("Doc comment of the `value` field specialization for T=int."), ...
+                type_: MappedType {
+                    rs_type: RsType { name: Some("i32"), ... },
+                    cc_type: CcType { name: Some("int"), ... },
+                },
+                access: Public,
+                offset: 0, ...
+            }], ...
+          }
+        }
+    );
+    // Instance method inside the struct template:
+    assert_ir_matches!(
+        ir,
+        quote! {
+          Func {
+            name: "GetValue",
+            owning_target: BazelLabel("//test:testing_target"),
+            mangled_name: "_ZNK8MyStructIiE8GetValueEv___test_testing_target", ...
+            doc_comment: Some("Doc comment of the GetValue method specialization for T=int."), ...
+            is_inline: true, ...
+            member_func_metadata: Some(MemberFuncMetadata {
+                record_id: ItemId(...),
+                instance_method_metadata: Some(InstanceMethodMetadata { ... }), ...
+            }), ...
+          }
+        }
+    );
+    Ok(())
+}
+
+#[test]
+fn test_multiple_typedefs_to_same_template() -> Result<()> {
+    let ir = ir_from_cc(
+        r#" #pragma clang lifetime_elision
+            template <typename T>
+            struct MyStruct {
+              void MyMethod() {}
+            };
+            using MyIntAlias = MyStruct<int>;
+            using MyIntAlias = MyStruct<int>;
+            using MyIntAlias2 = MyStruct<int>;
+            using MyFloatAlias = MyStruct<float>;
+            "#,
+    )?;
+
+    // Verify that there is only 1 record for each specialization.
+    assert_eq!(1, ir.records().filter(|r| r.cc_name == "MyStruct<int>").count());
+    assert_eq!(1, ir.records().filter(|r| r.cc_name == "MyStruct<float>").count());
+    let functions = ir
+        .functions()
+        .filter(|f| f.name == UnqualifiedIdentifier::Identifier(ir_id("MyMethod")))
+        .collect_vec();
+
+    // Verify that there is only 1 function per instantiation.
+    assert_eq!(2, functions.len());
+    let rec_id1 = functions[0].member_func_metadata.as_ref().unwrap().record_id;
+    let rec_id2 = functions[1].member_func_metadata.as_ref().unwrap().record_id;
+    assert_ne!(rec_id1, rec_id2);
+    Ok(())
+}
+
+#[test]
+fn test_templates_inheritance() -> Result<()> {
+    let ir = ir_from_cc(
+        r#" #pragma clang lifetime_elision
+            template <typename T>
+            class BaseTemplate {
+             protected:
+              BaseTemplate(T base_value) : base_value_(base_value) {}
+              const T& base_value() const { return base_value_; }
+             private:
+              T base_value_;
+            };
+
+            template <typename T>
+            class ClassTemplateDerivedFromClassTemplate : public BaseTemplate<T> {
+             public:
+              ClassTemplateDerivedFromClassTemplate(T base_value, T derived_value)
+                  : BaseTemplate<T>(base_value), derived_value_(derived_value) {}
+              T combined_value() const {
+                return 1000 * BaseTemplate<T>::base_value() + derived_value_;
+              }
+             private:
+              T derived_value_;
+            };
+
+            using TypeAliasForClassTemplateDerivedFromClassTemplate =
+                    ClassTemplateDerivedFromClassTemplate<int>;
+            "#,
+    )?;
+
+    // ClassTemplateDerivedFromClassTemplate is instantiated because of
+    // TypeAliasForClassTemplateDerivedFromClassTemplate..
+    assert_eq!(
+        1,
+        ir.records()
+            .filter(|r| r.cc_name.contains("ClassTemplateDerivedFromClassTemplate"))
+            .count()
+    );
+
+    // BaseTemplate is *not* instantiated in the generated bindings/IR.  The derived
+    // class's bindings work fine without the bindings for the base class (this
+    // is also true for non-templated base/derived classes).
+    assert_eq!(0, ir.records().filter(|r| r.cc_name.contains("BaseTemplate")).count());
+    Ok(())
+}
+
+#[test]
+fn test_aliased_class_template_instantiated_in_header() -> Result<()> {
+    // This aliased class template specialization is instantiated due to the code
+    // that is present in the header. We should not corrupt the AST by
+    // instantiating again.
+    let ir = ir_from_cc(
+        r#" #pragma clang lifetime_elision
+            template <typename T>
+            struct MyTemplate {
+                const T& GetValue() { return field; }
+                T field;
+            };
+
+            inline void my_full_instantiation() {
+                MyTemplate<int> t;
+                t.field = 123;
+                t.field = t.GetValue() * 123;
+            }
+
+            using MyAlias = MyTemplate<int>; "#,
+    )?;
+    assert_ir_matches!(
+        ir,
+        quote! {
+          Record {
+            rs_name: "__CcTemplateInst10MyTemplateIiE", ...
+            cc_name: "MyTemplate<int>", ...
+            fields: [Field { identifier: Some("field"), ... }], ...
+          }
+        }
+    );
+    assert_ir_matches!(ir, quote! { Func { name: "GetValue", ...  } });
+    Ok(())
+}
+
+#[test]
+fn test_aliased_class_template_partially_instantiated_in_header() -> Result<()> {
+    // Similar to `test_aliased_class_template_instantiated_in_header`, but doesn't
+    // instantiate all members.
+    let ir = ir_from_cc(
+        r#" #pragma clang lifetime_elision
+            template <typename T>
+            struct MyTemplate {
+                const T& GetValue() { return field; }
+                T field;
+            };
+
+            inline void my_instantiation() {
+                MyTemplate<int> t;
+                // Members of MyTemplate are not used/instantiated.
+            }
+
+            using MyAlias = MyTemplate<int>; "#,
+    )?;
+    assert_ir_matches!(
+        ir,
+        quote! {
+          Record {
+            rs_name: "__CcTemplateInst10MyTemplateIiE", ...
+            cc_name: "MyTemplate<int>", ...
+            fields: [Field { identifier: Some("field"), ... }], ...
+          }
+        }
+    );
+    assert_ir_matches!(ir, quote! { Func { name: "GetValue", ...  } });
+    Ok(())
+}
+
+#[test]
+fn test_no_instantiation_of_template_only_used_in_private_field() -> Result<()> {
+    let ir = ir_from_cc(
+        r#" #pragma clang lifetime_elision
+            template <typename T>
+            struct MyTemplate { T field; };
+
+            class MyStruct {
+             private:
+              MyTemplate<int> private_field_;
+            }; "#,
+    )?;
+    // 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" });
+    Ok(())
+}
+
+#[test]
+fn test_subst_template_type_parm_type_vs_const_when_non_const_template_param() -> Result<()> {
+    // This test (and
+    // `test_subst_template_type_parm_type_vs_const_when_const_template_param`)
+    // verifies that `importer.cc` preserves `const` qualifier attached *both* to
+    // QualType associated with:
+    // 1) SubstTemplateTypeParm (i.e. the template *argument* has `const`:
+    // `MyTemplate<const int>`) 2) TemplateTypeParmType used inside the template
+    // definition: `const T& GetConstRef()`
+    let ir = ir_from_cc(
+        r#" #pragma clang lifetime_elision
+            template <typename T>
+            struct MyTemplate {
+                const T& GetConstRef() const { return value; }
+                T& GetRef() { return value; }
+                T value;
+            };
+
+            // Just like the other test_subst_template_type_parm_type_vs_const...
+            // test, but using non-*const* int template parameter.
+            using MyAlias = MyTemplate<int>; "#,
+    )?;
+    assert_ir_matches!(
+        ir,
+        quote! {
+            Func {
+               name: "GetConstRef", ...
+               return_type: MappedType {
+                   rs_type: RsType {
+                       name: Some("&"), ...
+                       type_args: [RsType { name: Some("i32"), ...  }], ...
+                   },
+                   cc_type: CcType {
+                       name: Some("&"),
+                       is_const: false,
+                       type_args: [CcType {
+                           name: Some("int"),
+                           is_const: true, ...
+                       }], ...
+                   },
+               }, ...
+            }
+        }
+    );
+    assert_ir_matches!(
+        ir,
+        quote! {
+            Func {
+               name: "GetRef", ...
+               return_type: MappedType {
+                   rs_type: RsType {
+                       name: Some("&mut"), ...
+                       type_args: [RsType { name: Some("i32"), ...  }], ...
+                   },
+                   cc_type: CcType {
+                       name: Some("&"),
+                       is_const: false,
+                       type_args: [CcType {
+                           name: Some("int"),
+                           is_const: false, ...
+                       }], ...
+                   },
+               }, ...
+            }
+        }
+    );
+    Ok(())
+}
+
+#[test]
+fn test_subst_template_type_parm_type_vs_const_when_const_template_param() -> Result<()> {
+    // This test (and
+    // `test_subst_template_type_parm_type_vs_const_when_non_const_template_param`)
+    // verifies that `importer.cc` preserves `const` qualifier attached *both* to
+    // QualType associated with:
+    // 1) SubstTemplateTypeParm (i.e. the template *argument* has `const`:
+    // `MyTemplate<const int>`) 2) TemplateTypeParmType used inside the template
+    // definition: `const T& GetConstRef()`
+    let ir = ir_from_cc(
+        r#" #pragma clang lifetime_elision
+            template <typename T>
+            struct MyTemplate {
+                const T& GetConstRef() const { return value; }
+                T& GetRef() { return value; }
+                T value;
+            };
+
+            // Just like the other test_subst_template_type_parm_type_vs_const...
+            // test, but using *const* int template parameter.
+            using MyAlias = MyTemplate<const int>; "#,
+    )?;
+    assert_ir_matches!(
+        ir,
+        quote! {
+            Func {
+               name: "GetConstRef", ...
+               return_type: MappedType {
+                   rs_type: RsType {
+                       name: Some("&"), ...
+                       type_args: [RsType { name: Some("i32"), ...  }], ...
+                   },
+                   cc_type: CcType {
+                       name: Some("&"),
+                       is_const: false,
+                       type_args: [CcType {
+                           name: Some("int"),
+                           is_const: true, ...
+                       }], ...
+                   },
+               }, ...
+            }
+        }
+    );
+    assert_ir_matches!(
+        ir,
+        quote! {
+            Func {
+               name: "GetRef", ...
+               return_type: MappedType {
+                   rs_type: RsType {
+                       name: Some("&"), ...
+                       type_args: [RsType { name: Some("i32"), ...  }], ...
+                   },
+                   cc_type: CcType {
+                       name: Some("&"),
+                       is_const: false,
+                       type_args: [CcType {
+                           name: Some("int"),
+                           is_const: true, ...
+                       }], ...
+                   },
+               }, ...
+            }
+        }
+    );
+    Ok(())
+}
+
+#[test]
+fn test_template_and_alias_are_both_in_dependency() -> Result<()> {
+    // See also the `test_template_in_dependency_and_alias_in_current_target` test.
+    let ir = {
+        let dependency_src = r#" #pragma clang lifetime_elision
+                template <typename T>
+                struct MyTemplate {
+                    T GetValue();
+                    T field;
+                };
+                using MyAliasOfTemplate = MyTemplate<int>;
+                struct StructInDependency {}; "#;
+        let current_target_src = r#" #pragma clang lifetime_elision
+                /* no references to MyTemplate or MyAliasOfTemplate */
+                struct StructInCurrentTarget {}; "#;
+        ir_from_cc_dependency(current_target_src, dependency_src)?
+    };
+
+    // Just double-checking the test inputs VS target names.
+    let current_target = ir_testing::TESTING_TARGET;
+    let dependency = ir_testing::DEPENDENCY_TARGET;
+    assert_ir_matches!(
+        ir,
+        quote! {
+            Record { ...
+                cc_name: "StructInCurrentTarget", ...
+                owning_target: BazelLabel(#current_target), ...
+            }
+        }
+    );
+    assert_ir_matches!(
+        ir,
+        quote! {
+            Record { ...
+                cc_name: "StructInDependency", ...
+                owning_target: BazelLabel(#dependency), ...
+            }
+        }
+    );
+
+    // Type alias is only defined in `dependency`.
+    assert_ir_matches!(
+        ir,
+        quote! {
+            TypeAlias { ...
+                identifier: "MyAliasOfTemplate", ...
+                owning_target: BazelLabel(#dependency), ...
+            }
+        }
+    );
+    assert_ir_not_matches!(
+        ir,
+        quote! {
+            TypeAlias { ...
+                identifier: "MyAliasOfTemplate", ...
+                owning_target: BazelLabel(#current_target), ...
+            }
+        }
+    );
+
+    // The template should be instantiated in `dependency`, rather than in
+    // `current_target`.
+    // TODO(b/222001243): Fix which target contains the instantiations and then flip
+    // the test assertions below.  Tentative fix: cl/438580040.
+    assert_ir_not_matches!(
+        ir,
+        quote! {
+            Record { ...
+                cc_name: "MyTemplate<int>", ...
+                owning_target: BazelLabel(#dependency), ...
+            }
+        }
+    );
+    assert_ir_matches!(
+        ir,
+        quote! {
+            Record { ...
+                cc_name: "MyTemplate<int>", ...
+                owning_target: BazelLabel(#current_target), ...
+            }
+        }
+    );
+
+    // The template instantiations in the `dependency` should only produce type
+    // information (e.g. TypeAlias, Record) and don't need to produce Func
+    // items.
+    assert_ir_not_matches!(
+        ir,
+        quote! {
+            Func { ...
+                name: "GetValue", ...
+                owning_target: BazelLabel(#dependency), ...
+            }
+        }
+    );
+    // There should be nothing template-instantiation-related in the main test
+    // target. TODO(b/222001243): Fix which target contains the instantiations
+    // and then flip the test assertions below to `assert_ir_not_matches`.
+    assert_ir_matches!(
+        ir,
+        quote! {
+            Func { ...
+                name: "GetValue", ...
+                owning_target: BazelLabel(#current_target), ...
+            }
+        }
+    );
+
+    Ok(())
+}
+
+#[test]
+fn test_template_in_dependency_and_alias_in_current_target() -> Result<()> {
+    // See also the `test_template_and_alias_are_both_in_dependency` test.
+    let ir = {
+        let dependency_src = r#" #pragma clang lifetime_elision
+                template <typename T>
+                struct MyTemplate {
+                    T GetValue();
+                    T field;
+                };
+                struct StructInDependency{}; "#;
+        let current_target_src = r#" #pragma clang lifetime_elision
+                using MyAliasOfTemplate = MyTemplate<int>;
+                struct StructInCurrentTarget{}; "#;
+        ir_from_cc_dependency(current_target_src, dependency_src)?
+    };
+
+    // Just double-checking the test inputs VS target names.
+    let current_target = ir_testing::TESTING_TARGET;
+    let dependency = ir_testing::DEPENDENCY_TARGET;
+    assert_ir_matches!(
+        ir,
+        quote! {
+            Record { ...
+                cc_name: "StructInCurrentTarget", ...
+                owning_target: BazelLabel(#current_target), ...
+            }
+        }
+    );
+    assert_ir_matches!(
+        ir,
+        quote! {
+            Record { ...
+                cc_name: "StructInDependency", ...
+                owning_target: BazelLabel(#dependency), ...
+            }
+        }
+    );
+
+    // Type alias is only defined in `current_target`
+    assert_ir_not_matches!(
+        ir,
+        quote! {
+            TypeAlias { ...
+                identifier: "MyAliasOfTemplate", ...
+                owning_target: BazelLabel(#dependency), ...
+            }
+        }
+    );
+    assert_ir_matches!(
+        ir,
+        quote! {
+            TypeAlias { ...
+                identifier: "MyAliasOfTemplate", ...
+                owning_target: BazelLabel(#current_target), ...
+            }
+        }
+    );
+
+    // The template should be instantiated in `current_target`, rather than in
+    // `dependency`.
+    assert_ir_not_matches!(
+        ir,
+        quote! {
+            Record { ...
+                cc_name: "MyTemplate<int>", ...
+                owning_target: BazelLabel(#dependency), ...
+            }
+        }
+    );
+    assert_ir_matches!(
+        ir,
+        quote! {
+            Record { ...
+                cc_name: "MyTemplate<int>", ...
+                owning_target: BazelLabel(#current_target), ...
+            }
+        }
+    );
+
+    // There should be nothing template-instantiation-related in the dependency
+    // (since there is no instantiation there).
+    assert_ir_not_matches!(
+        ir,
+        quote! {
+            Func { ...
+                name: "GetValue", ...
+                owning_target: BazelLabel(#dependency), ...
+            }
+        }
+    );
+    // The template instantiations in the current target should produce not only
+    // type information (e.g. TypeAlias, Record) but also Func items (for
+    // methods of the instantiated class template).
+    assert_ir_matches!(
+        ir,
+        quote! {
+            Func { ...
+                name: "GetValue", ...
+                owning_target: BazelLabel(#current_target), ...
+            }
+        }
+    );
+
+    Ok(())
+}
+
+#[test]
 fn test_well_known_types_check_namespaces() -> Result<()> {
     // Check that we don't treat a type called `int32_t` in a user-defined
     // namespace as if it was the standard type `int32_t`.
diff --git a/rs_bindings_from_cc/ir_testing.rs b/rs_bindings_from_cc/ir_testing.rs
index a207743..3549ab3 100644
--- a/rs_bindings_from_cc/ir_testing.rs
+++ b/rs_bindings_from_cc/ir_testing.rs
@@ -27,11 +27,15 @@
     )
 }
 
+/// Target of the dependency used by `ir_from_cc_dependency`.
+/// Needs to be kept in sync with `kDependencyTarget` in `json_from_cc.cc`.
+pub const DEPENDENCY_TARGET: &str = "//test:dependency";
+
 /// Generates `IR` from a header that depends on another header.
 ///
 /// `header_source` of the header will be updated to contain the `#include` line
 /// for the header with `dependency_header_source`. The name of the dependency
-/// target is assumed to be `"//test:dependency"`.
+/// target is exposed as `DEPENDENCY_TARGET`.
 pub fn ir_from_cc_dependency(header_source: &str, dependency_header_source: &str) -> Result<IR> {
     const DEPENDENCY_HEADER_NAME: &str = "test/dependency_header.h";
 
diff --git a/rs_bindings_from_cc/src_code_gen.rs b/rs_bindings_from_cc/src_code_gen.rs
index 4be69ee..9fac534 100644
--- a/rs_bindings_from_cc/src_code_gen.rs
+++ b/rs_bindings_from_cc/src_code_gen.rs
@@ -173,6 +173,12 @@
     if func.is_inline {
         return false;
     }
+    // ## Member functions (or descendants) of class templates
+    //
+    // A thunk is required to force/guarantee template instantiation.
+    if func.is_member_or_descendant_of_class_template {
+        return false;
+    }
     // ## Virtual functions
     //
     // When calling virtual `A::Method()`, it's not necessarily the case that we'll
@@ -2593,6 +2599,133 @@
     }
 
     #[test]
+    fn test_template_in_dependency_and_alias_in_current_target() -> Result<()> {
+        // See also the test with the same name in `ir_from_cc_test.rs`.
+        let ir = {
+            let dependency_src = r#" #pragma clang lifetime_elision
+                    template <typename T>
+                    struct MyTemplate {
+                        T GetValue() { return field; }
+                        T field;
+                    }; "#;
+            let current_target_src = r#" #pragma clang lifetime_elision
+                    using MyAliasOfTemplate = MyTemplate<int>; "#;
+            ir_from_cc_dependency(current_target_src, dependency_src)?
+        };
+
+        let BindingsTokens { rs_api, rs_api_impl } = generate_bindings_tokens(&ir)?;
+        assert_rs_matches!(
+            rs_api,
+            quote! {
+                #[repr(C)]
+                pub struct __CcTemplateInst10MyTemplateIiE {
+                    pub field: i32,
+                }
+            }
+        );
+        assert_rs_matches!(
+            rs_api,
+            quote! {
+                impl __CcTemplateInst10MyTemplateIiE {
+                    #[inline(always)]
+                    pub fn GetValue<'a>(self: ... Pin<&'a mut Self>) -> i32 { unsafe {
+                        crate::detail::__rust_thunk___ZN10MyTemplateIiE8GetValueEv___test_testing_target(
+                            self)
+                    }}
+                }
+            }
+        );
+        assert_rs_matches!(
+            rs_api,
+            quote! {
+                pub type MyAliasOfTemplate = crate::__CcTemplateInst10MyTemplateIiE;
+            }
+        );
+        assert_rs_matches!(
+            rs_api,
+            quote! {
+                mod detail {
+                    ...
+                    extern "C" {
+                        ...
+                        pub(crate) fn
+                        __rust_thunk___ZN10MyTemplateIiE8GetValueEv___test_testing_target<'a>(
+                            __this: ... Pin<&'a mut crate::__CcTemplateInst10MyTemplateIiE>
+                        ) -> i32;
+                        ...
+                    }
+                }
+            }
+        );
+        assert_cc_matches!(
+            rs_api_impl,
+            quote! {
+                extern "C" int __rust_thunk___ZN10MyTemplateIiE8GetValueEv___test_testing_target(
+                        class MyTemplate<int>* __this) {
+                    return __this->GetValue();
+                }
+            }
+        );
+
+        Ok(())
+    }
+
+    #[test]
+    fn test_template_with_out_of_line_definition() -> Result<()> {
+        // See also an end-to-end test in the `test/templates/out_of_line_definition`
+        // directory.
+        let ir = ir_from_cc(
+            r#" #pragma clang lifetime_elision
+                template <typename T>
+                class MyTemplate final {
+                 public:
+                  static MyTemplate Create(T value);
+                  const T& value() const;
+
+                 private:
+                  T value_;
+                };
+
+                using MyTypeAlias = MyTemplate<int>; "#,
+        )?;
+
+        let BindingsTokens { rs_api_impl, .. } = generate_bindings_tokens(&ir)?;
+
+        // Even though the member functions above are *not* defined inline (e.g.
+        // IR::Func::is_inline is false), they still need to have thunks generated for
+        // them (to force/guarantee that the class template and its members get
+        // instantiated).  This is also covered in the following end-to-end
+        // tests:
+        // - test/templates/out_of_line_definition/ - without a thunk, the template
+        //   won't be instantiated and Rust bindings won't be able to call the member
+        //   function (there will be no instantiation of the member function in the C++
+        //   object files)
+        // - test/templates/definition_in_cc/ - the instantiation happens in the .cc
+        //   file and therefore the thunk is not *required* (but it doesn't hurt to have
+        //   the thunk)
+        assert_cc_matches!(
+            rs_api_impl,
+            quote! {
+                extern "C" class MyTemplate<int>
+                __rust_thunk___ZN10MyTemplateIiE6CreateEi___test_testing_target(int value) {
+                    return MyTemplate<int>::Create(std::forward<decltype(value)>(value));
+                }
+            }
+        );
+        assert_cc_matches!(
+            rs_api_impl,
+            quote! {
+                extern "C" int const&
+                __rust_thunk___ZNK10MyTemplateIiE5valueEv___test_testing_target(
+                        const class MyTemplate<int>*__this) {
+                    return __this->value();
+                }
+            }
+        );
+        Ok(())
+    }
+
+    #[test]
     fn test_simple_struct() -> Result<()> {
         let ir = ir_from_cc(&tokens_to_string(quote! {
             struct SomeStruct final {
diff --git a/rs_bindings_from_cc/support/offsetof.h b/rs_bindings_from_cc/support/offsetof.h
index 58876a0..2bbd611 100644
--- a/rs_bindings_from_cc/support/offsetof.h
+++ b/rs_bindings_from_cc/support/offsetof.h
@@ -47,7 +47,7 @@
 
 // CRUBIT_OFFSET_OF is a wrapper around the standard `offsetof` macro [1] that
 // adds support for using a type name (i.e. `T...`) that contains commas (e.g.
-// `TemplateClassWithTwoTemplateParameters<int, int>`).
+// `ClassTemplateWithTwoTemplateParameters<int, int>`).
 //
 // CRUBIT_OFFSET_OF doesn't require wrapping the type name in an extra set of
 // parens.  This aspect is achieved by making CRUBIT_OFFSET_OF a variadic macro
diff --git a/rs_bindings_from_cc/test/golden/doc_comment.h b/rs_bindings_from_cc/test/golden/doc_comment.h
index fc7dcea..411903f 100644
--- a/rs_bindings_from_cc/test/golden/doc_comment.h
+++ b/rs_bindings_from_cc/test/golden/doc_comment.h
@@ -71,4 +71,43 @@
 /// A type alias
 using MyTypeAlias = DocCommentSlashes;
 
+/// Class template.
+template <typename T>
+struct MyTemplate final {
+  /// A non-static member function.
+  const T& get_field_value() const { return value; }
+
+  /// Data member.
+  T value;
+};
+
+/// Class template specialization.
+template <>
+struct MyTemplate<float> final {
+  /// A non-static member function in a specialization.
+  const float& get_field_value() const { return value; }
+
+  /// Data member in a specialization.
+  float value;
+};
+
+/// Type alias to template instantiation.
+using MyInstantiation = MyTemplate<int>;
+
+/// Type alias to instantiation of a template specialization.
+using MySpecializedInstantiation = MyTemplate<float>;
+
+/// Class template with nested struct inside.
+template <typename T>
+struct OuterTemplate final {
+  /// Doc comment for the nested struct.
+  struct NestedStruct {
+    /// Data member in a nested struct.
+    T value;
+  };
+};
+
+/// Type alias to a struct nested in a template instantiation.
+using ConcreteNestedStruct = OuterTemplate<int>::NestedStruct;
+
 #endif  // CRUBIT_RS_BINDINGS_FROM_CC_TEST_GOLDEN_DOC_COMMENT_H_
diff --git a/rs_bindings_from_cc/test/golden/doc_comment_rs_api.rs b/rs_bindings_from_cc/test/golden/doc_comment_rs_api.rs
index 5def129..2d1b037 100644
--- a/rs_bindings_from_cc/test/golden/doc_comment_rs_api.rs
+++ b/rs_bindings_from_cc/test/golden/doc_comment_rs_api.rs
@@ -283,8 +283,136 @@
 /// A type alias
 pub type MyTypeAlias = crate::DocCommentSlashes;
 
+// rs_bindings_from_cc/test/golden/doc_comment.h;l=75
+// Error while generating bindings for item 'MyTemplate':
+// Class templates are not supported yet
+
+/// Class template specialization.
+#[derive(Clone, Copy)]
+#[repr(C)]
+pub struct __CcTemplateInst10MyTemplateIfE {
+    /// Data member in a specialization.
+    pub value: f32,
+}
+forward_declare::unsafe_define!(
+    forward_declare::symbol!("MyTemplate<float>"),
+    crate::__CcTemplateInst10MyTemplateIfE
+);
+
+impl Default for __CcTemplateInst10MyTemplateIfE {
+    #[inline(always)]
+    fn default() -> Self {
+        let mut tmp = crate::rust_std::mem::MaybeUninit::<Self>::zeroed();
+        unsafe {
+            crate::detail::__rust_thunk___ZN10MyTemplateIfEC1Ev___third_party_crubit_rs_bindings_from_cc_test_golden_doc_comment_cc(&mut tmp);
+            tmp.assume_init()
+        }
+    }
+}
+
+impl<'b> From<ctor::RvalueReference<'b, crate::__CcTemplateInst10MyTemplateIfE>>
+    for __CcTemplateInst10MyTemplateIfE
+{
+    #[inline(always)]
+    fn from(__param_0: ctor::RvalueReference<'b, crate::__CcTemplateInst10MyTemplateIfE>) -> Self {
+        let mut tmp = crate::rust_std::mem::MaybeUninit::<Self>::zeroed();
+        unsafe {
+            crate::detail::__rust_thunk___ZN10MyTemplateIfEC1EOS0____third_party_crubit_rs_bindings_from_cc_test_golden_doc_comment_cc(&mut tmp,__param_0);
+            tmp.assume_init()
+        }
+    }
+}
+
+// rs_bindings_from_cc/test/golden/doc_comment.h;l=86
+// Error while generating bindings for item 'MyTemplate<float>::operator=':
+// Bindings for this kind of operator are not supported
+
+// rs_bindings_from_cc/test/golden/doc_comment.h;l=86
+// Error while generating bindings for item 'MyTemplate<float>::operator=':
+// Bindings for this kind of operator are not supported
+
+impl __CcTemplateInst10MyTemplateIfE {
+    /// A non-static member function in a specialization.
+    #[inline(always)]
+    pub fn get_field_value<'a>(&'a self) -> &'a f32 {
+        unsafe {
+            crate::detail::__rust_thunk___ZNK10MyTemplateIfE15get_field_valueEv___third_party_crubit_rs_bindings_from_cc_test_golden_doc_comment_cc(self)
+        }
+    }
+}
+
+/// Type alias to template instantiation.
+pub type MyInstantiation = crate::__CcTemplateInst10MyTemplateIiE;
+
+/// Type alias to instantiation of a template specialization.
+pub type MySpecializedInstantiation = crate::__CcTemplateInst10MyTemplateIfE;
+
+// rs_bindings_from_cc/test/golden/doc_comment.h;l=101
+// Error while generating bindings for item 'OuterTemplate':
+// Class templates are not supported yet
+
+// rs_bindings_from_cc/test/golden/doc_comment.h;l=111
+// Error while generating bindings for item 'ConcreteNestedStruct':
+// Unsupported type 'OuterTemplate<int>::NestedStruct': No generated bindings found for 'NestedStruct'
+
 // CRUBIT_RS_BINDINGS_FROM_CC_TEST_GOLDEN_DOC_COMMENT_H_
 
+/// Class template.
+#[derive(Clone, Copy)]
+#[repr(C)]
+pub struct __CcTemplateInst10MyTemplateIiE {
+    /// Data member.
+    pub value: i32,
+}
+forward_declare::unsafe_define!(
+    forward_declare::symbol!("MyTemplate<int>"),
+    crate::__CcTemplateInst10MyTemplateIiE
+);
+
+impl Default for __CcTemplateInst10MyTemplateIiE {
+    #[inline(always)]
+    fn default() -> Self {
+        let mut tmp = crate::rust_std::mem::MaybeUninit::<Self>::zeroed();
+        unsafe {
+            crate::detail::__rust_thunk___ZN10MyTemplateIiEC1Ev___third_party_crubit_rs_bindings_from_cc_test_golden_doc_comment_cc(&mut tmp);
+            tmp.assume_init()
+        }
+    }
+}
+
+impl<'b> From<ctor::RvalueReference<'b, crate::__CcTemplateInst10MyTemplateIiE>>
+    for __CcTemplateInst10MyTemplateIiE
+{
+    #[inline(always)]
+    fn from(__param_0: ctor::RvalueReference<'b, crate::__CcTemplateInst10MyTemplateIiE>) -> Self {
+        let mut tmp = crate::rust_std::mem::MaybeUninit::<Self>::zeroed();
+        unsafe {
+            crate::detail::__rust_thunk___ZN10MyTemplateIiEC1EOS0____third_party_crubit_rs_bindings_from_cc_test_golden_doc_comment_cc(&mut tmp,__param_0);
+            tmp.assume_init()
+        }
+    }
+}
+
+// rs_bindings_from_cc/test/golden/doc_comment.h;l=76
+// Error while generating bindings for item 'MyTemplate<int>::operator=':
+// Bindings for this kind of operator are not supported
+
+// rs_bindings_from_cc/test/golden/doc_comment.h;l=76
+// Error while generating bindings for item 'MyTemplate<int>::operator=':
+// Bindings for this kind of operator are not supported
+
+// A non-static member function.
+
+impl __CcTemplateInst10MyTemplateIiE {
+    /// A non-static member function.
+    #[inline(always)]
+    pub fn get_field_value<'a>(&'a self) -> &'a i32 {
+        unsafe {
+            crate::detail::__rust_thunk___ZNK10MyTemplateIiE15get_field_valueEv___third_party_crubit_rs_bindings_from_cc_test_golden_doc_comment_cc(self)
+        }
+    }
+}
+
 mod detail {
     #[allow(unused_imports)]
     use super::*;
@@ -342,6 +470,48 @@
             __param_0: ctor::RvalueReference<'b, crate::MultilineOneStar>,
         );
         pub(crate) fn __rust_thunk___Z3foov() -> i32;
+        pub(crate) fn __rust_thunk___ZN10MyTemplateIfEC1Ev___third_party_crubit_rs_bindings_from_cc_test_golden_doc_comment_cc<
+            'a,
+        >(
+            __this: &'a mut crate::rust_std::mem::MaybeUninit<
+                crate::__CcTemplateInst10MyTemplateIfE,
+            >,
+        );
+        pub(crate) fn __rust_thunk___ZN10MyTemplateIfEC1EOS0____third_party_crubit_rs_bindings_from_cc_test_golden_doc_comment_cc<
+            'a,
+            'b,
+        >(
+            __this: &'a mut crate::rust_std::mem::MaybeUninit<
+                crate::__CcTemplateInst10MyTemplateIfE,
+            >,
+            __param_0: ctor::RvalueReference<'b, crate::__CcTemplateInst10MyTemplateIfE>,
+        );
+        pub(crate) fn __rust_thunk___ZNK10MyTemplateIfE15get_field_valueEv___third_party_crubit_rs_bindings_from_cc_test_golden_doc_comment_cc<
+            'a,
+        >(
+            __this: &'a crate::__CcTemplateInst10MyTemplateIfE,
+        ) -> &'a f32;
+        pub(crate) fn __rust_thunk___ZN10MyTemplateIiEC1Ev___third_party_crubit_rs_bindings_from_cc_test_golden_doc_comment_cc<
+            'a,
+        >(
+            __this: &'a mut crate::rust_std::mem::MaybeUninit<
+                crate::__CcTemplateInst10MyTemplateIiE,
+            >,
+        );
+        pub(crate) fn __rust_thunk___ZN10MyTemplateIiEC1EOS0____third_party_crubit_rs_bindings_from_cc_test_golden_doc_comment_cc<
+            'a,
+            'b,
+        >(
+            __this: &'a mut crate::rust_std::mem::MaybeUninit<
+                crate::__CcTemplateInst10MyTemplateIiE,
+            >,
+            __param_0: ctor::RvalueReference<'b, crate::__CcTemplateInst10MyTemplateIiE>,
+        );
+        pub(crate) fn __rust_thunk___ZNK10MyTemplateIiE15get_field_valueEv___third_party_crubit_rs_bindings_from_cc_test_golden_doc_comment_cc<
+            'a,
+        >(
+            __this: &'a crate::__CcTemplateInst10MyTemplateIiE,
+        ) -> &'a i32;
     }
 }
 
@@ -414,3 +584,37 @@
 };
 const _: () =
     assert!(memoffset_unstable_const::offset_of!(crate::MultilineOneStar, i) * 8 == 0usize);
+
+const _: () = assert!(rust_std::mem::size_of::<crate::__CcTemplateInst10MyTemplateIfE>() == 4usize);
+const _: () =
+    assert!(rust_std::mem::align_of::<crate::__CcTemplateInst10MyTemplateIfE>() == 4usize);
+const _: () = {
+    static_assertions::assert_impl_all!(crate::__CcTemplateInst10MyTemplateIfE: Clone);
+};
+const _: () = {
+    static_assertions::assert_impl_all!(crate::__CcTemplateInst10MyTemplateIfE: Copy);
+};
+const _: () = {
+    static_assertions::assert_not_impl_all!(crate::__CcTemplateInst10MyTemplateIfE: Drop);
+};
+const _: () = assert!(
+    memoffset_unstable_const::offset_of!(crate::__CcTemplateInst10MyTemplateIfE, value) * 8
+        == 0usize
+);
+
+const _: () = assert!(rust_std::mem::size_of::<crate::__CcTemplateInst10MyTemplateIiE>() == 4usize);
+const _: () =
+    assert!(rust_std::mem::align_of::<crate::__CcTemplateInst10MyTemplateIiE>() == 4usize);
+const _: () = {
+    static_assertions::assert_impl_all!(crate::__CcTemplateInst10MyTemplateIiE: Clone);
+};
+const _: () = {
+    static_assertions::assert_impl_all!(crate::__CcTemplateInst10MyTemplateIiE: Copy);
+};
+const _: () = {
+    static_assertions::assert_not_impl_all!(crate::__CcTemplateInst10MyTemplateIiE: Drop);
+};
+const _: () = assert!(
+    memoffset_unstable_const::offset_of!(crate::__CcTemplateInst10MyTemplateIiE, value) * 8
+        == 0usize
+);
diff --git a/rs_bindings_from_cc/test/golden/doc_comment_rs_api_impl.cc b/rs_bindings_from_cc/test/golden/doc_comment_rs_api_impl.cc
index 3c7a4ca..44a54bd 100644
--- a/rs_bindings_from_cc/test/golden/doc_comment_rs_api_impl.cc
+++ b/rs_bindings_from_cc/test/golden/doc_comment_rs_api_impl.cc
@@ -142,6 +142,76 @@
   return __this->operator=(std::forward<decltype(__param_0)>(__param_0));
 }
 extern "C" int __rust_thunk___Z3foov() { return foo(); }
+extern "C" void
+__rust_thunk___ZN10MyTemplateIiEC1Ev___third_party_crubit_rs_bindings_from_cc_test_golden_doc_comment_cc(
+    class MyTemplate<int>* __this) {
+  crubit::construct_at(std::forward<decltype(__this)>(__this));
+}
+extern "C" void
+__rust_thunk___ZN10MyTemplateIiEC1ERKS0____third_party_crubit_rs_bindings_from_cc_test_golden_doc_comment_cc(
+    class MyTemplate<int>* __this, const class MyTemplate<int>& __param_0) {
+  crubit::construct_at(std::forward<decltype(__this)>(__this),
+                       std::forward<decltype(__param_0)>(__param_0));
+}
+extern "C" void
+__rust_thunk___ZN10MyTemplateIiEC1EOS0____third_party_crubit_rs_bindings_from_cc_test_golden_doc_comment_cc(
+    class MyTemplate<int>* __this, class MyTemplate<int>&& __param_0) {
+  crubit::construct_at(std::forward<decltype(__this)>(__this),
+                       std::forward<decltype(__param_0)>(__param_0));
+}
+extern "C" void
+__rust_thunk___ZN10MyTemplateIiED1Ev___third_party_crubit_rs_bindings_from_cc_test_golden_doc_comment_cc(
+    class MyTemplate<int>* __this) {
+  std::destroy_at(std::forward<decltype(__this)>(__this));
+}
+extern "C" class MyTemplate<int>&
+__rust_thunk___ZN10MyTemplateIiEaSERKS0____third_party_crubit_rs_bindings_from_cc_test_golden_doc_comment_cc(
+    class MyTemplate<int>* __this, const class MyTemplate<int>& __param_0) {
+  return __this->operator=(std::forward<decltype(__param_0)>(__param_0));
+} extern "C" class MyTemplate<int>&
+__rust_thunk___ZN10MyTemplateIiEaSEOS0____third_party_crubit_rs_bindings_from_cc_test_golden_doc_comment_cc(
+    class MyTemplate<int>* __this, class MyTemplate<int>&& __param_0) {
+  return __this->operator=(std::forward<decltype(__param_0)>(__param_0));
+} extern "C" int const&
+__rust_thunk___ZNK10MyTemplateIiE15get_field_valueEv___third_party_crubit_rs_bindings_from_cc_test_golden_doc_comment_cc(
+    const class MyTemplate<int>* __this) {
+  return __this->get_field_value();
+}
+extern "C" void
+__rust_thunk___ZN10MyTemplateIfEC1Ev___third_party_crubit_rs_bindings_from_cc_test_golden_doc_comment_cc(
+    class MyTemplate<float>* __this) {
+  crubit::construct_at(std::forward<decltype(__this)>(__this));
+}
+extern "C" void
+__rust_thunk___ZN10MyTemplateIfEC1ERKS0____third_party_crubit_rs_bindings_from_cc_test_golden_doc_comment_cc(
+    class MyTemplate<float>* __this, const class MyTemplate<float>& __param_0) {
+  crubit::construct_at(std::forward<decltype(__this)>(__this),
+                       std::forward<decltype(__param_0)>(__param_0));
+}
+extern "C" void
+__rust_thunk___ZN10MyTemplateIfEC1EOS0____third_party_crubit_rs_bindings_from_cc_test_golden_doc_comment_cc(
+    class MyTemplate<float>* __this, class MyTemplate<float>&& __param_0) {
+  crubit::construct_at(std::forward<decltype(__this)>(__this),
+                       std::forward<decltype(__param_0)>(__param_0));
+}
+extern "C" void
+__rust_thunk___ZN10MyTemplateIfED1Ev___third_party_crubit_rs_bindings_from_cc_test_golden_doc_comment_cc(
+    class MyTemplate<float>* __this) {
+  std::destroy_at(std::forward<decltype(__this)>(__this));
+}
+extern "C" class MyTemplate<float>&
+__rust_thunk___ZN10MyTemplateIfEaSERKS0____third_party_crubit_rs_bindings_from_cc_test_golden_doc_comment_cc(
+    class MyTemplate<float>* __this, const class MyTemplate<float>& __param_0) {
+  return __this->operator=(std::forward<decltype(__param_0)>(__param_0));
+} extern "C" class MyTemplate<float>&
+__rust_thunk___ZN10MyTemplateIfEaSEOS0____third_party_crubit_rs_bindings_from_cc_test_golden_doc_comment_cc(
+    class MyTemplate<float>* __this, class MyTemplate<float>&& __param_0) {
+  return __this->operator=(std::forward<decltype(__param_0)>(__param_0));
+} extern "C" float const&
+__rust_thunk___ZNK10MyTemplateIfE15get_field_valueEv___third_party_crubit_rs_bindings_from_cc_test_golden_doc_comment_cc(
+    const class MyTemplate<float>* __this) {
+  return __this->get_field_value();
+}
 
 static_assert(sizeof(class DocCommentSlashes) == 4);
 static_assert(alignof(class DocCommentSlashes) == 4);
@@ -163,4 +233,12 @@
 static_assert(alignof(class MultilineOneStar) == 4);
 static_assert(CRUBIT_OFFSET_OF(i, class MultilineOneStar) * 8 == 0);
 
+static_assert(sizeof(class MyTemplate<int>) == 4);
+static_assert(alignof(class MyTemplate<int>) == 4);
+static_assert(CRUBIT_OFFSET_OF(value, class MyTemplate<int>) * 8 == 0);
+
+static_assert(sizeof(class MyTemplate<float>) == 4);
+static_assert(alignof(class MyTemplate<float>) == 4);
+static_assert(CRUBIT_OFFSET_OF(value, class MyTemplate<float>) * 8 == 0);
+
 #pragma clang diagnostic pop
diff --git a/rs_bindings_from_cc/test/golden/templates.h b/rs_bindings_from_cc/test/golden/templates.h
new file mode 100644
index 0000000..fae6253
--- /dev/null
+++ b/rs_bindings_from_cc/test/golden/templates.h
@@ -0,0 +1,34 @@
+// 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_GOLDEN_TEMPLATES_H_
+#define THIRD_PARTY_CRUBIT_RS_BINDINGS_FROM_CC_TEST_GOLDEN_TEMPLATES_H_
+
+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_;
+};
+
+using MyTypeAlias = MyTemplate<int>;
+using OtherTypeAliasInSameTarget = MyTemplate<int>;
+
+template <typename T1, typename T2>
+struct TemplateWithTwoParams {
+  T1 value1;
+  T2 value2;
+};
+
+using AliasToTemplateWithTwoParams = TemplateWithTwoParams<int, float>;
+
+#endif  // THIRD_PARTY_CRUBIT_RS_BINDINGS_FROM_CC_TEST_GOLDEN_TEMPLATES_H_
diff --git a/rs_bindings_from_cc/test/golden/templates_rs_api.rs b/rs_bindings_from_cc/test/golden/templates_rs_api.rs
new file mode 100644
index 0000000..33dc4ae
--- /dev/null
+++ b/rs_bindings_from_cc/test/golden/templates_rs_api.rs
@@ -0,0 +1,173 @@
+// 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
+
+// Automatically @generated Rust bindings for C++ target
+// //rs_bindings_from_cc/test/golden:templates_cc
+#![rustfmt::skip]
+#![feature(const_ptr_offset_from, custom_inner_attributes, negative_impls)]
+#![allow(non_camel_case_types)]
+#![allow(non_snake_case)]
+#![allow(non_upper_case_globals)]
+#![deny(warnings)]
+
+use ::std as rust_std;
+
+// 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
+
+// rs_bindings_from_cc/test/golden/templates.h;l=8
+// Error while generating bindings for item 'MyTemplate':
+// Class templates are not supported yet
+
+pub type MyTypeAlias = crate::__CcTemplateInst10MyTemplateIiE;
+
+pub type OtherTypeAliasInSameTarget = crate::__CcTemplateInst10MyTemplateIiE;
+
+// rs_bindings_from_cc/test/golden/templates.h;l=26
+// Error while generating bindings for item 'TemplateWithTwoParams':
+// Class templates are not supported yet
+
+pub type AliasToTemplateWithTwoParams = crate::__CcTemplateInst21TemplateWithTwoParamsIifE;
+
+// THIRD_PARTY_CRUBIT_RS_BINDINGS_FROM_CC_TEST_GOLDEN_TEMPLATES_H_
+
+#[repr(C)]
+pub struct __CcTemplateInst10MyTemplateIiE {
+    value_: i32,
+}
+forward_declare::unsafe_define!(
+    forward_declare::symbol!("MyTemplate<int>"),
+    crate::__CcTemplateInst10MyTemplateIiE
+);
+
+impl !Unpin for __CcTemplateInst10MyTemplateIiE {}
+
+// rs_bindings_from_cc/test/golden/templates.h;l=9
+// Error while generating bindings for item 'MyTemplate<int>::MyTemplate<int>':
+// Unsafe constructors (e.g. with no elided or explicit lifetimes) are intentionally not supported
+
+// rs_bindings_from_cc/test/golden/templates.h;l=9
+// Error while generating bindings for item 'MyTemplate<int>::MyTemplate<int>':
+// Unsafe constructors (e.g. with no elided or explicit lifetimes) are intentionally not supported
+
+// rs_bindings_from_cc/test/golden/templates.h;l=9
+// Error while generating bindings for item 'MyTemplate<int>::MyTemplate':
+// Parameter #0 is not supported: Unsupported type 'class MyTemplate<int> &&': Unsupported type: && without lifetime
+
+// rs_bindings_from_cc/test/golden/templates.h;l=9
+// Error while generating bindings for item 'MyTemplate<int>::operator=':
+// Bindings for this kind of operator are not supported
+
+// rs_bindings_from_cc/test/golden/templates.h;l=9
+// Error while generating bindings for item 'MyTemplate<int>::operator=':
+// Parameter #0 is not supported: Unsupported type 'class MyTemplate<int> &&': Unsupported type: && without lifetime
+
+impl __CcTemplateInst10MyTemplateIiE {
+    #[inline(always)]
+    pub fn Create(value: i32) -> crate::__CcTemplateInst10MyTemplateIiE {
+        unsafe {
+            crate::detail::__rust_thunk___ZN10MyTemplateIiE6CreateEi___third_party_crubit_rs_bindings_from_cc_test_golden_templates_cc(value)
+        }
+    }
+}
+
+impl __CcTemplateInst10MyTemplateIiE {
+    #[inline(always)]
+    pub unsafe fn value(__this: *const crate::__CcTemplateInst10MyTemplateIiE) -> *const i32 {
+        crate::detail::__rust_thunk___ZNK10MyTemplateIiE5valueEv___third_party_crubit_rs_bindings_from_cc_test_golden_templates_cc(__this)
+    }
+}
+
+#[repr(C)]
+pub struct __CcTemplateInst21TemplateWithTwoParamsIifE {
+    pub value1: i32,
+    pub value2: f32,
+}
+forward_declare::unsafe_define!(
+    forward_declare::symbol!("TemplateWithTwoParams<int, float>"),
+    crate::__CcTemplateInst21TemplateWithTwoParamsIifE
+);
+
+impl !Unpin for __CcTemplateInst21TemplateWithTwoParamsIifE {}
+
+// rs_bindings_from_cc/test/golden/templates.h;l=27
+// Error while generating bindings for item 'TemplateWithTwoParams<int, float>::TemplateWithTwoParams<int, float>':
+// Unsafe constructors (e.g. with no elided or explicit lifetimes) are intentionally not supported
+
+// rs_bindings_from_cc/test/golden/templates.h;l=27
+// Error while generating bindings for item 'TemplateWithTwoParams<int, float>::TemplateWithTwoParams<int, float>':
+// Unsafe constructors (e.g. with no elided or explicit lifetimes) are intentionally not supported
+
+// rs_bindings_from_cc/test/golden/templates.h;l=27
+// Error while generating bindings for item 'TemplateWithTwoParams<int, float>::TemplateWithTwoParams':
+// Parameter #0 is not supported: Unsupported type 'struct TemplateWithTwoParams<int, float> &&': Unsupported type: && without lifetime
+
+// rs_bindings_from_cc/test/golden/templates.h;l=27
+// Error while generating bindings for item 'TemplateWithTwoParams<int, float>::operator=':
+// Bindings for this kind of operator are not supported
+
+// rs_bindings_from_cc/test/golden/templates.h;l=27
+// Error while generating bindings for item 'TemplateWithTwoParams<int, float>::operator=':
+// Parameter #0 is not supported: Unsupported type 'struct TemplateWithTwoParams<int, float> &&': Unsupported type: && without lifetime
+
+mod detail {
+    #[allow(unused_imports)]
+    use super::*;
+    extern "C" {
+        pub(crate) fn __rust_thunk___ZN10MyTemplateIiE6CreateEi___third_party_crubit_rs_bindings_from_cc_test_golden_templates_cc(
+            value: i32,
+        ) -> crate::__CcTemplateInst10MyTemplateIiE;
+        pub(crate) fn __rust_thunk___ZNK10MyTemplateIiE5valueEv___third_party_crubit_rs_bindings_from_cc_test_golden_templates_cc(
+            __this: *const crate::__CcTemplateInst10MyTemplateIiE,
+        ) -> *const i32;
+    }
+}
+
+const _: () = assert!(rust_std::mem::size_of::<Option<&i32>>() == rust_std::mem::size_of::<&i32>());
+
+const _: () = assert!(rust_std::mem::size_of::<crate::__CcTemplateInst10MyTemplateIiE>() == 4usize);
+const _: () =
+    assert!(rust_std::mem::align_of::<crate::__CcTemplateInst10MyTemplateIiE>() == 4usize);
+const _: () = {
+    static_assertions::assert_not_impl_all!(crate::__CcTemplateInst10MyTemplateIiE: Copy);
+};
+const _: () = {
+    static_assertions::assert_not_impl_all!(crate::__CcTemplateInst10MyTemplateIiE: Drop);
+};
+const _: () = assert!(
+    memoffset_unstable_const::offset_of!(crate::__CcTemplateInst10MyTemplateIiE, value_) * 8
+        == 0usize
+);
+
+const _: () = assert!(
+    rust_std::mem::size_of::<crate::__CcTemplateInst21TemplateWithTwoParamsIifE>() == 8usize
+);
+const _: () = assert!(
+    rust_std::mem::align_of::<crate::__CcTemplateInst21TemplateWithTwoParamsIifE>() == 4usize
+);
+const _: () = {
+    static_assertions::assert_not_impl_all!(
+        crate::__CcTemplateInst21TemplateWithTwoParamsIifE: Copy
+    );
+};
+const _: () = {
+    static_assertions::assert_not_impl_all!(
+        crate::__CcTemplateInst21TemplateWithTwoParamsIifE: Drop
+    );
+};
+const _: () = assert!(
+    memoffset_unstable_const::offset_of!(
+        crate::__CcTemplateInst21TemplateWithTwoParamsIifE,
+        value1
+    ) * 8
+        == 0usize
+);
+const _: () = assert!(
+    memoffset_unstable_const::offset_of!(
+        crate::__CcTemplateInst21TemplateWithTwoParamsIifE,
+        value2
+    ) * 8
+        == 32usize
+);
diff --git a/rs_bindings_from_cc/test/golden/templates_rs_api_impl.cc b/rs_bindings_from_cc/test/golden/templates_rs_api_impl.cc
new file mode 100644
index 0000000..26f113e
--- /dev/null
+++ b/rs_bindings_from_cc/test/golden/templates_rs_api_impl.cc
@@ -0,0 +1,79 @@
+// 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 <cstddef>
+#include <memory>
+
+#include "rs_bindings_from_cc/support/cxx20_backports.h"
+#include "rs_bindings_from_cc/support/offsetof.h"
+#include "rs_bindings_from_cc/test/golden/templates.h"
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wthread-safety-analysis"
+extern "C" void
+__rust_thunk___ZN10MyTemplateIiEC1Ev___third_party_crubit_rs_bindings_from_cc_test_golden_templates_cc(
+    class MyTemplate<int>* __this) {
+  crubit::construct_at(std::forward<decltype(__this)>(__this));
+}
+extern "C" void
+__rust_thunk___ZN10MyTemplateIiEC1ERKS0____third_party_crubit_rs_bindings_from_cc_test_golden_templates_cc(
+    class MyTemplate<int>* __this, const class MyTemplate<int>& __param_0) {
+  crubit::construct_at(std::forward<decltype(__this)>(__this),
+                       std::forward<decltype(__param_0)>(__param_0));
+}
+extern "C" void
+__rust_thunk___ZN10MyTemplateIiED1Ev___third_party_crubit_rs_bindings_from_cc_test_golden_templates_cc(
+    class MyTemplate<int>* __this) {
+  std::destroy_at(std::forward<decltype(__this)>(__this));
+}
+extern "C" class MyTemplate<int>&
+__rust_thunk___ZN10MyTemplateIiEaSERKS0____third_party_crubit_rs_bindings_from_cc_test_golden_templates_cc(
+    class MyTemplate<int>* __this, const class MyTemplate<int>& __param_0) {
+  return __this->operator=(std::forward<decltype(__param_0)>(__param_0));
+} extern "C" class MyTemplate<int>
+__rust_thunk___ZN10MyTemplateIiE6CreateEi___third_party_crubit_rs_bindings_from_cc_test_golden_templates_cc(
+    int value) {
+  return MyTemplate<int>::Create(std::forward<decltype(value)>(value));
+} extern "C" int const&
+__rust_thunk___ZNK10MyTemplateIiE5valueEv___third_party_crubit_rs_bindings_from_cc_test_golden_templates_cc(
+    const class MyTemplate<int>* __this) {
+  return __this->value();
+}
+extern "C" void
+__rust_thunk___ZN21TemplateWithTwoParamsIifEC1Ev___third_party_crubit_rs_bindings_from_cc_test_golden_templates_cc(
+    class TemplateWithTwoParams<int, float>* __this) {
+  crubit::construct_at(std::forward<decltype(__this)>(__this));
+}
+extern "C" void
+__rust_thunk___ZN21TemplateWithTwoParamsIifEC1ERKS0____third_party_crubit_rs_bindings_from_cc_test_golden_templates_cc(
+    class TemplateWithTwoParams<int, float>* __this,
+    const class TemplateWithTwoParams<int, float>& __param_0) {
+  crubit::construct_at(std::forward<decltype(__this)>(__this),
+                       std::forward<decltype(__param_0)>(__param_0));
+}
+extern "C" void
+__rust_thunk___ZN21TemplateWithTwoParamsIifED1Ev___third_party_crubit_rs_bindings_from_cc_test_golden_templates_cc(
+    class TemplateWithTwoParams<int, float>* __this) {
+  std::destroy_at(std::forward<decltype(__this)>(__this));
+}
+extern "C" class TemplateWithTwoParams<int, float>&
+__rust_thunk___ZN21TemplateWithTwoParamsIifEaSERKS0____third_party_crubit_rs_bindings_from_cc_test_golden_templates_cc(
+    class TemplateWithTwoParams<int, float>* __this,
+    const class TemplateWithTwoParams<int, float>& __param_0) {
+  return __this->operator=(std::forward<decltype(__param_0)>(__param_0));
+}
+
+static_assert(sizeof(class MyTemplate<int>) == 4);
+static_assert(alignof(class MyTemplate<int>) == 4);
+
+static_assert(sizeof(class TemplateWithTwoParams<int, float>) == 8);
+static_assert(alignof(class TemplateWithTwoParams<int, float>) == 4);
+static_assert(
+    CRUBIT_OFFSET_OF(value1, class TemplateWithTwoParams<int, float>) * 8 == 0);
+static_assert(CRUBIT_OFFSET_OF(value2,
+                               class TemplateWithTwoParams<int, float>) *
+                  8 ==
+              32);
+
+#pragma clang diagnostic pop
diff --git a/rs_bindings_from_cc/test/templates/definition_in_cc/BUILD b/rs_bindings_from_cc/test/templates/definition_in_cc/BUILD
new file mode 100644
index 0000000..78b841d
--- /dev/null
+++ b/rs_bindings_from_cc/test/templates/definition_in_cc/BUILD
@@ -0,0 +1,17 @@
+"""End-to-end example of using type aliases to fully-instantiated templates."""
+
+load("@rules_rust//rust:defs.bzl", "rust_test")
+
+licenses(["notice"])
+
+cc_library(
+    name = "definition_in_cc",
+    srcs = ["definition_in_cc.cc"],
+    hdrs = ["definition_in_cc.h"],
+)
+
+rust_test(
+    name = "main",
+    srcs = ["test.rs"],
+    cc_deps = [":definition_in_cc"],
+)
diff --git a/rs_bindings_from_cc/test/templates/definition_in_cc/definition_in_cc.cc b/rs_bindings_from_cc/test/templates/definition_in_cc/definition_in_cc.cc
new file mode 100644
index 0000000..48853e3
--- /dev/null
+++ b/rs_bindings_from_cc/test/templates/definition_in_cc/definition_in_cc.cc
@@ -0,0 +1,20 @@
+// 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/definition_in_cc/definition_in_cc.h"
+
+// static
+template <typename T>
+MyTemplate<T> MyTemplate<T>::Create(T value) {
+  MyTemplate<T> result;
+  result.value_ = value;
+  return result;
+}
+
+template <typename T>
+const T& MyTemplate<T>::value() const {
+  return value_;
+}
+
+template class MyTemplate<int>;
diff --git a/rs_bindings_from_cc/test/templates/definition_in_cc/definition_in_cc.h b/rs_bindings_from_cc/test/templates/definition_in_cc/definition_in_cc.h
new file mode 100644
index 0000000..00bb591
--- /dev/null
+++ b/rs_bindings_from_cc/test/templates/definition_in_cc/definition_in_cc.h
@@ -0,0 +1,22 @@
+// 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 CRUBIT_RS_BINDINGS_FROM_CC_TEST_TEMPLATES_DEFINITION_IN_CC_DEFINITION_IN_CC_H_
+#define CRUBIT_RS_BINDINGS_FROM_CC_TEST_TEMPLATES_DEFINITION_IN_CC_DEFINITION_IN_CC_H_
+
+#pragma clang lifetime_elision
+
+template <typename T>
+class MyTemplate {
+ public:
+  static MyTemplate Create(T value);
+  const T& value() const;
+
+ private:
+  T value_;
+};
+
+using MyTypeAlias = MyTemplate<int>;
+
+#endif  // CRUBIT_RS_BINDINGS_FROM_CC_TEST_TEMPLATES_DEFINITION_IN_CC_DEFINITION_IN_CC_H_
diff --git a/rs_bindings_from_cc/test/templates/definition_in_cc/test.rs b/rs_bindings_from_cc/test/templates/definition_in_cc/test.rs
new file mode 100644
index 0000000..6ae0841
--- /dev/null
+++ b/rs_bindings_from_cc/test/templates/definition_in_cc/test.rs
@@ -0,0 +1,12 @@
+// 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 {
+    #[test]
+    fn test_member_function_of_class_template_defined_in_cc_file() {
+        let s = definition_in_cc::MyTypeAlias::Create(123);
+        assert_eq!(123, *s.value());
+    }
+}
diff --git a/rs_bindings_from_cc/test/templates/no_instantiation_in_template_target/BUILD b/rs_bindings_from_cc/test/templates/no_instantiation_in_template_target/BUILD
new file mode 100644
index 0000000..b9b456c
--- /dev/null
+++ b/rs_bindings_from_cc/test/templates/no_instantiation_in_template_target/BUILD
@@ -0,0 +1,26 @@
+"""End-to-end example of using type aliases that refer to fully-instantiated
+templates in a different target (where the target with the header doesn't
+have this particular instantiation)."""
+
+load("@rules_rust//rust:defs.bzl", "rust_test")
+
+licenses(["notice"])
+
+cc_library(
+    name = "template_without_instantiation",
+    hdrs = ["template_without_instantiation.h"],
+)
+
+cc_library(
+    name = "type_alias_in_different_target",
+    hdrs = ["type_alias_in_different_target.h"],
+    deps = [":template_without_instantiation"],
+)
+
+rust_test(
+    name = "main",
+    srcs = ["test.rs"],
+    cc_deps = [
+        ":type_alias_in_different_target",
+    ],
+)
diff --git a/rs_bindings_from_cc/test/templates/no_instantiation_in_template_target/template_without_instantiation.h b/rs_bindings_from_cc/test/templates/no_instantiation_in_template_target/template_without_instantiation.h
new file mode 100644
index 0000000..ba97a74
--- /dev/null
+++ b/rs_bindings_from_cc/test/templates/no_instantiation_in_template_target/template_without_instantiation.h
@@ -0,0 +1,27 @@
+// 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 CRUBIT_RS_BINDINGS_FROM_CC_TEST_TEMPLATES_NO_INSTANTIATION_IN_TEMPLATE_TARGET_TEMPLATE_WITHOUT_INSTANTIATION_H_
+#define CRUBIT_RS_BINDINGS_FROM_CC_TEST_TEMPLATES_NO_INSTANTIATION_IN_TEMPLATE_TARGET_TEMPLATE_WITHOUT_INSTANTIATION_H_
+
+#pragma clang lifetime_elision
+
+// This template is not instantiated anywhere in this header file
+// (this is what the test scenario exercised here cares about).
+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_;
+};
+
+#endif  // CRUBIT_RS_BINDINGS_FROM_CC_TEST_TEMPLATES_NO_INSTANTIATION_IN_TEMPLATE_TARGET_TEMPLATE_WITHOUT_INSTANTIATION_H_
diff --git a/rs_bindings_from_cc/test/templates/no_instantiation_in_template_target/test.rs b/rs_bindings_from_cc/test/templates/no_instantiation_in_template_target/test.rs
new file mode 100644
index 0000000..f66a5ad
--- /dev/null
+++ b/rs_bindings_from_cc/test/templates/no_instantiation_in_template_target/test.rs
@@ -0,0 +1,12 @@
+// 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 {
+    #[test]
+    fn test_alias_to_template_without_instantiation_in_different_target() {
+        let s = type_alias_in_different_target::TypeAliasInDifferentTarget::Create(321);
+        assert_eq!(321, *s.value());
+    }
+}
diff --git a/rs_bindings_from_cc/test/templates/no_instantiation_in_template_target/type_alias_in_different_target.h b/rs_bindings_from_cc/test/templates/no_instantiation_in_template_target/type_alias_in_different_target.h
new file mode 100644
index 0000000..af31f17
--- /dev/null
+++ b/rs_bindings_from_cc/test/templates/no_instantiation_in_template_target/type_alias_in_different_target.h
@@ -0,0 +1,12 @@
+// 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 CRUBIT_RS_BINDINGS_FROM_CC_TEST_TEMPLATES_NO_INSTANTIATION_IN_TEMPLATE_TARGET_TYPE_ALIAS_IN_DIFFERENT_TARGET_H_
+#define CRUBIT_RS_BINDINGS_FROM_CC_TEST_TEMPLATES_NO_INSTANTIATION_IN_TEMPLATE_TARGET_TYPE_ALIAS_IN_DIFFERENT_TARGET_H_
+
+#include "rs_bindings_from_cc/test/templates/no_instantiation_in_template_target/template_without_instantiation.h"
+
+using TypeAliasInDifferentTarget = MyTemplate<int>;
+
+#endif  // CRUBIT_RS_BINDINGS_FROM_CC_TEST_TEMPLATES_NO_INSTANTIATION_IN_TEMPLATE_TARGET_TYPE_ALIAS_IN_DIFFERENT_TARGET_H_
diff --git a/rs_bindings_from_cc/test/templates/non_type_template_params/BUILD b/rs_bindings_from_cc/test/templates/non_type_template_params/BUILD
new file mode 100644
index 0000000..f7a226b
--- /dev/null
+++ b/rs_bindings_from_cc/test/templates/non_type_template_params/BUILD
@@ -0,0 +1,16 @@
+"""End-to-end example of using type aliases to fully-instantiated templates."""
+
+load("@rules_rust//rust:defs.bzl", "rust_test")
+
+licenses(["notice"])
+
+cc_library(
+    name = "non_type_template_params",
+    hdrs = ["non_type_template_params.h"],
+)
+
+rust_test(
+    name = "main",
+    srcs = ["test.rs"],
+    cc_deps = [":non_type_template_params"],
+)
diff --git a/rs_bindings_from_cc/test/templates/non_type_template_params/non_type_template_params.h b/rs_bindings_from_cc/test/templates/non_type_template_params/non_type_template_params.h
new file mode 100644
index 0000000..67afd7e
--- /dev/null
+++ b/rs_bindings_from_cc/test/templates/non_type_template_params/non_type_template_params.h
@@ -0,0 +1,19 @@
+// 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 DEVTOOLS_RUST_CC_INTEROP_RS_BINDINGS_FROM_CC_TEST_TEMPLATES_NON_TYPE_TEMPLATE_PARAMS_NON_TYPE_TEMPLATE_PARAMS_H_
+#define DEVTOOLS_RUST_CC_INTEROP_RS_BINDINGS_FROM_CC_TEST_TEMPLATES_NON_TYPE_TEMPLATE_PARAMS_NON_TYPE_TEMPLATE_PARAMS_H_
+
+#pragma clang lifetime_elision
+
+template <int multiplier>
+class MyTemplate {
+ public:
+  static int Multiply(int value) { return value * multiplier; }
+};
+
+using MyMultiplierX100 = MyTemplate<100>;
+using MyMultiplierX1000 = MyTemplate<1000>;
+
+#endif  // DEVTOOLS_RUST_CC_INTEROP_RS_BINDINGS_FROM_CC_TEST_TEMPLATES_NON_TYPE_TEMPLATE_PARAMS_NON_TYPE_TEMPLATE_PARAMS_H_
diff --git a/rs_bindings_from_cc/test/templates/non_type_template_params/test.rs b/rs_bindings_from_cc/test/templates/non_type_template_params/test.rs
new file mode 100644
index 0000000..b5a3c90
--- /dev/null
+++ b/rs_bindings_from_cc/test/templates/non_type_template_params/test.rs
@@ -0,0 +1,14 @@
+// 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 non_type_template_params::*;
+
+    #[test]
+    fn test_non_type_template_params() {
+        assert_eq!(123 * 100, MyMultiplierX100::Multiply(123));
+        assert_eq!(123 * 1000, MyMultiplierX1000::Multiply(123));
+    }
+}
diff --git a/rs_bindings_from_cc/test/templates/out_of_line_definition/BUILD b/rs_bindings_from_cc/test/templates/out_of_line_definition/BUILD
new file mode 100644
index 0000000..064826c
--- /dev/null
+++ b/rs_bindings_from_cc/test/templates/out_of_line_definition/BUILD
@@ -0,0 +1,16 @@
+"""End-to-end example of using type aliases to fully-instantiated templates."""
+
+load("@rules_rust//rust:defs.bzl", "rust_test")
+
+licenses(["notice"])
+
+cc_library(
+    name = "out_of_line_definition",
+    hdrs = ["out_of_line_definition.h"],
+)
+
+rust_test(
+    name = "main",
+    srcs = ["test.rs"],
+    cc_deps = [":out_of_line_definition"],
+)
diff --git a/rs_bindings_from_cc/test/templates/out_of_line_definition/out_of_line_definition.h b/rs_bindings_from_cc/test/templates/out_of_line_definition/out_of_line_definition.h
new file mode 100644
index 0000000..ab21f17
--- /dev/null
+++ b/rs_bindings_from_cc/test/templates/out_of_line_definition/out_of_line_definition.h
@@ -0,0 +1,34 @@
+// 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 CRUBIT_RS_BINDINGS_FROM_CC_TEST_TEMPLATES_OUT_OF_LINE_DEFINITION_OUT_OF_LINE_DEFINITION_H_
+#define CRUBIT_RS_BINDINGS_FROM_CC_TEST_TEMPLATES_OUT_OF_LINE_DEFINITION_OUT_OF_LINE_DEFINITION_H_
+
+#pragma clang lifetime_elision
+
+template <typename T>
+class MyTemplate {
+ public:
+  static MyTemplate Create(T value);
+  const T& value() const;
+
+ private:
+  T value_;
+};
+
+template <typename T>
+MyTemplate<T> MyTemplate<T>::Create(T value) {
+  MyTemplate<T> result;
+  result.value_ = value;
+  return result;
+}
+
+template <typename T>
+const T& MyTemplate<T>::value() const {
+  return value_;
+}
+
+using MyTypeAlias = MyTemplate<int>;
+
+#endif  // CRUBIT_RS_BINDINGS_FROM_CC_TEST_TEMPLATES_OUT_OF_LINE_DEFINITION_OUT_OF_LINE_DEFINITION_H_
diff --git a/rs_bindings_from_cc/test/templates/out_of_line_definition/test.rs b/rs_bindings_from_cc/test/templates/out_of_line_definition/test.rs
new file mode 100644
index 0000000..94589b9
--- /dev/null
+++ b/rs_bindings_from_cc/test/templates/out_of_line_definition/test.rs
@@ -0,0 +1,12 @@
+// 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 {
+    #[test]
+    fn test_member_function_of_class_template_defined_out_of_line_in_h_file() {
+        let s = out_of_line_definition::MyTypeAlias::Create(123);
+        assert_eq!(123, *s.value());
+    }
+}
diff --git a/rs_bindings_from_cc/test/templates/two_template_parameters/BUILD b/rs_bindings_from_cc/test/templates/two_template_parameters/BUILD
new file mode 100644
index 0000000..12f84f7
--- /dev/null
+++ b/rs_bindings_from_cc/test/templates/two_template_parameters/BUILD
@@ -0,0 +1,16 @@
+"""End-to-end example of using type aliases to fully-instantiated templates."""
+
+load("@rules_rust//rust:defs.bzl", "rust_test")
+
+licenses(["notice"])
+
+cc_library(
+    name = "two_template_parameters",
+    hdrs = ["two_template_parameters.h"],
+)
+
+rust_test(
+    name = "main",
+    srcs = ["test.rs"],
+    cc_deps = [":two_template_parameters"],
+)
diff --git a/rs_bindings_from_cc/test/templates/two_template_parameters/test.rs b/rs_bindings_from_cc/test/templates/two_template_parameters/test.rs
new file mode 100644
index 0000000..cddd66d
--- /dev/null
+++ b/rs_bindings_from_cc/test/templates/two_template_parameters/test.rs
@@ -0,0 +1,14 @@
+// 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 {
+    #[test]
+    fn test_two_template_parameters() {
+        let s =
+            two_template_parameters::AliasToTemplateWithTwoParams { value1: 123, value2: 456.789 };
+        assert_eq!(123, s.value1);
+        assert_eq!(456.789, s.value2);
+    }
+}
diff --git a/rs_bindings_from_cc/test/templates/two_template_parameters/two_template_parameters.h b/rs_bindings_from_cc/test/templates/two_template_parameters/two_template_parameters.h
new file mode 100644
index 0000000..02e47e0
--- /dev/null
+++ b/rs_bindings_from_cc/test/templates/two_template_parameters/two_template_parameters.h
@@ -0,0 +1,18 @@
+// 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 CRUBIT_RS_BINDINGS_FROM_CC_TEST_TEMPLATES_TYPE_ALIAS_TYPE_ALIAS_H_
+#define CRUBIT_RS_BINDINGS_FROM_CC_TEST_TEMPLATES_TYPE_ALIAS_TYPE_ALIAS_H_
+
+#pragma clang lifetime_elision
+
+template <typename T1, typename T2>
+struct TemplateWithTwoParams {
+  T1 value1;
+  T2 value2;
+};
+
+using AliasToTemplateWithTwoParams = TemplateWithTwoParams<int, float>;
+
+#endif  // CRUBIT_RS_BINDINGS_FROM_CC_TEST_TEMPLATES_TYPE_ALIAS_TYPE_ALIAS_H_
diff --git a/rs_bindings_from_cc/test/templates/type_alias/BUILD b/rs_bindings_from_cc/test/templates/type_alias/BUILD
new file mode 100644
index 0000000..7b98ab9
--- /dev/null
+++ b/rs_bindings_from_cc/test/templates/type_alias/BUILD
@@ -0,0 +1,25 @@
+"""End-to-end example of using type aliases to fully-instantiated templates."""
+
+load("@rules_rust//rust:defs.bzl", "rust_test")
+
+licenses(["notice"])
+
+cc_library(
+    name = "type_alias",
+    hdrs = ["type_alias.h"],
+)
+
+cc_library(
+    name = "type_alias_in_different_target",
+    hdrs = ["type_alias_in_different_target.h"],
+    deps = [":type_alias"],
+)
+
+rust_test(
+    name = "main",
+    srcs = ["test.rs"],
+    cc_deps = [
+        ":type_alias",
+        ":type_alias_in_different_target",
+    ],
+)
diff --git a/rs_bindings_from_cc/test/templates/type_alias/test.rs b/rs_bindings_from_cc/test/templates/type_alias/test.rs
new file mode 100644
index 0000000..8137e26
--- /dev/null
+++ b/rs_bindings_from_cc/test/templates/type_alias/test.rs
@@ -0,0 +1,28 @@
+// 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 {
+    #[test]
+    fn test_alias_to_template_instantiation() {
+        let s = type_alias::MyTypeAlias::Create(123);
+        assert_eq!(123, *s.value());
+    }
+
+    #[test]
+    fn test_aliases_in_same_target_are_compatible() {
+        let s: type_alias::MyTypeAlias = type_alias::MyTypeAlias::Create(456);
+        let s2: type_alias::OtherTypeAliasInSameTarget = s;
+        assert_eq!(456, *s2.value());
+    }
+
+    #[test]
+    fn test_alias_in_different_target_than_template() {
+        let s = type_alias_in_different_target::TypeAliasInDifferentTarget::Create(789);
+        assert_eq!(789, *s.value());
+
+        // TODO: Test cross-target bridging:
+        // let s2: type_alias::MyTypeAlias = s.bridge_into();
+    }
+}
diff --git a/rs_bindings_from_cc/test/templates/type_alias/type_alias.h b/rs_bindings_from_cc/test/templates/type_alias/type_alias.h
new file mode 100644
index 0000000..e55d96d
--- /dev/null
+++ b/rs_bindings_from_cc/test/templates/type_alias/type_alias.h
@@ -0,0 +1,28 @@
+// 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 CRUBIT_RS_BINDINGS_FROM_CC_TEST_TEMPLATES_TYPE_ALIAS_TYPE_ALIAS_H_
+#define CRUBIT_RS_BINDINGS_FROM_CC_TEST_TEMPLATES_TYPE_ALIAS_TYPE_ALIAS_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_;
+};
+
+using MyTypeAlias = MyTemplate<int>;
+using OtherTypeAliasInSameTarget = MyTemplate<int>;
+
+#endif  // CRUBIT_RS_BINDINGS_FROM_CC_TEST_TEMPLATES_TYPE_ALIAS_TYPE_ALIAS_H_
diff --git a/rs_bindings_from_cc/test/templates/type_alias/type_alias_in_different_target.h b/rs_bindings_from_cc/test/templates/type_alias/type_alias_in_different_target.h
new file mode 100644
index 0000000..e8df930
--- /dev/null
+++ b/rs_bindings_from_cc/test/templates/type_alias/type_alias_in_different_target.h
@@ -0,0 +1,12 @@
+// 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_TYPE_ALIAS_IN_DIFFERENT_TARGET_H_
+#define THIRD_PARTY_CRUBIT_RS_BINDINGS_FROM_CC_TEST_TEMPLATES_TYPE_ALIAS_IN_DIFFERENT_TARGET_H_
+
+#include "rs_bindings_from_cc/test/templates/type_alias/type_alias.h"
+
+using TypeAliasInDifferentTarget = MyTemplate<int>;
+
+#endif  // THIRD_PARTY_CRUBIT_RS_BINDINGS_FROM_CC_TEST_TEMPLATES_TYPE_ALIAS_IN_DIFFERENT_TARGET_H_