Skip to content

Conversation

@anforowicz
Copy link
Contributor

PTAL?

If this PR looks okay, then the next commit/PR (anforowicz@8ab4ecc) should be able to add support for Vec<Box<T>> (because of how Box<T> is special when evaluating the orphan rule.

/cc @zetafunction who has kindly provided initial feedback/review at anforowicz#4

@anforowicz anforowicz force-pushed the arbitrary-inner-type-in-named-impl-key branch from 6e061fe to 9212d1d Compare October 13, 2025 19:56
This commit refactors `write_...` functions to enable formatting a C++
representation of a `Type` into any generic `impl std::fmt::Write`
instead of only supporting formatting into an `OutFile`.  This is then
used to provide `fn stringify_type`.

The new function is not used in this commit, but will become quite
handy in a follow-up commit when `inner` type of generics/templates may
not necessarily be a simple `std::fmt`-friendly `Type::Ident`.  Without
the refactored `write_...` APIs, the follow-up would have to split
*single* `writeln!` invocations (such as the ones in `fn
write_shared_ptr` or `fn write_cxx_vector`) into *multiple* ones - e.g.
into: `write!` + `write_type` + `write!` + `write_type` + `writeln!`.

The new function is a little bit redundant wrt the already existing
`trait ToTypename`, but we can't replace that trait just yet:

* The trait has slightly different semantics (formatting not the whole
  type represented by `self`, but only formatting the inner `T`).
* The trait works with `Ident` rather than `Type` (this will change
  in the follow-up commit mentioned above).
Before this commit `NamedImplKey` could only represent the inner type as
`rust: &'a Ident`.  For example, it could represent `Vec<Foo>` but could
not represent `Vec<Box<Foo>>` where the inner type (`Box<Foo>` in our
example) is not a simple identifier.

After this commit `NamedImplKey` is refactored to support an arbitrary
inner type.  Note that (to simplify and to minimize risks associated
with this commit) this new ability is not actually used at this point -
it is planned to be used in follow-up commits to incrementally relax
generic type argument restrictions in `syntax/check.rs`.

This commit is quite big, but it seems difficult to extract some changes
to smaller, separate commits, because all of the changes stem from the
refactoring of the `NamedImplKey`.  At a high-level this commit contains
the following changes:

1. `syntax/instantiate.rs`: Changing `pub rust: &'a Ident` field of
   `NamedImplKey` to `pub inner: &'a Type`.  This is the main/root
   change in this commit.
2. `gen/src/write.rs`: supporting arbitrary inner types
   when writing C++ thunks exposing instantiations/monomorphizations of
   templates/generics supported by `cxx`.
    * This depends on `fn stringify_type` introduced in
      `gen/src/write.rs` in an earlier commit.
    * Handling arbitrary inner types *in general* means that we can
      delete `enum UniquePtr` which provided handling of two *specific*
      inner types.
3. `macro/src/expand.rs`: supporting arbitrary inner types
   when writing Rust thunks exposing instantiations/monomorphizations of
   templates/generics supported by `cxx`.
    * Using `#inner` instead of `#ident` may now (optionally) cover
      generic lifetime arguments.  This is why this commit also changes
      `macro/src/generics.rs`.  And this is why we can no longer need
      the `ty_generics` field from `struct Impl`.
    * One minor functional change here is changing the error messages
      so that references to type names are generated purely in the
      generated bindings, without depending on `fn display_namespaced`.
4. `syntax/mangle.rs`: supporting mangling of individual types.  This
   helps to:
    * Support the (long-term, not-yet-realized) high-level goal of
      actually allowing and using arbitrary inner types
    * Deduplicate mangling code details that were somewhat duplicated
      in `macro/src/expand.rs` and `gen/src/write.rs`.
5. `syntax/types.rs`: Supporting arbitrary inner types in
    * `fn is_maybe_trivial`
    * `fn is_local` (this function supports an earlier refactoring
      that changed how `cxx` decides whether to provide an *implicit*
      impl of a given generic/template instantiation/monomorphization)
@anforowicz anforowicz force-pushed the arbitrary-inner-type-in-named-impl-key branch from 9212d1d to 18a1c1c Compare October 27, 2025 18:42
@anforowicz
Copy link
Contributor Author

Rebased + minor tweak after a self-review: s/ stringify! / ::core::stringify! in generated code

Copy link
Owner

@dtolnay dtolnay left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you — I am merging this with some changes in #1669.

let inner = ty.to_typename(out.types);
let instance = ty.to_mangled(out.types);
let inner = stringify_type(ty, out.types);
let instance = crate::syntax::mangle::type_(ty)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mangle module is already imported and used elsewhere in this file.

Suggested change
let instance = crate::syntax::mangle::type_(ty)
let instance = mangle::type_(ty)


impl<'a> Write for OutFile<'a> {
fn write_str(&mut self, s: &str) -> fmt::Result {
self.content.borrow_mut().write(s);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self.content.borrow_mut().write(s);
self.content.get_mut().write(s);

/// before `syntax/check.rs` has rejected unsupported generic type parameters.
pub(crate) fn type_(t: &Type) -> Option<Symbol> {
match t {
Type::Ident(named_type) => Some(join!(named_type.rust)),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going to give conflicting symbols when an identical Rust name exists in multiple namespaces. The original code before this PR was mangling using the C++ namespace and name.

// src/lib.rs

mod two;

#[cxx::bridge(namespace = "one")]
pub mod one {
    struct Context { x: i32 }
    impl Box<Context> {}
}
// src/two.rs

#[cxx::bridge(namespace = "two")]
pub mod two {
    struct Context { x: i8 }
    impl Box<Context> {}
}
error: symbol `cxxbridge1$box$Context$alloc` is already defined
 --> src/two.rs:6:23
  |
6 |     impl Box<Context> {}
  |                       ^^

or for a C++ type:

  = note: rust-lld: error: duplicate symbol: cxxbridge1$unique_ptr$Context$null
          >>> defined at main.rs.cc:64 (target/debug/build/example-aff44aa21d738c2a/out/cxxbridge/sources/example/src/main.rs.cc:64)
          >>>            4be10217b215df70-main.rs.o:(cxxbridge1$unique_ptr$Context$null) in archive target/debug/build/example-aff44aa21d738c2a/out/libexample-cxx.a
          >>> defined at two.rs.cc:64 (target/debug/build/example-aff44aa21d738c2a/out/cxxbridge/sources/example/src/two.rs.cc:64)
          >>>            4be10217b215df70-two.rs.o:(.text.cxxbridge1$unique_ptr$Context$null+0x0) in archive target/debug/build/example-aff44aa21d738c2a/out/libexample-cxx.a

.map_or(key.end_span, |explicit| explicit.brace_token.span.join());
let unsafe_token = format_ident!("unsafe", span = begin_span);
let prevent_unwind_drop_label = format!("::{} as Drop>::drop", ident);
let prevent_unwind_drop_label = quote! { #inner }.to_string();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not an adequate replacement for what was here before.

#pragma once
#include <rust/cxx.h>

class Thing;

inline void take_thing(rust::Box<Thing>) {}
#[cxx::bridge]
mod ffi {
    extern "Rust" {
        type Thing;
    }
    unsafe extern "C++" {
        include!("example/include/header.h");
        fn take_thing(_: Box<Thing>);
    }
}

struct Thing;

impl Drop for Thing {
    fn drop(&mut self) {
        panic!("...");
    }
}

fn main() {
    ffi::take_thing(Box::new(Thing));
}

Before:

panic in ffi function <example::ffi::Thing as Drop>::drop, aborting.

After:

panic in ffi function <example::ffiThing, aborting.

#[cfg_attr(not(proc_macro), expect(dead_code))]
pub gt_token: Option<Token![>]>,
#[cfg_attr(not(proc_macro), expect(dead_code))]
/// Mangled form of the `outer` type.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment does not match the behavior of NamedImplKey::new.

Suggested change
/// Mangled form of the `outer` type.
/// Mangled form of the `inner` type.

"cxxbridge1$unique_ptr$std$vector${}$",
resolve.name.to_symbol(),
);
let unique_ptr_prefix = format!("cxxbridge1$unique_ptr$std$vector${}$", key.symbol,);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let unique_ptr_prefix = format!("cxxbridge1$unique_ptr$std$vector${}$", key.symbol,);
let unique_ptr_prefix = format!("cxxbridge1$unique_ptr$std$vector${}$", key.symbol);

unsafe extern "C" {
#[link_name = #link_new]
fn __vector_new #impl_generics() -> *mut ::cxx::CxxVector<#elem #ty_generics>;
fn __vector_new #impl_generics() -> *mut ::cxx::CxxVector<#inner>;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not going to compile without #ty_generics.

#[cxx::bridge]
mod ffi {
    struct Borrowed<'a> {
        s: &'a str,
    }
    extern "Rust" {
        fn f(_: &CxxVector<Borrowed>);
    }
}

fn f(_: &cxx::CxxVector<ffi::Borrowed>) {}
error[E0106]: missing lifetime specifier
 --> src/main.rs:7:28
  |
7 |         fn f(_: &CxxVector<Borrowed>);
  |                            ^^^^^^^^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'a` lifetime
  |
7 |         fn f(_: &CxxVector<Borrowed<'a>>);
  |                                    ++++

#unsafe_token impl #impl_generics ::cxx::memory::SharedPtrTarget for #inner #ty_generics {
fn __typename(f: &mut ::cxx::core::fmt::Formatter<'_>) -> ::cxx::core::fmt::Result {
f.write_str(#name)
f.write_str(::core::stringify!(#inner))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using stringify for this is not a good approach.

#[cxx::bridge]
pub mod ffi {
    struct Context<'a> {
        session: &'a str,
    }
    struct Server<'srv> {
        ctx: SharedPtr<Context<'srv>>,
    }
    struct Client<'clt> {
        ctx: SharedPtr<Context<'clt>>,
    }
}

fn main() {
    let _ = *cxx::SharedPtr::<ffi::Context>::null();
}

Before:

called deref on a null SharedPtr<Context>

After:

called deref on a null SharedPtr<Context < 'srv >>

The spacing is wonky, and this is using a random one of the lifetime names which is going to be misleading for the user if their panic is happening in "client" code that has nothing to do with "server" code.

(resolved_generics, Some(resolved_generics))
} else {
(
explicit_generics,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This explicit_generics is never the best set of lifetime names to use for an impl. In this codepath there is no explicit impl, so explicit_generics is populated with lifetime names from some random usage of the generic type in a function signature or struct. In general those lifetime names are not meaningful at all outside the context of that single function or struct. This code should not be propagating them onto an impl block (which is going to become visible in rustdoc).

For example:

struct Context<'sess> {
    session: &'sess str,
}
struct Server<'srv> {
    ctx: SharedPtr<Context<'srv>>,
}
struct Client<'clt> {
    ctx: SharedPtr<Context<'clt>>,
}

It does not make sense for the documentation of Context to be rendered as:

  • impl<'sess> ExternType for Context<'sess>
  • impl<'srv> SharedPtrTarget for Context<'srv>

The correct impl would be impl<'sess> SharedPtrTarget for Context<'sess> (which was generated before this PR).

Comment on lines +134 to +138
Type::RustBox(ty1) => type_(&ty1.inner).map(|s| join!("box", s)),
Type::RustVec(ty1) => type_(&ty1.inner).map(|s| join!("rust_vec", s)),
Type::UniquePtr(ty1) => type_(&ty1.inner).map(|s| join!("unique_ptr", s)),
Type::SharedPtr(ty1) => type_(&ty1.inner).map(|s| join!("shared_ptr", s)),
Type::WeakPtr(ty1) => type_(&ty1.inner).map(|s| join!("weak_ptr", s)),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are all currently unreachable, right?

Suggested change
Type::RustBox(ty1) => type_(&ty1.inner).map(|s| join!("box", s)),
Type::RustVec(ty1) => type_(&ty1.inner).map(|s| join!("rust_vec", s)),
Type::UniquePtr(ty1) => type_(&ty1.inner).map(|s| join!("unique_ptr", s)),
Type::SharedPtr(ty1) => type_(&ty1.inner).map(|s| join!("shared_ptr", s)),
Type::WeakPtr(ty1) => type_(&ty1.inner).map(|s| join!("weak_ptr", s)),

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, correct.

@dtolnay dtolnay merged commit 1cbc3e3 into dtolnay:master Nov 15, 2025
29 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants