Add a `rs_name` field to `IncompleteRecord`

So far we used `cc_name` in the generated bindings, assuming that it will be a valid identifier on the `.rs` side. This is however not true for forward declared class template specializations.

PiperOrigin-RevId: 456578468
diff --git a/rs_bindings_from_cc/importers/cxx_record.cc b/rs_bindings_from_cc/importers/cxx_record.cc
index 8eccb6d..4ecad22 100644
--- a/rs_bindings_from_cc/importers/cxx_record.cc
+++ b/rs_bindings_from_cc/importers/cxx_record.cc
@@ -5,7 +5,6 @@
 #include "rs_bindings_from_cc/importers/cxx_record.h"
 
 #include "absl/strings/match.h"
-#include "absl/strings/substitute.h"
 #include "rs_bindings_from_cc/ast_convert.h"
 #include "clang/AST/ASTContext.h"
 #include "clang/AST/CXXInheritance.h"
@@ -112,6 +111,7 @@
     ictx_.MarkAsSuccessfullyImported(record_decl);
     return IncompleteRecord{
         .cc_name = std::move(cc_name),
+        .rs_name = std::move(rs_name),
         .id = GenerateItemId(record_decl),
         .owning_target = ictx_.GetOwningTarget(record_decl),
         // We generate top level bindings for implicit class template
diff --git a/rs_bindings_from_cc/ir.cc b/rs_bindings_from_cc/ir.cc
index 97f3f2e..bc8544d 100644
--- a/rs_bindings_from_cc/ir.cc
+++ b/rs_bindings_from_cc/ir.cc
@@ -362,6 +362,7 @@
 llvm::json::Value IncompleteRecord::ToJson() const {
   llvm::json::Object record{
       {"cc_name", cc_name},
+      {"rs_name", rs_name},
       {"id", id},
       {"owning_target", owning_target},
   };
diff --git a/rs_bindings_from_cc/ir.h b/rs_bindings_from_cc/ir.h
index a5ec007..777166a 100644
--- a/rs_bindings_from_cc/ir.h
+++ b/rs_bindings_from_cc/ir.h
@@ -613,6 +613,7 @@
 struct IncompleteRecord {
   llvm::json::Value ToJson() const;
   std::string cc_name;
+  std::string rs_name;
   ItemId id;
   BazelLabel owning_target;
   llvm::Optional<ItemId> enclosing_namespace_id;
diff --git a/rs_bindings_from_cc/ir.rs b/rs_bindings_from_cc/ir.rs
index cfc213b..b354bf6 100644
--- a/rs_bindings_from_cc/ir.rs
+++ b/rs_bindings_from_cc/ir.rs
@@ -373,6 +373,7 @@
 #[derive(Debug, PartialEq, Eq, Hash, Clone, Deserialize)]
 pub struct IncompleteRecord {
     pub cc_name: String,
+    pub rs_name: String,
     pub id: ItemId,
     pub owning_target: BazelLabel,
     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 3c0809c..5fc7c4d 100644
--- a/rs_bindings_from_cc/ir_from_cc_test.rs
+++ b/rs_bindings_from_cc/ir_from_cc_test.rs
@@ -3334,3 +3334,33 @@
         }
     );
 }
+
+#[test]
+fn test_incomplete_record_has_rs_name() {
+    let ir = ir_from_cc(
+        r#"
+        namespace test_namespace_bindings {
+          template <typename T>
+          struct MyTemplate {
+            void processT(T t);
+          };
+
+          struct Param {};
+
+          template<> struct MyTemplate<Param>;
+        }"#,
+    )
+    .unwrap();
+
+    assert_ir_matches!(
+        ir,
+        quote! {
+            ...
+            IncompleteRecord {
+              cc_name: "test_namespace_bindings::MyTemplate<test_namespace_bindings::Param>",
+              rs_name: "__CcTemplateInstN23test_namespace_bindings10MyTemplateINS_5ParamEEE",
+              ...
+            } ...
+        }
+    );
+}
diff --git a/rs_bindings_from_cc/src_code_gen.rs b/rs_bindings_from_cc/src_code_gen.rs
index 7cda76d..d4dee7f 100644
--- a/rs_bindings_from_cc/src_code_gen.rs
+++ b/rs_bindings_from_cc/src_code_gen.rs
@@ -992,8 +992,8 @@
 
 /// Generates Rust source code for a given incomplete record declaration.
 fn generate_incomplete_record(incomplete_record: &IncompleteRecord) -> Result<TokenStream> {
-    let ident = make_rs_ident(&incomplete_record.cc_name);
-    let name = &incomplete_record.cc_name;
+    let ident = make_rs_ident(&incomplete_record.rs_name);
+    let name = &incomplete_record.rs_name;
     Ok(quote! {
         forward_declare::forward_declare!(
             pub #ident __SPACE__ = __SPACE__ forward_declare::symbol!(#name)
@@ -2181,7 +2181,7 @@
                 namespace_qualifier,
                 crate_ident,
             } => {
-                let record_ident = make_rs_ident(&incomplete_record.cc_name);
+                let record_ident = make_rs_ident(&incomplete_record.rs_name);
                 let namespace_idents = namespace_qualifier.iter();
                 match crate_ident {
                     Some(ci) => {
@@ -6106,4 +6106,36 @@
         );
         Ok(())
     }
+
+    #[test]
+    fn test_forward_declared_class_template_specialization_symbol() -> Result<()> {
+        let rs_api = generate_bindings_tokens(&ir_from_cc(
+            r#"
+            namespace test_namespace_bindings {
+              template <typename T>
+              struct MyTemplate {
+                void processT(T t);
+              };
+
+              struct Param {};
+
+              template<> struct MyTemplate<Param>;
+            }"#,
+        )?)?
+        .rs_api;
+
+        assert_rs_matches!(
+            rs_api,
+            quote! {
+                ...
+                pub mod test_namespace_bindings {
+                    ...
+                    forward_declare::forward_declare!(pub __CcTemplateInstN23test_namespace_bindings10MyTemplateINS_5ParamEEE = forward_declare::symbol!("__CcTemplateInstN23test_namespace_bindings10MyTemplateINS_5ParamEEE"));
+                    ...
+                }
+                ...
+            }
+        );
+        Ok(())
+    }
 }
diff --git a/rs_bindings_from_cc/test/golden/templates.h b/rs_bindings_from_cc/test/golden/templates.h
index e3eb416..0e6c0a9 100644
--- a/rs_bindings_from_cc/test/golden/templates.h
+++ b/rs_bindings_from_cc/test/golden/templates.h
@@ -54,4 +54,9 @@
 using TopLevelTemplateWithNonTopLevelParam =
     MyTopLevelTemplate<test_namespace_bindings::TemplateParam>;
 
+template <>
+struct MyTopLevelTemplate<int>;
+
+void processForwardDeclaredSpecialization(MyTopLevelTemplate<int>* i);
+
 #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
index 6a5068d..37825d7 100644
--- a/rs_bindings_from_cc/test/golden/templates_rs_api.rs
+++ b/rs_bindings_from_cc/test/golden/templates_rs_api.rs
@@ -268,6 +268,17 @@
 pub type TopLevelTemplateWithNonTopLevelParam =
     crate::__CcTemplateInst18MyTopLevelTemplateIN23test_namespace_bindings13TemplateParamEE;
 
+forward_declare::forward_declare!(pub __CcTemplateInst18MyTopLevelTemplateIiE = forward_declare::symbol!("__CcTemplateInst18MyTopLevelTemplateIiE"));
+
+#[inline(always)]
+pub fn processForwardDeclaredSpecialization<'a>(
+    i: Option<crate::rust_std::pin::Pin<&'a mut crate::__CcTemplateInst18MyTopLevelTemplateIiE>>,
+) {
+    unsafe {
+        crate::detail::__rust_thunk___Z36processForwardDeclaredSpecializationP18MyTopLevelTemplateIiE(i)
+    }
+}
+
 // THIRD_PARTY_CRUBIT_RS_BINDINGS_FROM_CC_TEST_GOLDEN_TEMPLATES_H_
 
 #[ctor::recursively_pinned]
@@ -1266,6 +1277,14 @@
             >,
             __param_0: ctor::RvalueReference<'b, crate::test_namespace_bindings::TemplateParam>,
         ) -> crate::rust_std::pin::Pin<&'a mut crate::test_namespace_bindings::TemplateParam>;
+        #[link_name = "_Z36processForwardDeclaredSpecializationP18MyTopLevelTemplateIiE"]
+        pub(crate) fn __rust_thunk___Z36processForwardDeclaredSpecializationP18MyTopLevelTemplateIiE<
+            'a,
+        >(
+            i: Option<
+                crate::rust_std::pin::Pin<&'a mut crate::__CcTemplateInst18MyTopLevelTemplateIiE>,
+            >,
+        );
         pub(crate) fn __rust_thunk___ZN23test_namespace_bindings10MyTemplateI14DifferentScopeEC1Ev__2f_2fthird_5fparty_2fcrubit_2frs_5fbindings_5ffrom_5fcc_2ftest_2fgolden_3atemplates_5fcc<
             'a,
         >(