Provide conversion operators for `Into` implementations of types.

This requires modifying our infrastructure to support traits with
generics. I've kept this conservative as there are some open design
questions on full generics support. We
only support generating trait thunks with type generic parameters and
those types should not contain free variables.

PiperOrigin-RevId: 815902729
Change-Id: I7431d06e28b94dcf6ceee133d0d33c720c2904e4
diff --git a/cc_bindings_from_rs/generate_bindings/database/sugared_ty.rs b/cc_bindings_from_rs/generate_bindings/database/sugared_ty.rs
index 8889fd3..dbac07b 100644
--- a/cc_bindings_from_rs/generate_bindings/database/sugared_ty.rs
+++ b/cc_bindings_from_rs/generate_bindings/database/sugared_ty.rs
@@ -84,6 +84,11 @@
         });
         Some(SugaredTyList { tys, hir_tys })
     }
+
+    // TODO(b/449759899): Expand this to support all uninhabited types. Rename to `is_uninhabited`.
+    pub fn is_never(&self) -> bool {
+        *self.mid.kind() == ty::TyKind::Never
+    }
 }
 
 /// A list of `SugaredTy`s that can be created lazily from a list of `Ty` and
diff --git a/cc_bindings_from_rs/generate_bindings/generate_function.rs b/cc_bindings_from_rs/generate_bindings/generate_function.rs
index 55856ce..e32338a 100644
--- a/cc_bindings_from_rs/generate_bindings/generate_function.rs
+++ b/cc_bindings_from_rs/generate_bindings/generate_function.rs
@@ -422,10 +422,10 @@
     None
 }
 
-struct Param<'tcx> {
-    cc_name: Ident,
-    cpp_type: TokenStream,
-    ty: SugaredTy<'tcx>,
+pub(crate) struct Param<'tcx> {
+    pub(crate) cc_name: Ident,
+    pub(crate) cpp_type: TokenStream,
+    pub(crate) ty: SugaredTy<'tcx>,
 }
 
 fn can_shared_refs_to_ty_alias_mut_refs<'tcx>(tcx: TyCtxt<'tcx>, target_ty: Ty<'tcx>) -> bool {
@@ -480,6 +480,102 @@
     Some(refs)
 }
 
+/// Generates the wrapping code to call a thunk and return its result.
+/// This can be checking parameter invariants or creating a slot to pass as an output pointer.
+pub(crate) fn generate_thunk_call<'tcx>(
+    db: &dyn BindingsGenerator<'tcx>,
+    def_id: DefId,
+    thunk_name: Ident,
+    rs_return_type: SugaredTy<'tcx>,
+    takes_self_by_copy: bool,
+    has_self_param: bool,
+    params: &[Param<'tcx>],
+) -> Result<CcSnippet> {
+    let mut prereqs = CcPrerequisites::default();
+    let mut tokens = TokenStream::new();
+
+    let mut thunk_args = params
+        .iter()
+        .enumerate()
+        .map(|(i, Param { cc_name, ty, .. })| {
+            if i == 0 && has_self_param {
+                if takes_self_by_copy {
+                    // Self-by-copy methods are `const` qualified. The Rust thunk does not
+                    // accept a const pointer, but we can just const_cast since underlying C++
+                    // object is not modified: Rust copies the object before passing it into
+                    // the by-value method.
+                    tokens.extend(quote! {
+                        auto& #cc_name = const_cast<
+                            std::remove_cvref_t<decltype(*this)>&>(*this);
+                    });
+                } else {
+                    tokens.extend(quote! { auto&& #cc_name = *this; });
+                }
+            }
+            let tcx = db.tcx();
+            cc_param_to_c_abi(
+                db,
+                cc_name.clone(),
+                *ty,
+                post_analysis_typing_env(tcx, def_id),
+                &mut prereqs.includes,
+                &mut tokens,
+            )
+        })
+        .collect::<Result<Vec<TokenStream>>>()?;
+
+    if let Some(refs_to_check) = refs_to_check_for_aliasing(db, params) {
+        let mut_cpp_tys = refs_to_check.mutable.iter().map(|param| &param.cpp_type);
+        let mut_cpp_names = refs_to_check.mutable.iter().map(|param| &param.cc_name);
+        let shared_cpp_tys = refs_to_check.shared.iter().map(|param| &param.cpp_type);
+        let shared_cpp_names = refs_to_check.shared.iter().map(|param| &param.cc_name);
+        prereqs.includes.insert(db.support_header("internal/check_no_mutable_aliasing.h"));
+        tokens.extend(quote! {
+            __NEWLINE__
+            crubit::internal::CheckNoMutableAliasing(
+                crubit::internal::AsMutPtrDatas<#( #mut_cpp_tys ),*>( #( #mut_cpp_names ),* ),
+                crubit::internal::AsPtrDatas<#( #shared_cpp_tys ),*>(
+                    #( #shared_cpp_names ),* )
+            );
+            __NEWLINE__
+        });
+    }
+
+    let return_body = if is_bridged_type(db, rs_return_type.mid())?.is_none()
+        && is_c_abi_compatible_by_value(rs_return_type.mid())
+    {
+        // C++ compilers can emit diagnostics if a function marked [[noreturn]] looks like it
+        // might return. In this scenario, we just call the (also [[noreturn]]) thunk.
+        let return_expr = if rs_return_type.is_never() {
+            quote! {}
+        } else {
+            quote! {return}
+        };
+        quote! {
+            #return_expr __crubit_internal::#thunk_name(#( #thunk_args ),*);
+        }
+    } else {
+        let ReturnConversion { storage_name, unpack_expr } = cc_return_value_from_c_abi(
+            db,
+            expect_format_cc_ident("return_value"),
+            rs_return_type,
+            &mut prereqs,
+            &mut tokens,
+            /*recursive=*/ false,
+        )?;
+        thunk_args.push(quote! { #storage_name });
+        // We don't have to worry about the [[noreturn]] situation described above because all
+        // [[noreturn]] functions will take that branch.
+        quote! {
+            __crubit_internal::#thunk_name(#( #thunk_args ),*);
+            return #unpack_expr;
+        }
+    };
+
+    tokens.extend(return_body);
+    Ok(CcSnippet { prereqs, tokens })
+}
+
 /// Implementation of `BindingsGenerator::generate_function`.
 pub fn generate_function(db: &dyn BindingsGenerator<'_>, def_id: DefId) -> Result<ApiSnippets> {
     let tcx = db.tcx();
@@ -594,7 +690,6 @@
         .map(|Param { cc_name, cpp_type, .. }| quote! { #cpp_type #cc_name })
         .collect_vec();
     let rs_return_type = SugaredTy::fn_output(&sig_mid, sig_hir);
-    let fn_never_returns = *rs_return_type.mid().kind() == ty::TyKind::Never;
     let main_api = {
         let doc_comment = {
             let doc_comment = generate_doc_comment(db, def_id);
@@ -639,7 +734,7 @@
             }
         }
         // Attribute: noreturn
-        if fn_never_returns {
+        if rs_return_type.is_never() {
             attributes.push(quote! {[[noreturn]]});
         }
 
@@ -681,83 +776,16 @@
         )?
         .into_tokens(&mut prereqs);
 
-        let mut statements = TokenStream::new();
-        let mut thunk_args = params
-            .iter()
-            .enumerate()
-            .map(|(i, Param { cc_name, ty, .. })| {
-                if i == 0 && function_kind.has_self_param() {
-                    if takes_self_by_copy {
-                        // Self-by-copy methods are `const` qualified. The Rust thunk does not
-                        // accept a const pointer, but we can just const_cast since underlying C++
-                        // object is not modified: Rust copies the object before passing it into
-                        // the by-value method.
-                        statements.extend(quote! {
-                            auto& #cc_name = const_cast<
-                                std::remove_cvref_t<decltype(*this)>&>(*this);
-                        });
-                    } else {
-                        statements.extend(quote! { auto&& #cc_name = *this; });
-                    }
-                }
-                cc_param_to_c_abi(
-                    db,
-                    cc_name.clone(),
-                    *ty,
-                    post_analysis_typing_env(tcx, def_id),
-                    &mut prereqs.includes,
-                    &mut statements,
-                )
-            })
-            .collect::<Result<Vec<TokenStream>>>()?;
-
-        if let Some(refs_to_check) = refs_to_check_for_aliasing(db, &params) {
-            let mut_cpp_tys = refs_to_check.mutable.iter().map(|param| &param.cpp_type);
-            let mut_cpp_names = refs_to_check.mutable.iter().map(|param| &param.cc_name);
-            let shared_cpp_tys = refs_to_check.shared.iter().map(|param| &param.cpp_type);
-            let shared_cpp_names = refs_to_check.shared.iter().map(|param| &param.cc_name);
-            prereqs.includes.insert(db.support_header("internal/check_no_mutable_aliasing.h"));
-            statements.extend(quote! {
-                __NEWLINE__
-                crubit::internal::CheckNoMutableAliasing(
-                    crubit::internal::AsMutPtrDatas<#( #mut_cpp_tys ),*>( #( #mut_cpp_names ),* ),
-                    crubit::internal::AsPtrDatas<#( #shared_cpp_tys ),*>(
-                        #( #shared_cpp_names ),* )
-                );
-                __NEWLINE__
-            });
-        }
-
-        let impl_body: TokenStream = if is_bridged_type(db, rs_return_type.mid())?.is_none()
-            && is_c_abi_compatible_by_value(rs_return_type.mid())
-        {
-            // C++ compilers can emit diagnostics if a function marked [[noreturn]] looks like it
-            // might return. In this scenario, we just call the (also [[noreturn]]) thunk.
-            let return_expr = if fn_never_returns {
-                quote! {}
-            } else {
-                quote! {return}
-            };
-            quote! {
-                #return_expr __crubit_internal::#thunk_name(#( #thunk_args ),*);
-            }
-        } else {
-            let ReturnConversion { storage_name, unpack_expr } = cc_return_value_from_c_abi(
-                db,
-                expect_format_cc_ident("return_value"),
-                rs_return_type,
-                &mut prereqs,
-                &mut statements,
-                /*recursive=*/ false,
-            )?;
-            thunk_args.push(quote! { #storage_name });
-            // We don't have to worry about the [[noreturn]] situation described above because all
-            // [[noreturn]] functions will take that branch.
-            quote! {
-                __crubit_internal::#thunk_name(#( #thunk_args ),*);
-                return #unpack_expr;
-            }
-        };
+        let impl_body = generate_thunk_call(
+            db,
+            def_id,
+            thunk_name,
+            rs_return_type,
+            takes_self_by_copy,
+            function_kind.has_self_param(),
+            &params,
+        )?
+        .into_tokens(&mut prereqs);
 
         CcSnippet {
             prereqs,
@@ -766,7 +794,6 @@
                 #thunk_decl
                 inline #main_api_ret_type #struct_name #main_api_fn_name (
                         #( #main_api_params ),* ) #method_qualifiers {
-                    #statements
                     #impl_body
                 }
                 __NEWLINE__
diff --git a/cc_bindings_from_rs/generate_bindings/generate_struct_and_union.rs b/cc_bindings_from_rs/generate_bindings/generate_struct_and_union.rs
index 4ed63b0..1d9074a 100644
--- a/cc_bindings_from_rs/generate_bindings/generate_struct_and_union.rs
+++ b/cc_bindings_from_rs/generate_bindings/generate_struct_and_union.rs
@@ -10,11 +10,12 @@
 // TODO(b/381888123): Seperate out enum generation.
 use crate::format_cc_ident;
 use crate::generate_doc_comment;
+use crate::generate_function::{generate_thunk_call, Param};
 use crate::{
     crate_features, generate_const, generate_deprecated_tag, generate_must_use_tag,
     generate_trait_thunks, generate_unsupported_def, get_layout, get_scalar_int_type,
-    get_tag_size_with_padding, is_bridged_type, is_exported, is_public_or_supported_export,
-    RsSnippet, SortedByDef, TraitThunks,
+    get_tag_size_with_padding, is_bridged_type, is_copy, is_exported,
+    is_public_or_supported_export, RsSnippet, SortedByDef, TraitThunks,
 };
 use arc_anyhow::{Context, Result};
 use code_gen_utils::{expect_format_cc_type_name, make_rs_ident, CcInclude};
@@ -353,6 +354,126 @@
     Rc::new(map)
 }
 
+fn generate_into_impls<'tcx>(
+    db: &dyn BindingsGenerator<'tcx>,
+    core: &AdtCoreBindings<'tcx>,
+) -> ApiSnippets {
+    let tcx = db.tcx();
+    let cc_struct_name = &core.cc_short_name;
+
+    let into_trait = tcx.get_diagnostic_item(sym::Into).expect("Could not find Into trait");
+
+    let from_map = db.from_trait_impls_by_argument(core.def_id.krate);
+    let from_impls = from_map.get(&core.self_ty).into_iter().flat_map(|vec| vec.iter()).filter_map(
+        |from_impl_id| {
+            let middle_trait_header = tcx
+                .impl_trait_header(*from_impl_id)
+                .expect("DefId for a `From` trait impl lacked a trait header");
+            let trait_ref = middle_trait_header.trait_ref.instantiate_identity();
+
+            let from_middle_ty = trait_ref.args.type_at(0);
+
+            // If our type contains type variables or constant variables (but not region variables),
+            // we can't generate an `into` impl.
+            if from_middle_ty.flags().contains(has_type_or_const_vars()) {
+                return None;
+            }
+            let sugar_ty = SugaredTy::missing_hir(from_middle_ty);
+            // We know that our type will always appear in FnReturn position for the `into` method.
+            // If our type isn't C++-compatible, we can't generate an `into` impl.
+            let cc_ty = db.format_ty_for_cc(sugar_ty, TypeLocation::FnReturn).ok()?;
+            Some((from_middle_ty, cc_ty, *from_impl_id))
+        },
+    );
+    let into_impls =
+        tcx.non_blanket_impls_for_ty(into_trait, core.self_ty).filter_map(|into_impl_id| {
+            let middle_trait_header = tcx
+                .impl_trait_header(into_impl_id)
+                .expect("DefId for an `Into` trait impl lacked a trait header");
+            // Index 0 of our trait ref is the self type, so index 1 is the type we're converting
+            // into.
+            let into_middle_ty =
+                middle_trait_header.trait_ref.instantiate_identity().args.type_at(1);
+
+            let sugar_ty = SugaredTy::missing_hir(into_middle_ty);
+            // If our type isn't Cxx compatible, we can't generate an `into` impl.
+            let cc_ty = db.format_ty_for_cc(sugar_ty, TypeLocation::FnReturn).ok()?;
+
+            Some((into_middle_ty, cc_ty, into_impl_id))
+        });
+
+    from_impls
+        .chain(into_impls)
+        .filter_map(|(middle_ty, cc_ty, def_id)| {
+            let mut prereqs = CcPrerequisites::default();
+            let cc_ty = cc_ty.into_tokens(&mut prereqs);
+
+            // Delay converting this type until we've successfully generated the thunks.
+            // We generate thunks for `into` here. This relies on the blanket impls of for `Into` in the stdlib to work.
+            let TraitThunks {
+                method_name_to_cc_thunk_name,
+                cc_thunk_decls,
+                rs_thunk_impls: rs_details,
+            } = generate_trait_thunks(db, into_trait, &[middle_ty], core).ok()?;
+
+            let thunk_name = method_name_to_cc_thunk_name
+                .into_values()
+                .exactly_one()
+                .expect("Expecting a single `into` method");
+
+            let cc_thunk_decls = cc_thunk_decls.into_tokens(&mut prereqs);
+            let doc_comment = generate_doc_comment(db, def_id);
+
+            let sugar_self_ty = SugaredTy::missing_hir(core.self_ty);
+            let self_cpp_ty = db
+                .format_ty_for_cc(
+                    sugar_self_ty,
+                    TypeLocation::FnParam { is_self_param: true, elided_is_output: true },
+                )
+                .expect(
+                    "ADT's self type should be C++-convertible after generate_adt_core succeeds",
+                );
+            let self_cpp_ty = self_cpp_ty.into_tokens(&mut prereqs);
+            let impl_body = generate_thunk_call(
+                db,
+                def_id,
+                thunk_name.clone(),
+                SugaredTy::missing_hir(middle_ty),
+                /*takes_self_by_copy=*/ is_copy(tcx, def_id, core.self_ty),
+                /*has_self_param=*/ true,
+                &[Param {
+                    cc_name: format_ident!("self"),
+                    cpp_type: self_cpp_ty,
+                    ty: sugar_self_ty,
+                }],
+            )
+            .expect("Self type of `Into` impl should be bridgeable");
+
+            let impl_body_tokens = impl_body.into_tokens(&mut prereqs);
+            prereqs.move_defs_to_fwd_decls();
+
+            Some(ApiSnippets {
+                main_api: CcSnippet {
+                    tokens: quote! {
+                    __NEWLINE__ #doc_comment
+                    explicit operator #cc_ty ( ) ; __NEWLINE__
+                    __NEWLINE__
+                    },
+                    prereqs,
+                },
+                cc_details: CcSnippet::new(quote! {
+                    #cc_thunk_decls
+
+                    #cc_struct_name :: operator  #cc_ty ( ) {
+                        #impl_body_tokens
+                    }
+                }),
+                rs_details,
+            })
+        })
+        .collect()
+}
+
 /// Formats an algebraic data type (an ADT - a struct, an enum, or a union)
 /// represented by `core`.  This function is infallible - after
 /// `generate_adt_core` returns success we have committed to emitting C++
@@ -437,6 +558,8 @@
         .flat_map(|assoc_item| generate_associated_item(db, assoc_item, &mut member_function_names))
         .collect();
 
+    let into_operator_snippets = generate_into_impls(db, core.as_ref());
+
     let ApiSnippets {
         main_api: public_functions_main_api,
         cc_details: public_functions_cc_details,
@@ -449,6 +572,7 @@
         copy_ctor_and_assignment_snippets,
         relocating_ctor_snippets,
         impl_items_snippets,
+        into_operator_snippets,
     ]
     .into_iter()
     .collect();
diff --git a/cc_bindings_from_rs/test/known_traits/from/BUILD b/cc_bindings_from_rs/test/known_traits/from/BUILD
new file mode 100644
index 0000000..d807c3c
--- /dev/null
+++ b/cc_bindings_from_rs/test/known_traits/from/BUILD
@@ -0,0 +1,52 @@
+"""End-to-end tests of `cc_bindings_from_rs`, focusing on From bindings."""
+
+load(
+    "@rules_rust//rust:defs.bzl",
+    "rust_library",
+)
+load(
+    "//cc_bindings_from_rs/bazel_support:cc_bindings_from_rust_rule.bzl",
+    "cc_bindings_from_rust",
+)
+load(
+    "//cc_bindings_from_rs/test/golden:golden_test.bzl",
+    "golden_test",
+)
+load("//common:crubit_wrapper_macros_oss.bzl", "crubit_cc_test")
+
+package(default_applicable_licenses = ["//:license"])
+
+rust_library(
+    name = "from",
+    testonly = 1,
+    srcs = ["from.rs"],
+    aspect_hints = [
+        "//features:supported",
+    ],
+    proc_macro_deps = [
+        "//support:crubit_annotate",
+    ],
+)
+
+golden_test(
+    name = "from_golden_test",
+    basename = "from",
+    golden_h = "from_cc_api.h",
+    golden_rs = "from_cc_api_impl.rs",
+    rust_library = "from",
+)
+
+cc_bindings_from_rust(
+    name = "from_cc_api",
+    testonly = 1,
+    crate = ":from",
+)
+
+crubit_cc_test(
+    name = "from_test",
+    srcs = ["from_test.cc"],
+    deps = [
+        ":from_cc_api",
+        "@googletest//:gtest_main",
+    ],
+)
diff --git a/cc_bindings_from_rs/test/known_traits/from/from.rs b/cc_bindings_from_rs/test/known_traits/from/from.rs
new file mode 100644
index 0000000..ab0a38b
--- /dev/null
+++ b/cc_bindings_from_rs/test/known_traits/from/from.rs
@@ -0,0 +1,87 @@
+// 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
+
+//! This crate is used as a test input for `cc_bindings_from_rs` and the
+//! generated C++ bindings are then tested via `from_test.cc`.
+
+use crubit_annotate::must_bind;
+
+#[must_bind]
+pub struct Opaque(pub i32);
+
+impl std::convert::From<Opaque> for i32 {
+    fn from(value: Opaque) -> Self {
+        value.0
+    }
+}
+
+impl From<Opaque> for i64 {
+    fn from(value: Opaque) -> i64 {
+        value.0 as i64
+    }
+}
+
+impl From<Opaque> for &'static str {
+    fn from(_: Opaque) -> &'static str {
+        "Opaque"
+    }
+}
+
+use std::convert;
+impl convert::From<Opaque> for i16 {
+    fn from(value: Opaque) -> i16 {
+        value.0.try_into().unwrap()
+    }
+}
+
+impl From<Opaque> for OpaqueRef<'static> {
+    fn from(value: Opaque) -> Self {
+        Self(value.into())
+    }
+}
+
+#[must_bind]
+pub struct OpaqueRef<'a>(&'a str);
+
+impl<'a> OpaqueRef<'a> {
+    #[must_bind]
+    pub fn create(s: &'a str) -> Self {
+        Self(s)
+    }
+
+    #[must_bind]
+    pub fn get_arg(&self) -> &'a str {
+        self.0
+    }
+}
+
+impl<'a> From<OpaqueRef<'a>> for &'a str {
+    fn from(value: OpaqueRef<'a>) -> &'a str {
+        value.get_arg()
+    }
+}
+
+// `From` impls with non-C++-compatible types shouldn't be bound.
+#[must_bind]
+pub struct NotFfiSafe(fn());
+
+#[allow(dead_code)]
+fn test() {}
+
+impl NotFfiSafe {
+    #[must_bind]
+    pub fn create() -> Self {
+        Self(test)
+    }
+}
+impl From<NotFfiSafe> for i32 {
+    fn from(_: NotFfiSafe) -> i32 {
+        42
+    }
+}
+impl From<NotFfiSafe> for fn() {
+    fn from(value: NotFfiSafe) -> fn() {
+        value.0
+    }
+}
diff --git a/cc_bindings_from_rs/test/known_traits/from/from_cc_api.h b/cc_bindings_from_rs/test/known_traits/from/from_cc_api.h
new file mode 100644
index 0000000..c51bdc9
--- /dev/null
+++ b/cc_bindings_from_rs/test/known_traits/from/from_cc_api.h
@@ -0,0 +1,292 @@
+// 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 C++ bindings for the following Rust crate:
+// from_golden
+// Features: supported
+
+// clang-format off
+#ifndef THIRD_PARTY_CRUBIT_CC_BINDINGS_FROM_RS_TEST_KNOWN_TRAITS_FROM_FROM_GOLDEN
+#define THIRD_PARTY_CRUBIT_CC_BINDINGS_FROM_RS_TEST_KNOWN_TRAITS_FROM_FROM_GOLDEN
+
+#include "support/annotations_internal.h"
+#include "support/internal/slot.h"
+#include "support/rs_std/str_ref.h"
+
+#include <cstddef>
+#include <cstdint>
+#include <type_traits>
+#include <utility>
+
+namespace from {
+struct OpaqueRef;
+// CRUBIT_ANNOTATE: must_bind=
+//
+// Generated from:
+// cc_bindings_from_rs/test/known_traits/from/from.rs;l=11
+struct CRUBIT_INTERNAL_RUST_TYPE(":: from_golden :: Opaque") alignas(4)
+    [[clang::trivial_abi]] Opaque final {
+ public:
+  // `Opaque` doesn't implement the `Default` trait
+  Opaque() = delete;
+
+  // Synthesized tuple constructor
+  explicit Opaque(std::int32_t __field0) : __field0(std::move(__field0)) {}
+
+  // No custom `Drop` impl and no custom "drop glue" required
+  ~Opaque() = default;
+  Opaque(Opaque&&) = default;
+  Opaque& operator=(Opaque&&) = default;
+
+  // `Opaque` doesn't implement the `Clone` trait
+  Opaque(const Opaque&) = delete;
+  Opaque& operator=(const Opaque&) = delete;
+  Opaque(::crubit::UnsafeRelocateTag, Opaque&& value) {
+    memcpy(this, &value, sizeof(value));
+  }
+  // Generated from:
+  // cc_bindings_from_rs/test/known_traits/from/from.rs;l=13
+  explicit operator std::int32_t();
+
+  // Generated from:
+  // cc_bindings_from_rs/test/known_traits/from/from.rs;l=19
+  explicit operator std::int64_t();
+
+  // Generated from:
+  // cc_bindings_from_rs/test/known_traits/from/from.rs;l=25
+  explicit operator rs_std::StrRef();
+
+  // Generated from:
+  // cc_bindings_from_rs/test/known_traits/from/from.rs;l=32
+  explicit operator std::int16_t();
+
+  // Generated from:
+  // cc_bindings_from_rs/test/known_traits/from/from.rs;l=38
+  explicit operator ::from::OpaqueRef();
+
+  union {
+    // Generated from:
+    // cc_bindings_from_rs/test/known_traits/from/from.rs;l=11
+    std::int32_t __field0;
+  };
+
+ private:
+  static void __crubit_field_offset_assertions();
+};
+
+// CRUBIT_ANNOTATE: must_bind=
+//
+// Generated from:
+// cc_bindings_from_rs/test/known_traits/from/from.rs;l=45
+struct CRUBIT_INTERNAL_RUST_TYPE(":: from_golden :: OpaqueRef") alignas(8)
+    [[clang::trivial_abi]] OpaqueRef final {
+ public:
+  // `OpaqueRef<'_>` doesn't implement the `Default` trait
+  OpaqueRef() = delete;
+
+  // No custom `Drop` impl and no custom "drop glue" required
+  ~OpaqueRef() = default;
+  OpaqueRef(OpaqueRef&&) = default;
+  OpaqueRef& operator=(OpaqueRef&&) = default;
+
+  // `OpaqueRef<'_>` doesn't implement the `Clone` trait
+  OpaqueRef(const OpaqueRef&) = delete;
+  OpaqueRef& operator=(const OpaqueRef&) = delete;
+  OpaqueRef(::crubit::UnsafeRelocateTag, OpaqueRef&& value) {
+    memcpy(this, &value, sizeof(value));
+  }
+
+  // CRUBIT_ANNOTATE: must_bind=
+  //
+  // Generated from:
+  // cc_bindings_from_rs/test/known_traits/from/from.rs;l=49
+  static ::from::OpaqueRef create(rs_std::StrRef s);
+
+  // CRUBIT_ANNOTATE: must_bind=
+  //
+  // Generated from:
+  // cc_bindings_from_rs/test/known_traits/from/from.rs;l=54
+  rs_std::StrRef get_arg() const;
+
+  // Generated from:
+  // cc_bindings_from_rs/test/known_traits/from/from.rs;l=59
+  explicit operator rs_std::StrRef();
+
+ private:
+  // Field type has been replaced with a blob of bytes: Can't format `&str`,
+  // because references are only supported in function parameter types, return
+  // types, and consts (b/286256327)
+  unsigned char __field0[16];
+
+ private:
+  static void __crubit_field_offset_assertions();
+};
+
+// CRUBIT_ANNOTATE: must_bind=
+//
+// Generated from:
+// cc_bindings_from_rs/test/known_traits/from/from.rs;l=67
+struct CRUBIT_INTERNAL_RUST_TYPE(":: from_golden :: NotFfiSafe") alignas(8)
+    [[clang::trivial_abi]] NotFfiSafe final {
+ public:
+  // `NotFfiSafe` doesn't implement the `Default` trait
+  NotFfiSafe() = delete;
+
+  // No custom `Drop` impl and no custom "drop glue" required
+  ~NotFfiSafe() = default;
+  NotFfiSafe(NotFfiSafe&&) = default;
+  NotFfiSafe& operator=(NotFfiSafe&&) = default;
+
+  // `NotFfiSafe` doesn't implement the `Clone` trait
+  NotFfiSafe(const NotFfiSafe&) = delete;
+  NotFfiSafe& operator=(const NotFfiSafe&) = delete;
+  NotFfiSafe(::crubit::UnsafeRelocateTag, NotFfiSafe&& value) {
+    memcpy(this, &value, sizeof(value));
+  }
+
+  // CRUBIT_ANNOTATE: must_bind=
+  //
+  // Generated from:
+  // cc_bindings_from_rs/test/known_traits/from/from.rs;l=74
+  static ::from::NotFfiSafe create();
+
+  // Generated from:
+  // cc_bindings_from_rs/test/known_traits/from/from.rs;l=78
+  explicit operator std::int32_t();
+
+ private:
+  // Field type has been replaced with a blob of bytes: Function pointers can't
+  // have a thunk: Any calling convention other than `extern "C"` requires a
+  // thunk
+  unsigned char __field0[8];
+
+ private:
+  static void __crubit_field_offset_assertions();
+};
+
+static_assert(
+    sizeof(Opaque) == 4,
+    "Verify that ADT layout didn't change since this header got generated");
+static_assert(
+    alignof(Opaque) == 4,
+    "Verify that ADT layout didn't change since this header got generated");
+static_assert(std::is_trivially_destructible_v<Opaque>);
+static_assert(std::is_trivially_move_constructible_v<Opaque>);
+static_assert(std::is_trivially_move_assignable_v<Opaque>);
+namespace __crubit_internal {
+extern "C" std::int32_t __crubit_thunk_into_ui32(::from::Opaque*);
+}
+Opaque::operator std::int32_t() {
+  auto&& self = *this;
+  return __crubit_internal::__crubit_thunk_into_ui32(&self);
+}
+namespace __crubit_internal {
+extern "C" std::int64_t __crubit_thunk_into_ui64(::from::Opaque*);
+}
+Opaque::operator std::int64_t() {
+  auto&& self = *this;
+  return __crubit_internal::__crubit_thunk_into_ui64(&self);
+}
+namespace __crubit_internal {
+extern "C" rs_std::StrRef
+__crubit_thunk_into_u_x00000026_x00000027static_x00000020str(::from::Opaque*);
+}
+Opaque::operator rs_std::StrRef() {
+  auto&& self = *this;
+  return __crubit_internal::
+      __crubit_thunk_into_u_x00000026_x00000027static_x00000020str(&self);
+}
+namespace __crubit_internal {
+extern "C" std::int16_t __crubit_thunk_into_ui16(::from::Opaque*);
+}
+Opaque::operator std::int16_t() {
+  auto&& self = *this;
+  return __crubit_internal::__crubit_thunk_into_ui16(&self);
+}
+namespace __crubit_internal {
+extern "C" void
+__crubit_thunk_into_uOpaqueRef_x0000003c_x00000027static_x0000003e(
+    ::from::Opaque*, ::from::OpaqueRef* __ret_ptr);
+}
+Opaque::operator ::from::OpaqueRef() {
+  auto&& self = *this;
+  crubit::Slot<::from::OpaqueRef> __return_value_ret_val_holder;
+  auto* __return_value_storage = __return_value_ret_val_holder.Get();
+  __crubit_internal::
+      __crubit_thunk_into_uOpaqueRef_x0000003c_x00000027static_x0000003e(
+          &self, __return_value_storage);
+  return std::move(__return_value_ret_val_holder).AssumeInitAndTakeValue();
+}
+inline void Opaque::__crubit_field_offset_assertions() {
+  static_assert(0 == offsetof(Opaque, __field0));
+}
+static_assert(
+    sizeof(OpaqueRef) == 16,
+    "Verify that ADT layout didn't change since this header got generated");
+static_assert(
+    alignof(OpaqueRef) == 8,
+    "Verify that ADT layout didn't change since this header got generated");
+static_assert(std::is_trivially_destructible_v<OpaqueRef>);
+static_assert(std::is_trivially_move_constructible_v<OpaqueRef>);
+static_assert(std::is_trivially_move_assignable_v<OpaqueRef>);
+namespace __crubit_internal {
+extern "C" void __crubit_thunk_create(rs_std::StrRef,
+                                      ::from::OpaqueRef* __ret_ptr);
+}
+inline ::from::OpaqueRef OpaqueRef::create(rs_std::StrRef s) {
+  crubit::Slot<::from::OpaqueRef> __return_value_ret_val_holder;
+  auto* __return_value_storage = __return_value_ret_val_holder.Get();
+  __crubit_internal::__crubit_thunk_create(s, __return_value_storage);
+  return std::move(__return_value_ret_val_holder).AssumeInitAndTakeValue();
+}
+
+namespace __crubit_internal {
+extern "C" rs_std::StrRef __crubit_thunk_get_uarg(::from::OpaqueRef const&);
+}
+inline rs_std::StrRef OpaqueRef::get_arg() const {
+  auto&& self = *this;
+  return __crubit_internal::__crubit_thunk_get_uarg(self);
+}
+namespace __crubit_internal {
+extern "C" rs_std::StrRef
+__crubit_thunk_into_u_x00000026_x00000027a_x00000020str(::from::OpaqueRef*);
+}
+OpaqueRef::operator rs_std::StrRef() {
+  auto&& self = *this;
+  return __crubit_internal::
+      __crubit_thunk_into_u_x00000026_x00000027a_x00000020str(&self);
+}
+inline void OpaqueRef::__crubit_field_offset_assertions() {
+  static_assert(0 == offsetof(OpaqueRef, __field0));
+}
+static_assert(
+    sizeof(NotFfiSafe) == 8,
+    "Verify that ADT layout didn't change since this header got generated");
+static_assert(
+    alignof(NotFfiSafe) == 8,
+    "Verify that ADT layout didn't change since this header got generated");
+static_assert(std::is_trivially_destructible_v<NotFfiSafe>);
+static_assert(std::is_trivially_move_constructible_v<NotFfiSafe>);
+static_assert(std::is_trivially_move_assignable_v<NotFfiSafe>);
+namespace __crubit_internal {
+extern "C" void __crubit_thunk_create(::from::NotFfiSafe* __ret_ptr);
+}
+inline ::from::NotFfiSafe NotFfiSafe::create() {
+  crubit::Slot<::from::NotFfiSafe> __return_value_ret_val_holder;
+  auto* __return_value_storage = __return_value_ret_val_holder.Get();
+  __crubit_internal::__crubit_thunk_create(__return_value_storage);
+  return std::move(__return_value_ret_val_holder).AssumeInitAndTakeValue();
+}
+namespace __crubit_internal {
+extern "C" std::int32_t __crubit_thunk_into_ui32(::from::NotFfiSafe*);
+}
+NotFfiSafe::operator std::int32_t() {
+  auto&& self = *this;
+  return __crubit_internal::__crubit_thunk_into_ui32(&self);
+}
+inline void NotFfiSafe::__crubit_field_offset_assertions() {
+  static_assert(0 == offsetof(NotFfiSafe, __field0));
+}
+}  // namespace from
+#endif  // THIRD_PARTY_CRUBIT_CC_BINDINGS_FROM_RS_TEST_KNOWN_TRAITS_FROM_FROM_GOLDEN
diff --git a/cc_bindings_from_rs/test/known_traits/from/from_cc_api_impl.rs b/cc_bindings_from_rs/test/known_traits/from/from_cc_api_impl.rs
new file mode 100644
index 0000000..45d331f
--- /dev/null
+++ b/cc_bindings_from_rs/test/known_traits/from/from_cc_api_impl.rs
@@ -0,0 +1,109 @@
+// 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 C++ bindings for the following Rust crate:
+// from_golden
+// Features: supported
+
+#![allow(unused_unsafe, deprecated, non_snake_case, unreachable_code)]
+#![allow(improper_ctypes_definitions)]
+#![deny(warnings)]
+
+const _: () = assert!(::std::mem::size_of::<::from_golden::Opaque>() == 4);
+const _: () = assert!(::std::mem::align_of::<::from_golden::Opaque>() == 4);
+#[unsafe(no_mangle)]
+unsafe extern "C" fn __crubit_thunk_into_ui32(
+    __self: &'static mut ::core::mem::MaybeUninit<::from_golden::Opaque>,
+) -> i32 {
+    unsafe {
+        let __self = __self.assume_init_read();
+        <::from_golden::Opaque as ::core::convert::Into<i32>>::into(__self)
+    }
+}
+#[unsafe(no_mangle)]
+unsafe extern "C" fn __crubit_thunk_into_ui64(
+    __self: &'static mut ::core::mem::MaybeUninit<::from_golden::Opaque>,
+) -> i64 {
+    unsafe {
+        let __self = __self.assume_init_read();
+        <::from_golden::Opaque as ::core::convert::Into<i64>>::into(__self)
+    }
+}
+#[unsafe(no_mangle)]
+unsafe extern "C" fn __crubit_thunk_into_u_x00000026_x00000027static_x00000020str(
+    __self: &'static mut ::core::mem::MaybeUninit<::from_golden::Opaque>,
+) -> &'static str {
+    unsafe {
+        let __self = __self.assume_init_read();
+        <::from_golden::Opaque as ::core::convert::Into<&'static str>>::into(__self)
+    }
+}
+#[unsafe(no_mangle)]
+unsafe extern "C" fn __crubit_thunk_into_ui16(
+    __self: &'static mut ::core::mem::MaybeUninit<::from_golden::Opaque>,
+) -> i16 {
+    unsafe {
+        let __self = __self.assume_init_read();
+        <::from_golden::Opaque as ::core::convert::Into<i16>>::into(__self)
+    }
+}
+#[unsafe(no_mangle)]
+unsafe extern "C" fn __crubit_thunk_into_uOpaqueRef_x0000003c_x00000027static_x0000003e(
+    __self: &'static mut ::core::mem::MaybeUninit<::from_golden::Opaque>,
+    __ret_ptr: *mut core::ffi::c_void,
+) -> () {
+    unsafe {
+        let __self = __self.assume_init_read();
+        let __rs_return_value = <::from_golden::Opaque as ::core::convert::Into<
+            ::from_golden::OpaqueRef<'static>,
+        >>::into(__self);
+        (__ret_ptr as *mut ::from_golden::OpaqueRef<'static>).write(__rs_return_value);
+    }
+}
+const _: () = assert!(::core::mem::offset_of!(::from_golden::Opaque, 0) == 0);
+const _: () = assert!(::std::mem::size_of::<::from_golden::OpaqueRef>() == 16);
+const _: () = assert!(::std::mem::align_of::<::from_golden::OpaqueRef>() == 8);
+#[unsafe(no_mangle)]
+unsafe extern "C" fn __crubit_thunk_create(
+    s: &'static str,
+    __ret_ptr: *mut core::ffi::c_void,
+) -> () {
+    unsafe {
+        let __rs_return_value = ::from_golden::OpaqueRef::create(s);
+        (__ret_ptr as *mut ::from_golden::OpaqueRef<'static>).write(__rs_return_value);
+    }
+}
+#[unsafe(no_mangle)]
+unsafe extern "C" fn __crubit_thunk_get_uarg(
+    __self: &'static ::from_golden::OpaqueRef<'static>,
+) -> &'static str {
+    unsafe { ::from_golden::OpaqueRef::get_arg(__self) }
+}
+#[unsafe(no_mangle)]
+unsafe extern "C" fn __crubit_thunk_into_u_x00000026_x00000027a_x00000020str(
+    __self: &'static mut ::core::mem::MaybeUninit<::from_golden::OpaqueRef<'static>>,
+) -> &'static str {
+    unsafe {
+        let __self = __self.assume_init_read();
+        <::from_golden::OpaqueRef as ::core::convert::Into<&'static str>>::into(__self)
+    }
+}
+const _: () = assert!(::std::mem::size_of::<::from_golden::NotFfiSafe>() == 8);
+const _: () = assert!(::std::mem::align_of::<::from_golden::NotFfiSafe>() == 8);
+#[unsafe(no_mangle)]
+unsafe extern "C" fn __crubit_thunk_create(__ret_ptr: *mut core::ffi::c_void) -> () {
+    unsafe {
+        let __rs_return_value = ::from_golden::NotFfiSafe::create();
+        (__ret_ptr as *mut ::from_golden::NotFfiSafe).write(__rs_return_value);
+    }
+}
+#[unsafe(no_mangle)]
+unsafe extern "C" fn __crubit_thunk_into_ui32(
+    __self: &'static mut ::core::mem::MaybeUninit<::from_golden::NotFfiSafe>,
+) -> i32 {
+    unsafe {
+        let __self = __self.assume_init_read();
+        <::from_golden::NotFfiSafe as ::core::convert::Into<i32>>::into(__self)
+    }
+}
diff --git a/cc_bindings_from_rs/test/known_traits/from/from_test.cc b/cc_bindings_from_rs/test/known_traits/from/from_test.cc
new file mode 100644
index 0000000..f916d6f
--- /dev/null
+++ b/cc_bindings_from_rs/test/known_traits/from/from_test.cc
@@ -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
+
+#include "cc_bindings_from_rs/test/known_traits/from/from.h"
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+namespace crubit {
+namespace {
+
+TEST(FromTest, FromImplsBecomeConversionOperators) {
+  from::Opaque opaque(123);
+  EXPECT_EQ(static_cast<int32_t>(opaque), 123);
+  EXPECT_EQ(static_cast<int64_t>(opaque), 123);
+  EXPECT_EQ(static_cast<from::OpaqueRef>(opaque).get_arg(), "Opaque");
+
+  from::OpaqueRef opaque_ref = from::OpaqueRef::create(rs_std::StrRef("hello"));
+  EXPECT_EQ(static_cast<rs_std::StrRef>(opaque_ref), "hello");
+
+  from::NotFfiSafe not_ffi_safe = from::NotFfiSafe::create();
+  EXPECT_EQ(static_cast<int32_t>(not_ffi_safe), 42);
+}
+
+}  // namespace
+}  // namespace crubit
diff --git a/cc_bindings_from_rs/test/known_traits/into/BUILD b/cc_bindings_from_rs/test/known_traits/into/BUILD
new file mode 100644
index 0000000..1d356ea
--- /dev/null
+++ b/cc_bindings_from_rs/test/known_traits/into/BUILD
@@ -0,0 +1,52 @@
+"""End-to-end tests of `cc_bindings_from_rs`, focusing on Into bindings."""
+
+load(
+    "@rules_rust//rust:defs.bzl",
+    "rust_library",
+)
+load(
+    "//cc_bindings_from_rs/bazel_support:cc_bindings_from_rust_rule.bzl",
+    "cc_bindings_from_rust",
+)
+load(
+    "//cc_bindings_from_rs/test/golden:golden_test.bzl",
+    "golden_test",
+)
+load("//common:crubit_wrapper_macros_oss.bzl", "crubit_cc_test")
+
+package(default_applicable_licenses = ["//:license"])
+
+rust_library(
+    name = "into",
+    testonly = 1,
+    srcs = ["into.rs"],
+    aspect_hints = [
+        "//features:supported",
+    ],
+    proc_macro_deps = [
+        "//support:crubit_annotate",
+    ],
+)
+
+golden_test(
+    name = "into_golden_test",
+    basename = "into",
+    golden_h = "into_cc_api.h",
+    golden_rs = "into_cc_api_impl.rs",
+    rust_library = "into",
+)
+
+cc_bindings_from_rust(
+    name = "into_cc_api",
+    testonly = 1,
+    crate = ":into",
+)
+
+crubit_cc_test(
+    name = "into_test",
+    srcs = ["into_test.cc"],
+    deps = [
+        ":into_cc_api",
+        "@googletest//:gtest_main",
+    ],
+)
diff --git a/cc_bindings_from_rs/test/known_traits/into/into.rs b/cc_bindings_from_rs/test/known_traits/into/into.rs
new file mode 100644
index 0000000..c1f57ad
--- /dev/null
+++ b/cc_bindings_from_rs/test/known_traits/into/into.rs
@@ -0,0 +1,109 @@
+// 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
+
+//! This crate is used as a test input for `cc_bindings_from_rs` and the
+//! generated C++ bindings are then tested via `into_test.cc`.
+
+// We explicitly want to test `Into` here, so disable the clippy warning about it.
+#![allow(clippy::from_over_into)]
+
+use crubit_annotate::must_bind;
+
+#[must_bind]
+pub struct Convert(pub i32);
+
+impl std::convert::Into<i32> for Convert {
+    #[must_bind]
+    fn into(self) -> i32 {
+        self.0
+    }
+}
+
+impl Into<i64> for Convert {
+    #[must_bind]
+    fn into(self) -> i64 {
+        self.0 as i64
+    }
+}
+
+impl Into<&'static str> for Convert {
+    #[must_bind]
+    fn into(self) -> &'static str {
+        "Convert"
+    }
+}
+
+use std::convert;
+impl convert::Into<i16> for Convert {
+    #[must_bind]
+    fn into(self) -> i16 {
+        self.0.try_into().unwrap()
+    }
+}
+
+#[must_bind]
+pub struct ConvertRef<'a>(&'a str);
+
+impl<'a> ConvertRef<'a> {
+    #[must_bind]
+    pub fn create(s: &'a str) -> Self {
+        Self(s)
+    }
+
+    #[must_bind]
+    pub fn transmigrate(self) -> Convert {
+        Convert(42)
+    }
+}
+
+impl<'a> Into<&'a str> for ConvertRef<'a> {
+    #[must_bind]
+    fn into(self) -> &'a str {
+        self.0
+    }
+}
+
+impl Into<Convert> for ConvertRef<'_> {
+    #[must_bind]
+    fn into(self) -> Convert {
+        Convert(42)
+    }
+}
+
+// `Into` impls with non-C++-compatible types shouldn't be bound.
+#[must_bind]
+pub struct NotFfiSafe(fn());
+
+impl Into<fn()> for NotFfiSafe {
+    fn into(self) -> fn() {
+        self.0
+    }
+}
+
+#[must_bind]
+pub struct ConvertModule(pub i32);
+
+pub mod another_module {
+    use super::ConvertModule;
+    use crubit_annotate::must_bind;
+
+    impl Into<i32> for ConvertModule {
+        #[must_bind]
+        fn into(self) -> i32 {
+            self.0
+        }
+    }
+}
+
+mod yet_another_module {
+    use super::ConvertModule;
+    use crubit_annotate::must_bind;
+
+    impl Into<i64> for ConvertModule {
+        #[must_bind]
+        fn into(self) -> i64 {
+            self.0 as i64
+        }
+    }
+}
diff --git a/cc_bindings_from_rs/test/known_traits/into/into_cc_api.h b/cc_bindings_from_rs/test/known_traits/into/into_cc_api.h
new file mode 100644
index 0000000..5fe745b
--- /dev/null
+++ b/cc_bindings_from_rs/test/known_traits/into/into_cc_api.h
@@ -0,0 +1,337 @@
+// 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 C++ bindings for the following Rust crate:
+// into_golden
+// Features: supported
+
+// clang-format off
+#ifndef THIRD_PARTY_CRUBIT_CC_BINDINGS_FROM_RS_TEST_KNOWN_TRAITS_INTO_INTO_GOLDEN
+#define THIRD_PARTY_CRUBIT_CC_BINDINGS_FROM_RS_TEST_KNOWN_TRAITS_INTO_INTO_GOLDEN
+
+#include "support/annotations_internal.h"
+#include "support/internal/slot.h"
+#include "support/rs_std/str_ref.h"
+
+#include <cstddef>
+#include <cstdint>
+#include <type_traits>
+#include <utility>
+
+namespace into {
+
+// CRUBIT_ANNOTATE: must_bind=
+//
+// Generated from:
+// cc_bindings_from_rs/test/known_traits/into/into.rs;l=14
+struct CRUBIT_INTERNAL_RUST_TYPE(":: into_golden :: Convert") alignas(4)
+    [[clang::trivial_abi]] Convert final {
+ public:
+  // `Convert` doesn't implement the `Default` trait
+  Convert() = delete;
+
+  // Synthesized tuple constructor
+  explicit Convert(std::int32_t __field0) : __field0(std::move(__field0)) {}
+
+  // No custom `Drop` impl and no custom "drop glue" required
+  ~Convert() = default;
+  Convert(Convert&&) = default;
+  Convert& operator=(Convert&&) = default;
+
+  // `Convert` doesn't implement the `Clone` trait
+  Convert(const Convert&) = delete;
+  Convert& operator=(const Convert&) = delete;
+  Convert(::crubit::UnsafeRelocateTag, Convert&& value) {
+    memcpy(this, &value, sizeof(value));
+  }
+  // Generated from:
+  // cc_bindings_from_rs/test/known_traits/into/into.rs;l=16
+  explicit operator std::int32_t();
+
+  // Generated from:
+  // cc_bindings_from_rs/test/known_traits/into/into.rs;l=23
+  explicit operator std::int64_t();
+
+  // Generated from:
+  // cc_bindings_from_rs/test/known_traits/into/into.rs;l=30
+  explicit operator rs_std::StrRef();
+
+  // Generated from:
+  // cc_bindings_from_rs/test/known_traits/into/into.rs;l=38
+  explicit operator std::int16_t();
+
+  union {
+    // Generated from:
+    // cc_bindings_from_rs/test/known_traits/into/into.rs;l=14
+    std::int32_t __field0;
+  };
+
+ private:
+  static void __crubit_field_offset_assertions();
+};
+
+// CRUBIT_ANNOTATE: must_bind=
+//
+// Generated from:
+// cc_bindings_from_rs/test/known_traits/into/into.rs;l=46
+struct CRUBIT_INTERNAL_RUST_TYPE(":: into_golden :: ConvertRef") alignas(8)
+    [[clang::trivial_abi]] ConvertRef final {
+ public:
+  // `ConvertRef<'_>` doesn't implement the `Default` trait
+  ConvertRef() = delete;
+
+  // No custom `Drop` impl and no custom "drop glue" required
+  ~ConvertRef() = default;
+  ConvertRef(ConvertRef&&) = default;
+  ConvertRef& operator=(ConvertRef&&) = default;
+
+  // `ConvertRef<'_>` doesn't implement the `Clone` trait
+  ConvertRef(const ConvertRef&) = delete;
+  ConvertRef& operator=(const ConvertRef&) = delete;
+  ConvertRef(::crubit::UnsafeRelocateTag, ConvertRef&& value) {
+    memcpy(this, &value, sizeof(value));
+  }
+
+  // CRUBIT_ANNOTATE: must_bind=
+  //
+  // Generated from:
+  // cc_bindings_from_rs/test/known_traits/into/into.rs;l=50
+  static ::into::ConvertRef create(rs_std::StrRef s);
+
+  // CRUBIT_ANNOTATE: must_bind=
+  //
+  // Generated from:
+  // cc_bindings_from_rs/test/known_traits/into/into.rs;l=55
+  ::into::Convert transmigrate() &&;
+
+  // Generated from:
+  // cc_bindings_from_rs/test/known_traits/into/into.rs;l=60
+  explicit operator rs_std::StrRef();
+
+  // Generated from:
+  // cc_bindings_from_rs/test/known_traits/into/into.rs;l=67
+  explicit operator ::into::Convert();
+
+ private:
+  // Field type has been replaced with a blob of bytes: Can't format `&str`,
+  // because references are only supported in function parameter types, return
+  // types, and consts (b/286256327)
+  unsigned char __field0[16];
+
+ private:
+  static void __crubit_field_offset_assertions();
+};
+
+// CRUBIT_ANNOTATE: must_bind=
+//
+// Generated from:
+// cc_bindings_from_rs/test/known_traits/into/into.rs;l=76
+struct CRUBIT_INTERNAL_RUST_TYPE(":: into_golden :: NotFfiSafe") alignas(8)
+    [[clang::trivial_abi]] NotFfiSafe final {
+ public:
+  // `NotFfiSafe` doesn't implement the `Default` trait
+  NotFfiSafe() = delete;
+
+  // No custom `Drop` impl and no custom "drop glue" required
+  ~NotFfiSafe() = default;
+  NotFfiSafe(NotFfiSafe&&) = default;
+  NotFfiSafe& operator=(NotFfiSafe&&) = default;
+
+  // `NotFfiSafe` doesn't implement the `Clone` trait
+  NotFfiSafe(const NotFfiSafe&) = delete;
+  NotFfiSafe& operator=(const NotFfiSafe&) = delete;
+  NotFfiSafe(::crubit::UnsafeRelocateTag, NotFfiSafe&& value) {
+    memcpy(this, &value, sizeof(value));
+  }
+
+ private:
+  // Field type has been replaced with a blob of bytes: Function pointers can't
+  // have a thunk: Any calling convention other than `extern "C"` requires a
+  // thunk
+  unsigned char __field0[8];
+
+ private:
+  static void __crubit_field_offset_assertions();
+};
+
+// CRUBIT_ANNOTATE: must_bind=
+//
+// Generated from:
+// cc_bindings_from_rs/test/known_traits/into/into.rs;l=85
+struct CRUBIT_INTERNAL_RUST_TYPE(":: into_golden :: ConvertModule") alignas(4)
+    [[clang::trivial_abi]] ConvertModule final {
+ public:
+  // `ConvertModule` doesn't implement the `Default` trait
+  ConvertModule() = delete;
+
+  // Synthesized tuple constructor
+  explicit ConvertModule(std::int32_t __field0)
+      : __field0(std::move(__field0)) {}
+
+  // No custom `Drop` impl and no custom "drop glue" required
+  ~ConvertModule() = default;
+  ConvertModule(ConvertModule&&) = default;
+  ConvertModule& operator=(ConvertModule&&) = default;
+
+  // `ConvertModule` doesn't implement the `Clone` trait
+  ConvertModule(const ConvertModule&) = delete;
+  ConvertModule& operator=(const ConvertModule&) = delete;
+  ConvertModule(::crubit::UnsafeRelocateTag, ConvertModule&& value) {
+    memcpy(this, &value, sizeof(value));
+  }
+  // Generated from:
+  // cc_bindings_from_rs/test/known_traits/into/into.rs;l=91
+  explicit operator std::int32_t();
+
+  // Generated from:
+  // cc_bindings_from_rs/test/known_traits/into/into.rs;l=103
+  explicit operator std::int64_t();
+
+  union {
+    // Generated from:
+    // cc_bindings_from_rs/test/known_traits/into/into.rs;l=85
+    std::int32_t __field0;
+  };
+
+ private:
+  static void __crubit_field_offset_assertions();
+};
+
+static_assert(
+    sizeof(Convert) == 4,
+    "Verify that ADT layout didn't change since this header got generated");
+static_assert(
+    alignof(Convert) == 4,
+    "Verify that ADT layout didn't change since this header got generated");
+static_assert(std::is_trivially_destructible_v<Convert>);
+static_assert(std::is_trivially_move_constructible_v<Convert>);
+static_assert(std::is_trivially_move_assignable_v<Convert>);
+namespace __crubit_internal {
+extern "C" std::int32_t __crubit_thunk_into_ui32(::into::Convert*);
+}
+Convert::operator std::int32_t() {
+  auto&& self = *this;
+  return __crubit_internal::__crubit_thunk_into_ui32(&self);
+}
+namespace __crubit_internal {
+extern "C" std::int64_t __crubit_thunk_into_ui64(::into::Convert*);
+}
+Convert::operator std::int64_t() {
+  auto&& self = *this;
+  return __crubit_internal::__crubit_thunk_into_ui64(&self);
+}
+namespace __crubit_internal {
+extern "C" rs_std::StrRef
+__crubit_thunk_into_u_x00000026_x00000027static_x00000020str(::into::Convert*);
+}
+Convert::operator rs_std::StrRef() {
+  auto&& self = *this;
+  return __crubit_internal::
+      __crubit_thunk_into_u_x00000026_x00000027static_x00000020str(&self);
+}
+namespace __crubit_internal {
+extern "C" std::int16_t __crubit_thunk_into_ui16(::into::Convert*);
+}
+Convert::operator std::int16_t() {
+  auto&& self = *this;
+  return __crubit_internal::__crubit_thunk_into_ui16(&self);
+}
+inline void Convert::__crubit_field_offset_assertions() {
+  static_assert(0 == offsetof(Convert, __field0));
+}
+static_assert(
+    sizeof(ConvertRef) == 16,
+    "Verify that ADT layout didn't change since this header got generated");
+static_assert(
+    alignof(ConvertRef) == 8,
+    "Verify that ADT layout didn't change since this header got generated");
+static_assert(std::is_trivially_destructible_v<ConvertRef>);
+static_assert(std::is_trivially_move_constructible_v<ConvertRef>);
+static_assert(std::is_trivially_move_assignable_v<ConvertRef>);
+namespace __crubit_internal {
+extern "C" void __crubit_thunk_create(rs_std::StrRef,
+                                      ::into::ConvertRef* __ret_ptr);
+}
+inline ::into::ConvertRef ConvertRef::create(rs_std::StrRef s) {
+  crubit::Slot<::into::ConvertRef> __return_value_ret_val_holder;
+  auto* __return_value_storage = __return_value_ret_val_holder.Get();
+  __crubit_internal::__crubit_thunk_create(s, __return_value_storage);
+  return std::move(__return_value_ret_val_holder).AssumeInitAndTakeValue();
+}
+
+namespace __crubit_internal {
+extern "C" void __crubit_thunk_transmigrate(::into::ConvertRef*,
+                                            ::into::Convert* __ret_ptr);
+}
+inline ::into::Convert ConvertRef::transmigrate() && {
+  auto&& self = *this;
+  crubit::Slot<::into::Convert> __return_value_ret_val_holder;
+  auto* __return_value_storage = __return_value_ret_val_holder.Get();
+  __crubit_internal::__crubit_thunk_transmigrate(&self, __return_value_storage);
+  return std::move(__return_value_ret_val_holder).AssumeInitAndTakeValue();
+}
+namespace __crubit_internal {
+extern "C" rs_std::StrRef
+__crubit_thunk_into_u_x00000026_x00000027a_x00000020str(::into::ConvertRef*);
+}
+ConvertRef::operator rs_std::StrRef() {
+  auto&& self = *this;
+  return __crubit_internal::
+      __crubit_thunk_into_u_x00000026_x00000027a_x00000020str(&self);
+}
+namespace __crubit_internal {
+extern "C" void __crubit_thunk_into_uConvert(::into::ConvertRef*,
+                                             ::into::Convert* __ret_ptr);
+}
+ConvertRef::operator ::into::Convert() {
+  auto&& self = *this;
+  crubit::Slot<::into::Convert> __return_value_ret_val_holder;
+  auto* __return_value_storage = __return_value_ret_val_holder.Get();
+  __crubit_internal::__crubit_thunk_into_uConvert(&self,
+                                                  __return_value_storage);
+  return std::move(__return_value_ret_val_holder).AssumeInitAndTakeValue();
+}
+inline void ConvertRef::__crubit_field_offset_assertions() {
+  static_assert(0 == offsetof(ConvertRef, __field0));
+}
+static_assert(
+    sizeof(NotFfiSafe) == 8,
+    "Verify that ADT layout didn't change since this header got generated");
+static_assert(
+    alignof(NotFfiSafe) == 8,
+    "Verify that ADT layout didn't change since this header got generated");
+static_assert(std::is_trivially_destructible_v<NotFfiSafe>);
+static_assert(std::is_trivially_move_constructible_v<NotFfiSafe>);
+static_assert(std::is_trivially_move_assignable_v<NotFfiSafe>);
+inline void NotFfiSafe::__crubit_field_offset_assertions() {
+  static_assert(0 == offsetof(NotFfiSafe, __field0));
+}
+static_assert(
+    sizeof(ConvertModule) == 4,
+    "Verify that ADT layout didn't change since this header got generated");
+static_assert(
+    alignof(ConvertModule) == 4,
+    "Verify that ADT layout didn't change since this header got generated");
+static_assert(std::is_trivially_destructible_v<ConvertModule>);
+static_assert(std::is_trivially_move_constructible_v<ConvertModule>);
+static_assert(std::is_trivially_move_assignable_v<ConvertModule>);
+namespace __crubit_internal {
+extern "C" std::int32_t __crubit_thunk_into_ui32(::into::ConvertModule*);
+}
+ConvertModule::operator std::int32_t() {
+  auto&& self = *this;
+  return __crubit_internal::__crubit_thunk_into_ui32(&self);
+}
+namespace __crubit_internal {
+extern "C" std::int64_t __crubit_thunk_into_ui64(::into::ConvertModule*);
+}
+ConvertModule::operator std::int64_t() {
+  auto&& self = *this;
+  return __crubit_internal::__crubit_thunk_into_ui64(&self);
+}
+inline void ConvertModule::__crubit_field_offset_assertions() {
+  static_assert(0 == offsetof(ConvertModule, __field0));
+}
+}  // namespace into
+#endif  // THIRD_PARTY_CRUBIT_CC_BINDINGS_FROM_RS_TEST_KNOWN_TRAITS_INTO_INTO_GOLDEN
diff --git a/cc_bindings_from_rs/test/known_traits/into/into_cc_api_impl.rs b/cc_bindings_from_rs/test/known_traits/into/into_cc_api_impl.rs
new file mode 100644
index 0000000..3ff816f
--- /dev/null
+++ b/cc_bindings_from_rs/test/known_traits/into/into_cc_api_impl.rs
@@ -0,0 +1,119 @@
+// 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 C++ bindings for the following Rust crate:
+// into_golden
+// Features: supported
+
+#![allow(unused_unsafe, deprecated, non_snake_case, unreachable_code)]
+#![allow(improper_ctypes_definitions)]
+#![deny(warnings)]
+
+const _: () = assert!(::std::mem::size_of::<::into_golden::Convert>() == 4);
+const _: () = assert!(::std::mem::align_of::<::into_golden::Convert>() == 4);
+#[unsafe(no_mangle)]
+unsafe extern "C" fn __crubit_thunk_into_ui32(
+    __self: &'static mut ::core::mem::MaybeUninit<::into_golden::Convert>,
+) -> i32 {
+    unsafe {
+        let __self = __self.assume_init_read();
+        <::into_golden::Convert as ::core::convert::Into<i32>>::into(__self)
+    }
+}
+#[unsafe(no_mangle)]
+unsafe extern "C" fn __crubit_thunk_into_ui64(
+    __self: &'static mut ::core::mem::MaybeUninit<::into_golden::Convert>,
+) -> i64 {
+    unsafe {
+        let __self = __self.assume_init_read();
+        <::into_golden::Convert as ::core::convert::Into<i64>>::into(__self)
+    }
+}
+#[unsafe(no_mangle)]
+unsafe extern "C" fn __crubit_thunk_into_u_x00000026_x00000027static_x00000020str(
+    __self: &'static mut ::core::mem::MaybeUninit<::into_golden::Convert>,
+) -> &'static str {
+    unsafe {
+        let __self = __self.assume_init_read();
+        <::into_golden::Convert as ::core::convert::Into<&'static str>>::into(__self)
+    }
+}
+#[unsafe(no_mangle)]
+unsafe extern "C" fn __crubit_thunk_into_ui16(
+    __self: &'static mut ::core::mem::MaybeUninit<::into_golden::Convert>,
+) -> i16 {
+    unsafe {
+        let __self = __self.assume_init_read();
+        <::into_golden::Convert as ::core::convert::Into<i16>>::into(__self)
+    }
+}
+const _: () = assert!(::core::mem::offset_of!(::into_golden::Convert, 0) == 0);
+const _: () = assert!(::std::mem::size_of::<::into_golden::ConvertRef>() == 16);
+const _: () = assert!(::std::mem::align_of::<::into_golden::ConvertRef>() == 8);
+#[unsafe(no_mangle)]
+unsafe extern "C" fn __crubit_thunk_create(
+    s: &'static str,
+    __ret_ptr: *mut core::ffi::c_void,
+) -> () {
+    unsafe {
+        let __rs_return_value = ::into_golden::ConvertRef::create(s);
+        (__ret_ptr as *mut ::into_golden::ConvertRef<'static>).write(__rs_return_value);
+    }
+}
+#[unsafe(no_mangle)]
+unsafe extern "C" fn __crubit_thunk_transmigrate(
+    __self: &'static mut ::core::mem::MaybeUninit<::into_golden::ConvertRef<'static>>,
+    __ret_ptr: *mut core::ffi::c_void,
+) -> () {
+    unsafe {
+        let __self = __self.assume_init_read();
+        let __rs_return_value = ::into_golden::ConvertRef::transmigrate(__self);
+        (__ret_ptr as *mut ::into_golden::Convert).write(__rs_return_value);
+    }
+}
+#[unsafe(no_mangle)]
+unsafe extern "C" fn __crubit_thunk_into_u_x00000026_x00000027a_x00000020str(
+    __self: &'static mut ::core::mem::MaybeUninit<::into_golden::ConvertRef<'static>>,
+) -> &'static str {
+    unsafe {
+        let __self = __self.assume_init_read();
+        <::into_golden::ConvertRef as ::core::convert::Into<&'static str>>::into(__self)
+    }
+}
+#[unsafe(no_mangle)]
+unsafe extern "C" fn __crubit_thunk_into_uConvert(
+    __self: &'static mut ::core::mem::MaybeUninit<::into_golden::ConvertRef<'static>>,
+    __ret_ptr: *mut core::ffi::c_void,
+) -> () {
+    unsafe {
+        let __self = __self.assume_init_read();
+        let __rs_return_value = <::into_golden::ConvertRef as ::core::convert::Into<
+            ::into_golden::Convert,
+        >>::into(__self);
+        (__ret_ptr as *mut ::into_golden::Convert).write(__rs_return_value);
+    }
+}
+const _: () = assert!(::std::mem::size_of::<::into_golden::NotFfiSafe>() == 8);
+const _: () = assert!(::std::mem::align_of::<::into_golden::NotFfiSafe>() == 8);
+const _: () = assert!(::std::mem::size_of::<::into_golden::ConvertModule>() == 4);
+const _: () = assert!(::std::mem::align_of::<::into_golden::ConvertModule>() == 4);
+#[unsafe(no_mangle)]
+unsafe extern "C" fn __crubit_thunk_into_ui32(
+    __self: &'static mut ::core::mem::MaybeUninit<::into_golden::ConvertModule>,
+) -> i32 {
+    unsafe {
+        let __self = __self.assume_init_read();
+        <::into_golden::ConvertModule as ::core::convert::Into<i32>>::into(__self)
+    }
+}
+#[unsafe(no_mangle)]
+unsafe extern "C" fn __crubit_thunk_into_ui64(
+    __self: &'static mut ::core::mem::MaybeUninit<::into_golden::ConvertModule>,
+) -> i64 {
+    unsafe {
+        let __self = __self.assume_init_read();
+        <::into_golden::ConvertModule as ::core::convert::Into<i64>>::into(__self)
+    }
+}
+const _: () = assert!(::core::mem::offset_of!(::into_golden::ConvertModule, 0) == 0);
diff --git a/cc_bindings_from_rs/test/known_traits/into/into_test.cc b/cc_bindings_from_rs/test/known_traits/into/into_test.cc
new file mode 100644
index 0000000..be119a5
--- /dev/null
+++ b/cc_bindings_from_rs/test/known_traits/into/into_test.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 "cc_bindings_from_rs/test/known_traits/into/into.h"
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+namespace crubit {
+namespace {
+
+TEST(IntoTest, ConvertConversionOperators) {
+  into::Convert convert(1563);
+  EXPECT_EQ(static_cast<int32_t>(convert), 1563);
+  EXPECT_EQ(static_cast<int64_t>(convert), 1563);
+  EXPECT_EQ(static_cast<rs_std::StrRef>(convert), "Convert");
+  EXPECT_EQ(static_cast<int16_t>(convert), 1563);
+}
+
+TEST(IntoTest, ConvertRefConversionOperators) {
+  into::ConvertRef convert_ref =
+      into::ConvertRef::create(rs_std::StrRef("Hello, World!"));
+  EXPECT_EQ(static_cast<rs_std::StrRef>(convert_ref), "Hello, World!");
+  EXPECT_EQ(static_cast<into::Convert>(convert_ref).__field0, 42);
+}
+
+}  // namespace
+}  // namespace crubit
diff --git a/cc_bindings_from_rs/test/lifetimes/lifetimes_cc_api.h b/cc_bindings_from_rs/test/lifetimes/lifetimes_cc_api.h
index e435d6c..71a244b 100644
--- a/cc_bindings_from_rs/test/lifetimes/lifetimes_cc_api.h
+++ b/cc_bindings_from_rs/test/lifetimes/lifetimes_cc_api.h
@@ -85,6 +85,15 @@
       "lifetime", "static")]] borrow_from_static_self()
       const& [[clang::annotate_type("lifetime", "static")]];
 
+  // Generated from:
+  // cc_bindings_from_rs/test/lifetimes/lifetimes.rs;l=19
+  explicit operator std::int32_t const& [[clang::annotate_type("lifetime",
+                                                               "a")]] ();
+
+  // Generated from:
+  // cc_bindings_from_rs/test/lifetimes/lifetimes.rs;l=25
+  explicit operator std::int32_t();
+
  private:
   // Field type has been replaced with a blob of bytes: Can't format `&i32`,
   // because references are only supported in function parameter types, return
@@ -256,6 +265,26 @@
   auto&& self = *this;
   return __crubit_internal::__crubit_thunk_borrow_ufrom_ustatic_uself(self);
 }
+namespace __crubit_internal {
+extern "C" std::int32_t const& [[clang::annotate_type(
+    "lifetime",
+    "a")]] __crubit_thunk_into_u_x00000026_x00000027a_x00000020i32(::lifetimes::
+                                                                       StructWithLifetime*);
+}
+StructWithLifetime::operator std::int32_t const& [[clang::annotate_type(
+    "lifetime", "a")]] () {
+  auto& self = const_cast<std::remove_cvref_t<decltype(*this)>&>(*this);
+  return __crubit_internal::
+      __crubit_thunk_into_u_x00000026_x00000027a_x00000020i32(&self);
+}
+namespace __crubit_internal {
+extern "C" std::int32_t __crubit_thunk_into_ui32(
+    ::lifetimes::StructWithLifetime*);
+}
+StructWithLifetime::operator std::int32_t() {
+  auto& self = const_cast<std::remove_cvref_t<decltype(*this)>&>(*this);
+  return __crubit_internal::__crubit_thunk_into_ui32(&self);
+}
 inline void StructWithLifetime::__crubit_field_offset_assertions() {
   static_assert(0 == offsetof(StructWithLifetime, field_with_lifetime));
 }
diff --git a/cc_bindings_from_rs/test/lifetimes/lifetimes_cc_api_impl.rs b/cc_bindings_from_rs/test/lifetimes/lifetimes_cc_api_impl.rs
index 9893128..cf5fc46 100644
--- a/cc_bindings_from_rs/test/lifetimes/lifetimes_cc_api_impl.rs
+++ b/cc_bindings_from_rs/test/lifetimes/lifetimes_cc_api_impl.rs
@@ -87,6 +87,26 @@
 ) -> &'static i32 {
     unsafe { ::lifetimes_golden::StructWithLifetime::borrow_from_static_self(__self) }
 }
+#[unsafe(no_mangle)]
+unsafe extern "C" fn __crubit_thunk_into_u_x00000026_x00000027a_x00000020i32(
+    __self: &'static mut ::core::mem::MaybeUninit<::lifetimes_golden::StructWithLifetime<'static>>,
+) -> &'static i32 {
+    unsafe {
+        let __self = __self.assume_init_read();
+        <::lifetimes_golden::StructWithLifetime as ::core::convert::Into<&'static i32>>::into(
+            __self,
+        )
+    }
+}
+#[unsafe(no_mangle)]
+unsafe extern "C" fn __crubit_thunk_into_ui32(
+    __self: &'static mut ::core::mem::MaybeUninit<::lifetimes_golden::StructWithLifetime<'static>>,
+) -> i32 {
+    unsafe {
+        let __self = __self.assume_init_read();
+        <::lifetimes_golden::StructWithLifetime as ::core::convert::Into<i32>>::into(__self)
+    }
+}
 const _: () = assert!(
     ::core::mem::offset_of!(::lifetimes_golden::StructWithLifetime, field_with_lifetime) == 0
 );