Support calling Rust-ABI functions through the generated bindings.

Before this CL the tool would only generate meaningful code in the
..._cc_api.h file.  This was sufficient for invoking `pub extern "C" fn`
Rust functions (simply redeclaring `#[no_mangle]` functions in C++,
or defining a small, `inline` C++ function that forwards to a slightly
renamed `#[export_name = ...]` function).

After this CL the tool will also populate ..._cc_api_impl.rs file with
Rust thunks that expose `pub fn` Rust functions that use the default
Rust ABI (rather than using `extern "C"` to ask for C ABI).  A small,
`inline` C++ function is generated to call into the think (quite similar
to how the `#[export_name = ...]` case is handled).

The thunks expose the same parameter and return types as the original
function via C ABI.  (Currently the tool only supports a limited set of
parameter types and return types - all of the supported types can be
directly used in an `extedn "C"` function/thunk.)

PiperOrigin-RevId: 489328746
diff --git a/cc_bindings_from_rs/bindings.rs b/cc_bindings_from_rs/bindings.rs
index 742f61c..f6d940e 100644
--- a/cc_bindings_from_rs/bindings.rs
+++ b/cc_bindings_from_rs/bindings.rs
@@ -3,10 +3,10 @@
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 
 use anyhow::{bail, Context, Result};
-use code_gen_utils::{format_cc_ident, format_cc_includes, CcInclude};
+use code_gen_utils::{format_cc_ident, format_cc_includes, make_rs_ident, CcInclude};
 use itertools::Itertools;
 use proc_macro2::TokenStream;
-use quote::quote;
+use quote::{format_ident, quote};
 use rustc_hir::{Item, ItemKind, Node, Unsafety};
 use rustc_interface::Queries;
 use rustc_middle::dep_graph::DepContext;
@@ -39,24 +39,25 @@
             quote! { __COMMENT__ #txt __NEWLINE__ }
         };
 
-        let h_body = {
-            let crate_content = format_crate(tcx).unwrap_or_else(|err| {
-                let txt = format!("Failed to generate bindings for the crate: {err}");
-                quote! { __COMMENT__ #txt }
-            });
+        let Self { h_body, rs_body } = format_crate(tcx).unwrap_or_else(|err| {
+            let txt = format!("Failed to generate bindings for the crate: {err}");
+            let src = quote! { __COMMENT__ #txt };
+            Self { h_body: src.clone(), rs_body: src }
+        });
+
+        let h_body = quote! {
+            #top_comment
+
             // TODO(b/251445877): Replace `#pragma once` with include guards.
-            quote! {
-                #top_comment
-                __HASH_TOKEN__ pragma once __NEWLINE__
-                __NEWLINE__
-                #crate_content
-            }
+            __HASH_TOKEN__ pragma once __NEWLINE__
+            __NEWLINE__
+
+            #h_body
         };
 
         let rs_body = quote! {
             #top_comment
-
-            // TODO(b/254097223): Include Rust thunks here.
+            #rs_body
         };
 
         Ok(Self { h_body, rs_body })
@@ -231,7 +232,6 @@
 
 /// Formats `ty` for Rust (e.g. to be used as a type of a parameter in a Rust
 /// thunk inside `..._cc_api_impl.rs`).
-#[allow(dead_code)] // TODO(b/254097223): Use from `format_fn`.
 fn format_ty_for_rs(ty: Ty) -> Result<TokenStream> {
     Ok(match ty.kind() {
         ty::TyKind::Bool
@@ -293,21 +293,30 @@
     api: TokenStream,
 
     /// Internal implementation details for `..._cc_api.h` file (e.g.
-    /// declarations of Rust thunks, `static_assert`s about `struct` layout,
-    /// etc.).
-    internals: Option<TokenStream>,
-    // TODO(b/254097223): Add `impl_: Option<TokenStream>` to carry Rust thunks.
+    /// declarations of Rust thunks).
+    cc_impl: Option<TokenStream>,
+
+    /// Rust implementation of the bindings:
+    /// - Definitions of FFI-friendly `extern "C"` thunks that C++ can call
+    ///   into.
+    /// - Static / `const` assrtions about layout of Rust structs
+    rs_impl: Option<TokenStream>,
 }
 
 impl BindingsSnippet {
     fn new() -> Self {
-        Self { includes: BTreeSet::new(), api: quote! {}, internals: None }
+        Self { includes: BTreeSet::new(), api: quote! {}, cc_impl: None, rs_impl: None }
     }
 }
 
 impl AddAssign for BindingsSnippet {
     fn add_assign(&mut self, rhs: Self) {
-        let Self { includes: mut rhs_includes, api: rhs_api, internals: rhs_internals } = rhs;
+        let Self {
+            includes: mut rhs_includes,
+            api: rhs_api,
+            cc_impl: rhs_cc_impl,
+            rs_impl: rhs_rs_impl,
+        } = rhs;
 
         self.includes.append(&mut rhs_includes);
         self.api.extend(rhs_api);
@@ -326,7 +335,8 @@
                 }
             }
         }
-        self.internals = concat_optional_tokens(self.internals.take(), rhs_internals);
+        self.cc_impl = concat_optional_tokens(self.cc_impl.take(), rhs_cc_impl);
+        self.rs_impl = concat_optional_tokens(self.rs_impl.take(), rhs_rs_impl);
     }
 }
 
@@ -356,7 +366,7 @@
     let def_id: DefId = def_id.to_def_id(); // Convert LocalDefId to DefId.
 
     let item_name = tcx.item_name(def_id);
-    let symbol_name = {
+    let mut symbol_name = {
         // Call to `mono` is ok - doc comment requires no generic parameters (although
         // lifetime parameters would have been okay).
         let instance = ty::Instance::mono(tcx, def_id);
@@ -381,6 +391,7 @@
         }
     }
 
+    let needs_thunk: bool;
     match sig.abi {
         // "C" ABI is okay: Before https://rust-lang.github.io/rfcs/2945-c-unwind-abi.html a Rust
         // panic that "escapes" a "C" ABI function leads to Undefined Behavior.  This is
@@ -389,66 +400,110 @@
         //
         // After https://rust-lang.github.io/rfcs/2945-c-unwind-abi.html a Rust panic that
         // tries to "escape" a "C" ABI function will terminate the program.  This is okay.
-        Abi::C { unwind: false } => (),
+        Abi::C { unwind: false } => {
+            needs_thunk = false;
+        },
 
         // "C-unwind" ABI is okay: After https://rust-lang.github.io/rfcs/2945-c-unwind-abi.html a
         // new "C-unwind" ABI may be used by Rust functions that want to safely propagate Rust
         // panics through frames that may belong to another language.
-        Abi::C { unwind: true } => (),
+        Abi::C { unwind: true } => {
+            needs_thunk = false;
+        },
 
-        // TODO(b/254097223): Add support for Rust thunks.
-        _ => bail!("Non-C ABI is not supported yet (b/254097223)"),
+        // All other ABIs trigger thunk generation.  This covers Rust ABI functions, but
+        // also ABIs that theoretically are understood both by C++ and Rust (e.g. see
+        // `format_cc_call_conv_as_clang_attribute` in `rs_bindings_from_cc/src_code_gen.rs`).
+        _ => {
+            let thunk_name = format!("__crubit_thunk_{}", symbol_name.name);
+            symbol_name = ty::SymbolName::new(tcx, &thunk_name);
+            needs_thunk = true;
+        },
     };
 
     let mut includes = BTreeSet::new();
-    let ret_type = format_ret_ty_for_cc(sig.output())
-        .context("Error formatting function return type")?
-        .into_tokens(&mut includes);
-    let fn_name = format_cc_ident(item_name.as_str()).context("Error formatting function name")?;
-    let arg_names = tcx
-        .fn_arg_names(def_id)
-        .iter()
-        .enumerate()
-        .map(|(index, ident)| {
-            format_cc_ident(ident.as_str())
-                .unwrap_or_else(|_err| format_cc_ident(&format!("__param_{index}")).unwrap())
-        })
-        .collect_vec();
-    let arg_types = sig
-        .inputs()
-        .iter()
-        .enumerate()
-        .map(|(index, ty)| Ok(
-            format_ty_for_cc(*ty)
-                .with_context(|| format!("Error formatting the type of parameter #{index}"))?
-                .into_tokens(&mut includes)
-        ))
-        .collect::<Result<Vec<_>>>()?;
     let api: TokenStream;
-    let internals: Option<TokenStream>;
-    if item_name.as_str() == symbol_name.name {
-        api = quote! {
-            extern "C" #ret_type #fn_name (
-                    #( #arg_types #arg_names ),*
-            );
-        };
-        internals = None;
+    let cc_impl: Option<TokenStream>;
+    {
+        let ret_type = format_ret_ty_for_cc(sig.output())
+            .context("Error formatting function return type")?
+            .into_tokens(&mut includes);
+        let fn_name =
+            format_cc_ident(item_name.as_str()).context("Error formatting function name")?;
+        let arg_names = tcx
+            .fn_arg_names(def_id)
+            .iter()
+            .enumerate()
+            .map(|(index, ident)| {
+                format_cc_ident(ident.as_str())
+                    .unwrap_or_else(|_err| format_cc_ident(&format!("__param_{index}")).unwrap())
+            })
+            .collect_vec();
+        let arg_types = sig
+            .inputs()
+            .iter()
+            .enumerate()
+            .map(|(index, ty)| {
+                Ok(format_ty_for_cc(*ty)
+                    .with_context(|| format!("Error formatting the type of parameter #{index}"))?
+                    .into_tokens(&mut includes))
+            })
+            .collect::<Result<Vec<_>>>()?;
+        if item_name.as_str() == symbol_name.name {
+            api = quote! {
+                extern "C" #ret_type #fn_name (
+                        #( #arg_types #arg_names ),*
+                );
+            };
+            cc_impl = None;
+        } else {
+            let exported_name =
+                format_cc_ident(symbol_name.name).context("Error formatting exported name")?;
+            api = quote! {
+                inline #ret_type #fn_name (
+                        #( #arg_types #arg_names ),* ) {
+                    return :: __crubit_internal :: #exported_name( #( #arg_names ),* );
+                }
+            };
+            cc_impl = Some(quote! {
+                extern "C" #ret_type #exported_name (
+                        #( #arg_types #arg_names ),*
+                );
+            });
+        }
+    }
+
+    let rs_impl = if !needs_thunk {
+        None
     } else {
-        let exported_name =
-            format_cc_ident(symbol_name.name).context("Error formatting exported name")?;
-        api = quote! {
-            inline #ret_type #fn_name (
-                    #( #arg_types #arg_names ),* ) {
-                return :: __crubit_internal :: #exported_name( #( #arg_names ),* );
+        let crate_name = make_rs_ident(tcx.crate_name(LOCAL_CRATE).as_str());
+        let fn_name = make_rs_ident(item_name.as_str());
+        let exported_name = make_rs_ident(symbol_name.name);
+        let ret_type = format_ty_for_rs(sig.output())?;
+        let arg_names = tcx
+            .fn_arg_names(def_id)
+            .iter()
+            .enumerate()
+            .map(|(index, ident)| {
+                if ident.as_str().is_empty() {
+                    format_ident!("__param_{index}")
+                } else {
+                    make_rs_ident(ident.as_str())
+                }
+            })
+            .collect_vec();
+        let arg_types =
+            sig.inputs().iter().copied().map(format_ty_for_rs).collect::<Result<Vec<_>>>()?;
+        Some({
+            quote! {
+                #[no_mangle]
+                extern "C" fn #exported_name( #( #arg_names: #arg_types ),* ) -> #ret_type {
+                    #crate_name :: #fn_name( #( #arg_names ),* )
+                }
             }
-        };
-        internals = Some(quote! {
-            extern "C" #ret_type #exported_name (
-                    #( #arg_types #arg_names ),*
-            );
-        });
+        })
     };
-    Ok(BindingsSnippet { includes, api, internals })
+    Ok(BindingsSnippet { includes, api, cc_impl, rs_impl })
 }
 
 /// Formats a Rust item idenfied by `def_id`.
@@ -496,7 +551,7 @@
 }
 
 /// Formats all public items from the Rust crate being compiled.
-fn format_crate(tcx: TyCtxt) -> Result<TokenStream> {
+fn format_crate(tcx: TyCtxt) -> Result<GeneratedBindings> {
     let snippets: BindingsSnippet = tcx
         .hir()
         .items()
@@ -526,8 +581,8 @@
             }
         }
     };
-    let internals = {
-        match snippets.internals {
+    let cc_impl = {
+        match snippets.cc_impl {
             None => quote! {},
             Some(details_body) => quote! {
                 namespace __crubit_internal {
@@ -537,11 +592,13 @@
             },
         }
     };
-    Ok(quote! {
+    let h_body = quote! {
         #includes __NEWLINE__
-        #internals
+        #cc_impl
         #api
-    })
+    };
+    let rs_body = snippets.rs_impl.unwrap_or_else(|| quote! {});
+    Ok(GeneratedBindings { h_body, rs_body })
 }
 
 #[cfg(test)]
@@ -560,7 +617,9 @@
     use rustc_span::def_id::LocalDefId;
     use std::path::PathBuf;
 
-    use token_stream_matchers::{assert_cc_matches, assert_cc_not_matches, assert_rs_not_matches};
+    use token_stream_matchers::{
+        assert_cc_matches, assert_cc_not_matches, assert_rs_matches, assert_rs_not_matches,
+    };
 
     pub fn get_sysroot_for_testing() -> PathBuf {
         let runfiles = runfiles::Runfiles::create().unwrap();
@@ -671,8 +730,8 @@
 
     #[test]
     fn test_generated_bindings_fn_export_name() {
-        // Coverage of how `BindingsSnippet::internals` are propagated when there are no
-        // `BindingsSnippet::impl` (e.g. no Rust thunks are needed).
+        // Coverage of how `BindingsSnippet::cc_impl` are propagated when there are no
+        // `BindingsSnippet::rs_impl` (e.g. no Rust thunks are needed).
         let test_src = r#"
                 #[export_name = "export_name"]
                 pub extern "C" fn public_function(x: f64, y: f64) -> f64 { x + y }
@@ -807,7 +866,8 @@
         test_format_def(test_src, "public_function", |result| {
             let result = result.expect("Test expects success here");
             assert!(result.includes.is_empty());
-            assert!(result.internals.is_none());
+            assert!(result.cc_impl.is_none());
+            assert!(result.rs_impl.is_none());
             assert_cc_matches!(
                 result.api,
                 quote! {
@@ -832,7 +892,8 @@
         test_format_def(test_src, "explicit_unit_return_type", |result| {
             let result = result.expect("Test expects success here");
             assert!(result.includes.is_empty());
-            assert!(result.internals.is_none());
+            assert!(result.cc_impl.is_none());
+            assert!(result.rs_impl.is_none());
             assert_cc_matches!(
                 result.api,
                 quote! {
@@ -857,7 +918,8 @@
             // details).
             let result = result.expect("Test expects success here");
             assert!(result.includes.is_empty());
-            assert!(result.internals.is_none());
+            assert!(result.cc_impl.is_none());
+            assert!(result.rs_impl.is_none());
             assert_cc_matches!(
                 result.api,
                 quote! {
@@ -879,6 +941,7 @@
         test_format_def(test_src, "public_function", |result| {
             let result = result.expect("Test expects success here");
             assert!(result.includes.is_empty());
+            assert!(result.rs_impl.is_none());
             assert_cc_matches!(
                 result.api,
                 quote! {
@@ -888,7 +951,7 @@
                 }
             );
             assert_cc_matches!(
-                result.internals.expect("This test expects separate extern-C decl"),
+                result.cc_impl.expect("This test expects separate extern-C decl"),
                 quote! {
                     extern "C" double ...(double x, double y);
                 }
@@ -905,6 +968,7 @@
         test_format_def(test_src, "public_function", |result| {
             let result = result.expect("Test expects success here");
             assert!(result.includes.is_empty());
+            assert!(result.rs_impl.is_none());
             assert_cc_matches!(
                 result.api,
                 quote! {
@@ -914,7 +978,7 @@
                 }
             );
             assert_cc_matches!(
-                result.internals.expect("This test expects separate extern-C decl"),
+                result.cc_impl.expect("This test expects separate extern-C decl"),
                 quote! {
                     extern "C" double export_name(double x, double y);
                 }
@@ -950,13 +1014,37 @@
                 pub const fn foo(i: i32) -> i32 { i * 42 }
             "#;
         test_format_def(test_src, "foo", |result| {
-            // TODO(lukasza): Update test expectations below once `const fn` example from
-            // the testcase doesn't just error out (and is instead supported as
-            // a non-`consteval` binding).
             // TODO(b/254095787): Update test expectations below once `const fn` from Rust
             // is translated into a `consteval` C++ function.
-            let err = result.expect_err("Test expects an error here");
-            assert_eq!(err, "Non-C ABI is not supported yet (b/254097223)");
+            let result = result.expect("Test expects success here");
+            assert!(!result.includes.is_empty());
+            assert!(result.cc_impl.is_some());
+            let thunk_name = quote!{ __crubit_thunk__RNvCsgl0yn9Ytt4z_8rust_out3foo };
+            assert_cc_matches!(
+                result.cc_impl.unwrap(),
+                quote! {
+                    extern "C" std::int32_t #thunk_name( std::int32_t i);
+                }
+            );
+            assert_cc_matches!(
+                result.api,
+                quote! {
+                    inline std::int32_t foo(std::int32_t i) {
+                        return ::__crubit_internal::#thunk_name(i);
+                    }
+                }
+            );
+            assert!(result.rs_impl.is_some());
+            assert_rs_matches!(
+                result.rs_impl.unwrap(),
+                quote! {
+                    #[no_mangle]
+                    extern "C"
+                    fn #thunk_name(i: i32) -> i32 {
+                        rust_out::foo(i)
+                    }
+                }
+            );
         });
     }
 
@@ -972,7 +1060,8 @@
         test_format_def(test_src, "may_throw", |result| {
             let result = result.expect("Test expects success here");
             assert!(result.includes.is_empty());
-            assert!(result.internals.is_none());
+            assert!(result.cc_impl.is_none());
+            assert!(result.rs_impl.is_none());
             assert_cc_matches!(
                 result.api,
                 quote! {
@@ -998,7 +1087,8 @@
         test_format_def(test_src, "type_aliased_return", |result| {
             let result = result.expect("Test expects success here");
             assert!(result.includes.is_empty());
-            assert!(result.internals.is_none());
+            assert!(result.cc_impl.is_none());
+            assert!(result.rs_impl.is_none());
             assert_cc_matches!(
                 result.api,
                 quote! {
@@ -1129,19 +1219,101 @@
             "#;
         test_format_def(test_src, "async_function", |result| {
             let err = result.expect_err("Test expects an error here");
-            assert_eq!(err, "Non-C ABI is not supported yet (b/254097223)");
+            assert_eq!(err, "Error formatting function return type: \
+                             The following Rust type is not supported yet: \
+                             impl std::future::Future<Output = ()>");
         });
     }
 
     #[test]
-    fn test_format_def_unsupported_fn_non_c_abi() {
+    fn test_format_def_fn_rust_abi() {
         let test_src = r#"
-                pub fn default_rust_abi_function() {}
+                pub fn add(x: f64, y: f64) -> f64 { x * y }
             "#;
-        test_format_def(test_src, "default_rust_abi_function", |result| {
-            let err = result.expect_err("Test expects an error here");
-            assert_eq!(err, "Non-C ABI is not supported yet (b/254097223)");
-        })
+        test_format_def(test_src, "add", |result| {
+            let result = result.expect("Test expects success here");
+            assert!(result.includes.is_empty());
+            assert!(result.cc_impl.is_some());
+            let thunk_name = quote! { __crubit_thunk__RNvCsgl0yn9Ytt4z_8rust_out3add };
+            assert_cc_matches!(
+                result.cc_impl.unwrap(),
+                quote! {
+                    extern "C" double #thunk_name(double x, double y);
+                }
+            );
+            assert_cc_matches!(
+                result.api,
+                quote! {
+                    inline double add(double x, double y) {
+                        return ::__crubit_internal::#thunk_name(x, y);
+                    }
+                }
+            );
+            assert!(result.rs_impl.is_some());
+            assert_rs_matches!(
+                result.rs_impl.unwrap(),
+                quote! {
+                    #[no_mangle]
+                    extern "C"
+                    fn #thunk_name(x: f64, y: f64) -> f64 {
+                        rust_out::add(x, y)
+                    }
+                }
+            );
+        });
+    }
+
+    #[test]
+    fn test_format_def_fn_vectorcall_abi() {
+        // This tests a function call that is not a C-ABI, and is not the default Rust
+        // ABI.  It can't use "stdcall", because it is not supported on the
+        // targets where Crubit's tests run.  So, it ended up using
+        // "vectorcall".
+        //
+        // This test almost entirely replicates `test_format_def_fn_rust_abi`, except
+        // for the `extern "vectorcall"` part in the `test_src` test input.
+        //
+        // This test verifies the current behavior that gives reasonable and functional
+        // FFI bindings.  OTOH, in the future we may decide to avoid having the
+        // extra thunk for cases where the given non-C-ABI function call
+        // convention is supported by both C++ and Rust
+        // (see also `format_cc_call_conv_as_clang_attribute` in
+        // `rs_bindings_from_cc/src_code_gen.rs`)
+        let test_src = r#"
+                #![feature(abi_vectorcall)]
+                pub extern "vectorcall" fn add(x: f64, y: f64) -> f64 { x * y }
+            "#;
+        test_format_def(test_src, "add", |result| {
+            let result = result.expect("Test expects success here");
+            assert!(result.includes.is_empty());
+            assert!(result.cc_impl.is_some());
+            let thunk_name = quote! { __crubit_thunk__RNvCsgl0yn9Ytt4z_8rust_out3add };
+            assert_cc_matches!(
+                result.cc_impl.unwrap(),
+                quote! {
+                    extern "C" double #thunk_name(double x, double y);
+                }
+            );
+            assert_cc_matches!(
+                result.api,
+                quote! {
+                    inline double add(double x, double y) {
+                        return ::__crubit_internal::#thunk_name(x, y);
+                    }
+                }
+            );
+            assert!(result.rs_impl.is_some());
+            assert_rs_matches!(
+                result.rs_impl.unwrap(),
+                quote! {
+                    #[no_mangle]
+                    extern "C"
+                    fn #thunk_name(x: f64, y: f64) -> f64 {
+                        rust_out::add(x, y)
+                    }
+                }
+            );
+        });
     }
 
     #[test]
@@ -1169,7 +1341,8 @@
         test_format_def(test_src, "foo", |result| {
             let result = result.expect("Test expects success here");
             assert!(result.includes.is_empty());
-            assert!(result.internals.is_none());
+            assert!(result.cc_impl.is_none());
+            assert!(result.rs_impl.is_none());
             assert_cc_matches!(
                 result.api,
                 quote! {
@@ -1189,7 +1362,8 @@
         test_format_def(test_src, "some_function", |result| {
             let result = result.expect("Test expects success here");
             assert!(result.includes.is_empty());
-            assert!(result.internals.is_none());
+            assert!(result.cc_impl.is_none());
+            assert!(result.rs_impl.is_none());
             assert_cc_matches!(
                 result.api,
                 quote! {
@@ -1200,32 +1374,44 @@
     }
 
     #[test]
-    fn test_format_def_fn_export_name_with_anonymous_parameter_names() {
+    fn test_format_def_fn_with_multiple_anonymous_parameter_names() {
         let test_src = r#"
-                #[export_name = "export_name"]
-                pub extern "C" fn public_function(_: f64, _: f64) {}
+                pub fn foo(_: f64, _: f64) {}
             "#;
-        test_format_def(test_src, "public_function", |result| {
+        test_format_def(test_src, "foo", |result| {
             let result = result.expect("Test expects success here");
             assert!(result.includes.is_empty());
+            assert!(result.cc_impl.is_some());
             assert_cc_matches!(
-                result.api,
+                result.cc_impl.unwrap(),
                 quote! {
-                    inline void public_function(double __param_0, double __param_1) {
-                        return ::__crubit_internal::export_name(__param_0, __param_1);
-                    }
+                    extern "C" void ...(
+                        double __param_0, double __param_1);
                 }
             );
             assert_cc_matches!(
-                result.internals.expect("This test expects separate extern-C decl"),
+                result.api,
                 quote! {
-                    extern "C" void export_name(double __param_0, double __param_1);
+                    inline void foo(double __param_0, double __param_1) {
+                        return ::__crubit_internal
+                               ::...(
+                                   __param_0, __param_1);
+                    }
+                }
+            );
+            assert!(result.rs_impl.is_some());
+            assert_rs_matches!(
+                result.rs_impl.unwrap(),
+                quote! {
+                    #[no_mangle]
+                    extern "C" fn ...(__param_0: f64, __param_1: f64) -> () {
+                        rust_out::foo(__param_0, __param_1)
+                    }
                 }
             );
         });
     }
 
-
     #[test]
     fn test_format_def_unsupported_fn_param_type() {
         let test_src = r#"
@@ -1247,12 +1433,9 @@
                 pub fn fn_with_params(_param: ()) {}
             "#;
         test_format_def(test_src, "fn_with_params", |result| {
-            // TODO(b/254097223): Change the expectations once Rust-ABI functions are
-            // supported. Note that the test cannot use `extern "C"` in the
-            // meantime, because `()` is not FFI-safe (i.e. Rust won't allow
-            // using it with `extern "C"`).
             let err = result.expect_err("Test expects an error here");
-            assert_eq!(err, "Non-C ABI is not supported yet (b/254097223)");
+            assert_eq!(err, "Error formatting the type of parameter #0: \
+                             The unit type `()` / `void` is only supported as a return type");
         });
     }
 
@@ -1642,6 +1825,7 @@
     {
         use rustc_session::config::{
             CodegenOptions, CrateType, Input, Options, OutputType, OutputTypes,
+            SymbolManglingVersion,
         };
 
         const TEST_FILENAME: &str = "crubit_unittests.rs";
@@ -1663,7 +1847,17 @@
                 ("stable_features".to_string(), rustc_lint_defs::Level::Allow),
             ],
             cg: CodegenOptions {
+                // As pointed out in `panics_and_exceptions.md` the tool only supports `-C
+                // panic=abort` and therefore we explicitly opt into this config for tests.
                 panic: Some(rustc_target::spec::PanicStrategy::Abort),
+                // To simplify how unit tests are authored we force a specific mangling algorithm -
+                // this way the tests can hardcode mangled-name-depdendent expectations (e.g. names
+                // of thunks expected in test output).  The value below has been chosen based on
+                // <internal link>/llvm-coverage-instrumentation.html#rust-symbol-mangling which
+                // points out that `v0` mangling can be used to "ensure consistent and reversible
+                // name mangling" in situations when "mangled names must be consistent across
+                // compilations".
+                symbol_mangling_version: Some(SymbolManglingVersion::V0),
                 ..Default::default()
             },
             ..Default::default()
diff --git a/cc_bindings_from_rs/cc_bindings_from_rs.rs b/cc_bindings_from_rs/cc_bindings_from_rs.rs
index 6226426..692260b 100644
--- a/cc_bindings_from_rs/cc_bindings_from_rs.rs
+++ b/cc_bindings_from_rs/cc_bindings_from_rs.rs
@@ -311,8 +311,7 @@
             let rs_input_path = self.tempdir.path().join("test_crate.rs");
             std::fs::write(
                 &rs_input_path,
-                r#" #[no_mangle]
-                    pub extern "C" fn public_function() {
+                r#" pub fn public_function() {
                         private_function()
                     }
 
@@ -330,6 +329,9 @@
             args.extend([
                 "--".to_string(),
                 format!("--codegen=panic={}", &self.panic_mechanism),
+                // See comments about `SymbolManglingVersion::V0`in `test::run_compiler` in
+                // `bindings.rs` for rationale behind using `symbol-mangling-version=v0`.
+                "--codegen=symbol-mangling-version=v0".to_string(),
                 "--crate-type=lib".to_string(),
                 format!("--sysroot={}", get_sysroot_for_testing().display()),
                 rs_input_path.display().to_string(),
@@ -356,9 +358,16 @@
 
 #pragma once
 
+namespace __crubit_internal {
+extern "C" void
+__crubit_thunk__RNvCslKXfKXPWofF_10test_crate15public_function();
+}
 namespace test_crate {
-extern "C" void public_function();
-}"#
+inline void public_function() {
+  return ::__crubit_internal::
+      __crubit_thunk__RNvCslKXfKXPWofF_10test_crate15public_function();
+}
+}  // namespace test_crate"#
         );
 
         assert!(test_result.rs_path.exists());
@@ -367,6 +376,11 @@
             rs_body,
             r#"// Automatically @generated C++ bindings for the following Rust crate:
 // test_crate
+
+#[no_mangle]
+extern "C" fn __crubit_thunk__RNvCslKXfKXPWofF_10test_crate15public_function() -> () {
+    test_crate::public_function()
+}
 "#
         );
         Ok(())
diff --git a/cc_bindings_from_rs/test/functions/functions.rs b/cc_bindings_from_rs/test/functions/functions.rs
index a4e76f9..f1cee94 100644
--- a/cc_bindings_from_rs/test/functions/functions.rs
+++ b/cc_bindings_from_rs/test/functions/functions.rs
@@ -31,6 +31,14 @@
     x + y
 }
 
+pub fn add_i32_via_rust_abi(x: i32, y: i32) -> i32 {
+    x + y
+}
+
+pub fn add_i32_via_rust_abi_with_duplicated_param_names(x: i32, y: i32, _: i32, _: i32) -> i32 {
+    x + y
+}
+
 static G_I32: Mutex<i32> = Mutex::new(0);
 
 // Presence of the API below tests how bindings handle functions returning
diff --git a/cc_bindings_from_rs/test/functions/functions_test.cc b/cc_bindings_from_rs/test/functions/functions_test.cc
index 8fa8ba4..6870222 100644
--- a/cc_bindings_from_rs/test/functions/functions_test.cc
+++ b/cc_bindings_from_rs/test/functions/functions_test.cc
@@ -32,7 +32,7 @@
 }
 
 TEST(FunctionsTest, AddInt32ViaExternCWithMangling) {
-  // TODO(b/254097223): Uncomment the test assertion below after ensuring that
+  // TODO(b/258449205): Uncomment the test assertion below after ensuring that
   // the `genrule` in `test/functions/BUILD` invokes `cc_bindings_from_rs` with
   // the same rustc cmdline flags as when `rustc` is used to build
   // `functions.rs` for `rust_library`.  Otherwise, the mangled name will be
@@ -42,6 +42,15 @@
   // EXPECT_EQ(12 + 34, add_i32_via_extern_c_with_mangling(12, 34));
 }
 
+TEST(FunctionsTest, AddInt32ViaRustAbi) {
+  EXPECT_EQ(12 + 34, add_i32_via_rust_abi(12, 34));
+}
+
+TEST(FunctionsTest, AddInt32ViaRustAbiWithDuplicatedParamNames) {
+  EXPECT_EQ(12 + 34,
+            add_i32_via_rust_abi_with_duplicated_param_names(12, 34, 56, 78));
+}
+
 TEST(FunctionsTest, VoidReturningFunctionWithExportName) {
   set_global_i32_via_extern_c_with_export_name(123);
   EXPECT_EQ(123, get_global_i32_via_extern_c_with_export_name());