Skip to content

Instantly share code, notes, and snippets.

@adetaylor
Last active October 10, 2022 13:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save adetaylor/28cb49a866ce4e03ee749f264b0fc1cb to your computer and use it in GitHub Desktop.
Save adetaylor/28cb49a866ce4e03ee749f264b0fc1cb to your computer and use it in GitHub Desktop.


Summary

Allow methods to be called on container types without allowing references to the contained type.

Motivation

It's becoming common to make mixed Rust and C++ binaries using tools such as cxx, autocxx or crubit. Such tools aim to make it ergonomic and natural to call C++ methods on C++ types from Rust.

The most natural way to do this would be to have a plain Rust reference to a C++ type, but this can lead to UB. In realistic mixed-language scenarios, it's impossible to eliminate the chance that C++ will retain a reference to the same data, and that reference may enter the Rust domain at multiple points. Unless the type is a zero-sized type, such overlapping Rust references are undefined behavior.

Instead, Rust/C++ interop tools (autocxx and crubit) want to use a C++-specific 'smart reference' type in Rust, such as CppRef<T>, which actually wraps a raw Rust pointer. For smooth interop, it's important to be able to call methods on such 'smart references'. For example:

let some_cpp_ref: CppRef<SomeCppType> = ...; // obtained from C++
some_cpp_ref.SomeCppMethod(); // method belongs to SomeCppType

Currently, this can be achieved using the arbitrary_self_types feature, but only by implementing Deref.

#![feature(arbitrary_self_types)]

struct SomeCppType(u32); // would be generated by interop tools

impl SomeCppType { // would be generated by interop tools
  fn SomeCppMethod(self: CppRef<SomeCppType>) {
    // call through to C++ using the pointer inside the CppRef
  }
}

struct CppRef<T>(*const T);

impl<T> core::ops::Deref for CppRef<T> {
  type Target = T;
  fn deref(&self) -> &T {
    unreachable!()
  }
}

fn main() {
  let my_cpp_type = SomeCppType(13);
  let pointer_to_cpp_type: *const SomeCppType = &my_cpp_type; // really would get from C++
  let cpp_ref = CppRef(pointer_to_cpp_type);
  cpp_ref.SomeCppMethod();
}

Note that Deref::deref is not actually used; instead the method call ends up being simply <Deref::Target>::SomeCppMethod(cpp_ref). But the existence of Deref::deref is a concern for us, because we want to make it impossible to create Rust references to these C++ types.

This issue proposes that it should be possible to call methods on a container type without requiring the implementation of Deref.

Proposal

This proposes that the (existing, unstable) core::ops::Receiver trait gain a Target associated type:

pub trait Receiver {
  type Target: ?Sized;
}

Receiver<Target> means that a type may act as a method receiver on behalf of the target. In our example above, CppRef<T> would simply implement Receiver instead of Deref.

Implementation

The check to ensure method receivers are well formed would consider any type implementing Receiver<Target=T> to be a valid receiver type for T. (Previously, Receiver was only considered if #[arbitrary_self_types] was inactive, and only for the results of a deref).

Most changes are within probing for methods. Method candidates will be considered valid for X if the receiver implements Receiver<Target=X>. For consistency with the other probed method types, such methods will be considered for ADTs and dynamic types (dyn Trait). Such probing won't apply to foreign types (extern type).

Feature gating

Until this is stabilised, this new behavior becomes active only if both the receiver_trait and arbitrary_self_types features are enabled.

Drawbacks

None known, but this is a powerful trait so we should think of unexpected uses carefully. (On the one hand, its capabilities are already possible using Deref; on the other hand, it's evaluated at a different point in method resolution.)

Open questions

  • Do we need an additional feature gate, or is the combination of existing unstable features sufficient?
  • Should *const T and *mut T implement this trait?
  • Does it make sense to allow this for dyn Trait? (Experimentation required)
  • Should the extra search process for #[rustc_has_incoherent_inherent_impls] also look for these new kinds of receiver?

Alternatives

  • C++ interop avoids references and always uses raw pointers. This is impractical because interop would require extensive use of unsafe, even in cases where tooling can prove certain invariants are safe (for example, non-nullness or lifetime relationships).
  • C++ interop represents C++ types as zero-sized types in Rust. This prevents UB from overlapping references. This is the approach used by cxx for its opaque types (though not its "trivial" types). But some interop tools would like to store C++ types on the Rust stack, using the approaches pioneered by moveit, and that's not possible if the types are zero-sized.
  • Build on extern types (currently these are not sized so do not serve our purposes).
  • A variation on Deref is used such that extra method candidates are returned during the autoderef steps. This proved to be more complex than simply checking the Receiverness of a receiver at the end of that operation.

Acknowledgements

Thanks to Devin Jeanpierre and Luca Versari for reviewing an earlier similar idea and suggesting that this was a potentially better approach. Thanks to Manish Goregaokar and Tyler Mandry for reviewing an earlier draft of this proposal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment