Repository: dtolnay/cxx (language: rust)

Issue:
Expose a way to bypass is_trivially_destructible/_move_constructible check on extern types passed by value
As discussed in #339.


Discussion / hints:

Fix patch:
diff --git a/gen/src/builtin.rs b/gen/src/builtin.rs
index ab2b46536..7a4ddcd44 100644
--- a/gen/src/builtin.rs
+++ b/gen/src/builtin.rs
@@ -23,6 +23,7 @@ pub struct Builtins<'a> {
     pub rust_slice_new: bool,
     pub rust_slice_repr: bool,
     pub exception: bool,
+    pub relocatable: bool,
     pub content: Content<'a>,
 }
 
@@ -73,6 +74,10 @@ pub(super) fn write(out: &mut OutFile) {
         include.basetsd = true;
     }
 
+    if builtin.relocatable {
+        include.type_traits = true;
+    }
+
     out.begin_block(Block::Namespace("rust"));
     out.begin_block(Block::InlineNamespace("cxxbridge05"));
     writeln!(out, "// #include \"rust/cxx.h\"");
@@ -100,6 +105,7 @@ pub(super) fn write(out: &mut OutFile) {
     ifndef::write(out, builtin.rust_fn, "CXXBRIDGE05_RUST_FN");
     ifndef::write(out, builtin.rust_error, "CXXBRIDGE05_RUST_ERROR");
     ifndef::write(out, builtin.rust_isize, "CXXBRIDGE05_RUST_ISIZE");
+    ifndef::write(out, builtin.relocatable, "CXXBRIDGE05_RELOCATABLE");
 
     if builtin.manually_drop {
         out.next_section();
diff --git a/gen/src/write.rs b/gen/src/write.rs
index 2c3234c72..c806e55d0 100644
--- a/gen/src/write.rs
+++ b/gen/src/write.rs
@@ -262,7 +262,7 @@ fn check_enum<'a>(out: &mut OutFile<'a>, enm: &'a Enum) {
 }
 
 fn check_trivial_extern_type(out: &mut OutFile, id: &Pair) {
-    // NOTE: The following two static assertions are just nice-to-have and not
+    // NOTE: The following static assertion is just nice-to-have and not
     // necessary for soundness. That's because triviality is always declared by
     // the user in the form of an unsafe impl of cxx::ExternType:
     //
@@ -273,31 +273,35 @@ fn check_trivial_extern_type(out: &mut OutFile, id: &Pair) {
     //
     // Since the user went on the record with their unsafe impl to unsafely
     // claim they KNOW that the type is trivial, it's fine for that to be on
-    // them if that were wrong.
+    // them if that were wrong. However, in practice correctly reasoning about
+    // the relocatability of C++ types is challenging, particularly if the type
+    // definition were to change over time, so for now we add this check.
+    //
+    // There may be legitimate reasons to opt out of this assertion for support
+    // of types that the programmer knows are soundly Rust-movable despite not
+    // being recognized as such by the C++ type system due to a move constructor
+    // or destructor. To opt out of the relocatability check, they need to do
+    // one of the following things in any header used by `include!` in their
+    // bridge.
+    //
+    //      --- if they define the type:
+    //      struct MyType {
+    //        ...
+    //    +   using IsRelocatable = std::true_type;
+    //      };
+    //
+    //      --- otherwise:
+    //    + template <>
+    //    + struct rust::IsRelocatable<MyType> : std::true_type {};
     //
-    // There may be a legitimate reason we'll want to remove these assertions
-    // for support of types that the programmer knows are Rust-movable despite
-    // not being recognized as such by the C++ type system due to a move
-    // constructor or destructor.
 
-    let id = &id.to_fully_qualified();
-    out.include.type_traits = true;
-    writeln!(out, "static_assert(");
-    writeln!(
-        out,
-        "    ::std::is_trivially_move_constructible<{}>::value,",
-        id,
-    );
-    writeln!(
-        out,
-        "    \"type {} marked as Trivial in Rust is not trivially move constructible in C++\");",
-        id,
-    );
+    let id = id.to_fully_qualified();
+    out.builtin.relocatable = true;
     writeln!(out, "static_assert(");
-    writeln!(out, "    ::std::is_trivially_destructible<{}>::value,", id);
+    writeln!(out, "    ::rust::IsRelocatable<{}>::value,", id);
     writeln!(
         out,
-        "    \"type {} marked as Trivial in Rust is not trivially destructible in C++\");",
+        "    \"type {} marked as Trivial in Rust is not trivially move constructible and trivially destructible in C++\");",
         id,
     );
 }
diff --git a/include/cxx.h b/include/cxx.h
index 45c382806..b048e2762 100644
--- a/include/cxx.h
+++ b/include/cxx.h
@@ -267,6 +267,27 @@ using isize = ssize_t;
 std::ostream &operator<<(std::ostream &, const String &);
 std::ostream &operator<<(std::ostream &, const Str &);
 
+// IsRelocatable<T> is used in assertions that a C++ type passed by value
+// between Rust and C++ is soundly relocatable by Rust.
+//
+// There may be legitimate reasons to opt out of the check for support of types
+// that the programmer knows are soundly Rust-movable despite not being
+// recognized as such by the C++ type system due to a move constructor or
+// destructor. To opt out of the relocatability check, do either of the
+// following things in any header used by `include!` in the bridge.
+//
+//      --- if you define the type:
+//      struct MyType {
+//        ...
+//    +   using IsRelocatable = std::true_type;
+//      };
+//
+//      --- otherwise:
+//    + template <>
+//    + struct rust::IsRelocatable<MyType> : std::true_type {};
+template <typename T>
+struct IsRelocatable;
+
 // Snake case aliases for use in code that uses this style for type names.
 using string = String;
 using str = Str;
@@ -281,6 +302,8 @@ template <typename Signature, bool Throws = false>
 using fn = Fn<Signature, Throws>;
 template <typename Signature>
 using try_fn = TryFn<Signature>;
+template <typename T>
+using is_relocatable = IsRelocatable<T>;
 
 
 
@@ -552,5 +575,42 @@ template <typename T>
 Vec<T>::Vec(unsafe_bitcopy_t, const Vec &bits) noexcept : repr(bits.repr) {}
 #endif // CXXBRIDGE05_RUST_VEC
 
+#ifndef CXXBRIDGE05_RELOCATABLE
+#define CXXBRIDGE05_RELOCATABLE
+namespace detail {
+template <typename... Ts>
+struct make_void {
+  using type = void;
+};
+
+template <typename... Ts>
+using void_t = typename make_void<Ts...>::type;
+
+template <typename Void, template <typename...> class, typename...>
+struct detect : std::false_type {};
+template <template <typename...> class T, typename... A>
+struct detect<void_t<T<A...>>, T, A...> : std::true_type {};
+
+template <template <typename...> class T, typename... A>
+using is_detected = detect<void, T, A...>;
+
+template <typename T>
+using detect_IsRelocatable = typename T::IsRelocatable;
+
+template <typename T>
+struct get_IsRelocatable
+    : std::is_same<typename T::IsRelocatable, std::true_type> {};
+} // namespace detail
+
+template <typename T>
+struct IsRelocatable
+    : std::conditional<
+          detail::is_detected<detail::detect_IsRelocatable, T>::value,
+          detail::get_IsRelocatable<T>,
+          std::integral_constant<
+              bool, std::is_trivially_move_constructible<T>::value &&
+                        std::is_trivially_destructible<T>::value>>::type {};
+#endif // CXXBRIDGE05_RELOCATABLE
+
 } // namespace cxxbridge05
 } // namespace rust


Test patch: