By-value trivially-relocatable types with destructors
Types like unique_ptr<int> have nontrivial destructors, but nevertheless can be supported by value in Rust. They can be moved around in memory with memcpy, and we need only destroy the final object via Drop.
C++ calls this property "trivially-relocatable" and it will finally be standardized in C++26.
While unique_ptr has special support in cxx, many other types fall into this category, e.g. PIMPL classes. Today, cxx requires them to be Opaque and therefore boxed as e.g. UniquePtr, adding a layer of indirection and an extra heap allocation. I would suggest a new cxx::kind::Relocatable for these types:
#[cxx::bridge]
mod bridge {
extern "C++" {
type Database;
}
}
unsafe impl ExternType for Database {
type Id = type_id!("Database");
type Kind = cxx::kind::Relocatable;
}
cxx would generate impl Drop for Database which calls the C++ destructor, and generate static_assert(std::is_trivially_relocatable<Database>). (As that type trait isn't available before C++26, cxx can define an alternative extension point similar to the somewhat-unfortunately-named cxx::IsRelocatable)
An alternative would be to extend cxx::kind::Trivial to allow types with destructors, but this would require impl Drop for such trivial types, which adds overhead unless eliminated by e.g. LTO.
Would you be likely to accept a patch for this feature?
I found that there's already rust::cxxbridge1::IsRelocatable, which in addition to the documented "trivially movable and trivially destructible" allows types with a IsRelocatable marker.
It seems that handwritten unsafe bindings work:
C++:
class CppType {
int val;
public:
// non-trivial move ctor and non-trivial dtor
CppType(CppType&&) noexcept;
~CppType();
// promise that move+destroy old is equivalent to memcpy+forget old:
using IsRelocatable = std::true_type;
};
void use_by_value(CppType p);
template<typename T> void call_destructor(T& obj) { obj.~T(); }
Rust:
struct CppType {
val: i32,
}
unsafe impl ExternType for CppType {
type Id = type_id!("CppType");
type Kind = cxx::kind::Trivial;
}
impl Drop for CppType {
fn drop(&mut self) {
bridge::CppType_dtor(self)
}
}
#[cxx::bridge]
pub mod bridge {
unsafe extern "C++" {
include!("header.h");
type CppType = super::CppType;
#[cxx_name = "call_destructor"]
fn CppType_dtor(obj: &mut CppType);
fn use_by_value(p: CppType);
}
I just came up with this approach and it seems to work, but is it sound?
Yes that is the intended way to express this use case.
If adding IsRelocatable into the definition of CppType is not possible then you can alternatively insert the following into any header included in the bridge:
#pragma once
#include "rust/cxx.h"
#include "..."
#include <type_traits>
template <>
struct rust::IsRelocatable<CppType> : std::true_type {};
I would accept a PR to eliminate the need for call_destructor using derive(Drop) on a type alias.
#[cxx::bridge]
mod ffi {
unsafe extern "C++" {
#[derive(Drop)]
type CppType = super::CppType;
}
}
which will produce impl Drop for super::CppType forwarding to the C++ type's destructor.