Crate cxx_enumext

Source
Expand description

cxx-enumext is a Rust crate that extends the cxx library to provide a mapping between Rust variant enums and c++ standards like std::variant, std::optional and std::expected. Unfortunately the memory modal and null pointer optimisations in play make direct mappings between the two extremely difficult. As such, cxx-enumext provides analogs which should be identical in usage.

rust::enm::variant is a analog for std::variant which can fully map a #[repr(c)] Rust Enum. The #[cxx_enumext::extern_type] macro will transform yur enums apropreatly as well as provide some sanity check static assertions about their contents for the current limitations of cxx ffi.

rust::enum::optional is a derivation from rust::enm::variant which provides the interface of std::optional

rust::enum::expected is a derivation from rust::enm::variant which provides the interface of std::expected

§Quick tutorial

To use cxx-enumext, first start by adding cxx to your project. Then add the following to your Cargo.toml:

[dependencies]
cxx-enumext = "0.1"

If your going to put other extern(d) types or shared types inside your enums declare them in a seperate module so that you don’t deal with the c++ compiler complaing about incompleate types

//! my_crate/src/data.rs

#[cxx::bridge]
pub mod ffi {

    #[derive(Debug)]
    struct SharedData {
        size: i64,
        tags: Vec<String>,
    }

    extern "Rust" {
        type RustValue;
        fn read(&self) -> &String;

        fn new_rust_value() -> Box<RustValue>;
    }
}

fn new_rust_value() -> Box<RustValue> {
    Box::new(RustValue {
        value: "A Hidden Rust String".into(),
    })
}

#[derive(Default, Debug, Clone)]
pub struct RustValue {
    value: String,
}

impl RustValue {
    pub fn read(&self) -> &String {
        &self.value
    }
    pub fn new(val: &str) -> Self {
        RustValue {
            value: val.to_string(),
        }
    }
}

Now, simply declare your enum, optional, or expected

//! my_crate/src/enum.rs

use data::{RustValue, ffi::SharedData};

// enums -> variants are declared as is
// all variant types are supported
// (Unnamed struct, named structs, unit, single type wrappers)
#[derive(Debug)]
#[cxx_enumext::extern_type]
pub enum RustEnum<'a> {
    Empty,
    Num(i64),
    String(String),
    Bool(bool),
    Shared(SharedData),
    SharedRef(&'a SharedData),
    Opaque(Box<RustValue>),
    OpaqueRef(&'a RustValue),
    Tuple(i32, i32),
    Struct { val: i32, str: String },
    Unit1,
    Unit2,
}

// Declare type aliases for an Optional, "alias" can be for `Optional<T>` or
// `cxx_enumext::Optional<T>`, neither type truely exists but the name will be
// used to determine the enum variants to generate

// `cxx_name` and `namespace` are supported and function identically to `cxx`
#[cxx_enumext::extern_type(cxx_name = "OptionalInt32")]
#[derive(Debug)]
pub type OptionalI32 = Optional<i32>;


// Declare type aliases for an Expected, "alias" can be for `Expected<T,E>` or
// `cxx_enumext::Expected<T,E>`, neither type truely exists but the name will be
// used to determine the enum variants to generate
#[cxx_enumext::extern_type]
#[derive(Debug)]
pub type I32StringResult = cxx_enumext::Expected<i32, String>;


#[cxx::bridge]
pub mod ffi {

    unsafe extern "C++" {
        include!("my_crate/src/enum.h");

        type RustEnum<'a> = super::RustEnum<'a>;
        type I32StringResult = super::I32StringResult;
        type OptionalInt32 = super::OptionalI32;
    }
}

This will generate the cxx::ExternType impl as well as some non-exhaustive static assertions to check that contained types are at least externable.

for Optional and Expected types some std::convert::From<T> impls will be cenerated to convert from and to Option and Result types

Now, in your C++ file, make sure to #include the right headers:

#include "rust/cxx.h"
#include "rust/cxx_enumext.h"
#include "rust/cxx_enumext_macros.h"

And add a call to the CXX_DEFINE_VARIANT, CXXASYNC_DEFINE_OPTIONAL, and or CXXASYNC_DEFINE_EXPECTED macro as apropreate to define the C++ side of the types:

// my_crate/src/enum.h

#include "rust/cxx.h"
#include "rust/cxx_enumext.h"
#include "rust/cxx_enumext_macros.h"

#include "my_crate/src/data.rs.h"

// The first argument is the name of the C++ type, and the second argument is a parentheses
// enclosed list of variant directives to devine the inner variants.
// They must be declared in the same order as the rust enum and specify the appropriate C++ type.
// An optional third (actualy variadic) argument is placed verbatim in the resulting struct body.
//
// This macro must be invoked in the correct namespace to define the type.
CXX_DEFINE_VARIANT(
    RustEnum,
    (UNIT(Empty) /*(Alias name)*/, TYPE(Num, int64_t) /*(Alias name, type)*/,
     TYPE(String, rust::string) /*by value rust type*/,
     TYPE(Bool, bool) /*primative type*/,
     TYPE(Shared, SharedData) /*shared type*/,
     TYPE(
         SharedRef,
         std::reference_wrapper<SharedData>) /* if your using a refrence wrap is
                                                in a `std::reference_wrapper`*/
     ,
     TYPE(Opaque, rust::box<RustValue>) /* boxed opaque rust type*/,
     TYPE(OpaqueRef,
          std::reference_wrapper<RustValue>) /*refren to opaque rust type*/,
     TUPLE(Tuple, int32_t, int32_t) /*(Alias name, [list, of, types]) => struct
                                       aliast_t{ T1 _0; T2 _1; ...};*/
     ,
     STRUCT(Struct, int32_t val;
            rust::string str;) /*(Alias name, body of struct for members) struct
                                  alist_t{body}; */
     ,
     UNIT(Unit1) /*More the one unit type supported, each is just a `struct
                    alias_t {};`*/
     ,
     UNIT(Unit2) /*NO TRAILING COMMA*/
     ),
    /* optional section injected into the type body (for declareing member
       function etc.) */
    /* DO NOT DECLARE EXTRA MEMBER VARIABLES, THIS WILL MAKE THE TYPE HAVE */
    /* AN INCORRECT MEMORY LAYOUT */
)


// The first argument is the name of the C++ type, and the second contained type
// An optional third (actualy variadic) argument is placed verbatim in the resulting struct body.
CXX_DEFINE_OPTIONAL(OptionalInt32, int32_t)

// The first argument is the name of the C++ type, and the second and third
// are the expected and unexpected types respectively.
// An optional forth (actualy variadic) argument is placed verbatim in the resulting struct body.
CXX_DEFINE_EXPECTED(I32StringResult, int32_t, rust::string)

You’re all set! Now you can use the enum types on either side of your ffi.

§Installation notes

You will need a C++ compiler that implements c++17

§Refrence

§rust::enm::variant

Simplicited declaration


namespace rust {
namespace enm {

/// @brief The getf method which returns a pointer to T if the variant
// contains T, otherwise throws `bad_rust_variant_access`
template <std::size_t I, typename... Ts>
constexpr decltype(auto) get(variant_base<Ts...> &);

template <std::size_t I, typename... Ts>
constexpr decltype(auto) get(const variant_base<Ts...> &);

/// @brief The get_if method which returns a pointer to T if the variant
/// contains T other wise `nullptr`.
template <std::size_t I, typename... Ts>
constexpr std::add_pointer_t<variant_alternative_t<I, Ts...>>
get_if(variant_base<Ts...> *variant);

template <std::size_t I, typename... Ts>
constexpr const std::add_pointer_t<variant_alternative_t<I, Ts...>>
get_if(const variant_base<Ts...> *variant);

/// @brief The visit method which will pick the right type depending on the
/// `index` value.
template <typename Visitor, typename... Ts>
constexpr decltype(auto) visit(Visitor &&visitor, variant_base<Ts...> &);

template <typename Visitor, typename... Ts>
constexpr decltype(auto) visit(Visitor &&visitor, const variant_base<Ts...> &);


/// @brief The holds_alternative method which will return true if the
/// variant contains the type at index I
template <std::size_t I, typename... Ts>
constexpr bool holds_alternative(const variant_base<Ts...> &variant);

/// @brief The holds_alternative method which will return true if the
/// variant contains T
template <typename T, typename... Ts,
          typename = std::enable_if_t<
              exactly_once<std::is_same_v<Ts, std::decay_t<T>>...>::value>>
constexpr bool holds_alternative(const variant_base<Ts...> &variant);

template <typename... Ts> struct variant {
  /// @brief Converting constructor. Corresponds to (4) constructor of
  /// std::variant.
  template <typename T, typename D = std::decay_t<T>,
            typename = std::enable_if_t<is_unique_v<T> &&
                                        std::is_constructible_v<D, T>>>
  variant(T &&other) noexcept(std::is_nothrow_constructible_v<D, T>);

  /// @brief Participates in the resolution only if we can construct T from
  /// Args and if T is unique in Ts. Corresponds to (5) constructor of
  /// std::variant.
  template <typename T, typename... Args,
            typename = std::enable_if_t<is_unique_v<T>>,
            typename = std::enable_if_t<std::is_constructible_v<T, Args...>>>
  explicit variant(std::in_place_type_t<T> type, Args &&...args) noexcept(
      std::is_nothrow_constructible_v<T, Args...>);

  /// @brief Participates in the resolution only if the index is within range
  /// and if the type can be constructor from Args. Corresponds to (7) of
  /// std::variant.
  template <std::size_t I, typename... Args, typename T = type_from_index_t<I>,
            typename = std::enable_if_t<std::is_constructible_v<T, Args...>>>
  explicit variant(
      [[maybe_unused]] std::in_place_index_t<I> index,
      Args &&...args) noexcept(std::is_nothrow_constructible_v<T, Args...>);

  /// @brief Converts the std::variant to our variant. Participates only in
  /// the resolution if all types in Ts are copy constructable.
  template <typename... Rs, typename = std::enable_if_t<
                                all_same_v<Rs...> && all_copy_constructible_v>>
  variant(const std::variant<Rs...> &other);

  /// @brief Converts the std::variant to our variant. Participates only in
  /// the resolution if all types in Ts are move constructable.
  template <typename... Rs, typename = std::enable_if_t<
                                all_same_v<Rs...> && all_move_constructible_v>>
  variant(std::variant<Rs...> &&other);

  /// @brief Copy assignment. Statically fails if not every type in Ts is copy
  /// constructable. Corresponds to (1) assignment of std::variant.
  variant_base &operator=(const variant_base &other);

  /// @brief Deleted move assignment. Same as for the move constructor.
  /// Would correspond to (2) assignment of std::variant.
  variant_base &operator=(variant_base &&other) = delete;

  /// @brief Converting assignment. Corresponds to (3) assignment of
  /// std::variant.
  template <typename T, typename = std::enable_if_t<
                            is_unique_v<T> && std::is_constructible_v<T &&, T>>>
  variant_base &operator=(T &&other);

  /// @brief Converting assignment from std::variant. Participates only in the
  /// resolution if all types in Ts are copy constructable.
  template <typename... Rs, typename = std::enable_if_t<
                                all_same_v<Rs...> && all_copy_constructible_v>>
  variant_base &operator=(const std::variant<Rs...> &other);

  /// @brief Converting assignment from std::variant. Participates only in the
  /// resolution if all types in Ts are move constructable.
  template <typename... Rs, typename = std::enable_if_t<
                                all_same_v<Rs...> && all_move_constructible_v>>
  variant_base &operator=(std::variant<Rs...> &&other);

  /// @brief Emplace function. Participates in the resolution only if T is
  /// unique in Ts and if T can be constructed from Args. Offers strong
  /// exception guarantee. Corresponds to the (1) emplace function of
  /// std::variant.
  template <typename T, typename... Args,
            typename = std::enable_if_t<is_unique_v<T>>,
            typename = std::enable_if_t<std::is_constructible_v<T, Args...>>>
  T &emplace(Args &&...args);

  /// @brief Emplace function. Participates in the resolution only if T can be
  /// constructed from Args. Offers strong exception guarantee. Corresponds to
  /// the (2) emplace function of std::variant.
  ///
  /// The std::variant can have no valid state if the type throws during the
  /// construction. This is represented by the `valueless_by_exception` flag.
  /// The same approach is also used in absl::variant [2].
  /// In our case we can't accept valueless enums since we can't represent
  /// this in Rust. We must therefore provide a strong exception guarantee for
  /// all operations using `emplace`. Two famous implementations of never
  /// valueless variants are Boost/variant [3] and Boost/variant2 [4].
  /// Boost/variant2 uses two memory buffers - which would be not compatible
  /// with Rust Enum's memory layout. The Boost/variant backs up the old
  /// object and calls its d'tor before constructing the new object; It then
  /// copies the old data back to the buffer if the construction fails - which
  /// might contain garbage (since) the d'tor was already called.
  ///
  ///
  /// We take a similar approach to Boost/variant. Assuming that constructing
  /// or moving the new type can throw, we backup the old data, try to
  /// construct the new object in the final buffer, swap the buffers, such
  /// that the old object is back in its original place, destroy it and move
  /// the new object from the old buffer back to the final place.
  ///
  /// Sources
  ///
  /// [1]
  /// https://en.cppreference.com/w/cpp/utility/variant/valueless_by_exception
  /// [2]
  /// https://github.com/abseil/abseil-cpp/blob/master/absl/types/variant.h
  /// [3]
  /// https://www.boost.org/doc/libs/1_84_0/libs/variant2/doc/html/variant2.html
  /// [4]
  /// https://www.boost.org/doc/libs/1_84_0/doc/html/variant/design.html#variant.design.never-empty
  template <std::size_t I, typename... Args, typename T = type_from_index_t<I>,
            typename = std::enable_if_t<std::is_constructible_v<T, Args...>>>
  T &emplace(Args &&...args);

  constexpr std::size_t index() const noexcept;

  void swap(variant_base &other);


  struct bad_rust_variant_access : std::runtime_error {
    bad_rust_variant_access(std::size_t index)
        : std::runtime_error{"The index should be " + std::to_string(index)} {}
  };
}

} // namespace enm
} // namespace rust

§rust::enm::Optional

Simplicited declaration


namespace rust {
namespace enm {

struct bad_rust_optional_access : std::runtime_error {
  bad_rust_optional_access() : std::runtime_error{"Optional has no value"} {}
};

template <typename T> struct optional : public variant<monostate, T> {
  using base = variant<monostate, T>;

  optional() : base(monostate{}) {};
  optional(std::nullopt_t) : base(monostate{}) {};
  optional(const optional &) = default;
  optional(optional &&) = delete;

  using base::base;
  using base::operator=;

  using Some = T;
  using None = monostate;

  constexpr explicit operator bool() { return this->m_Index == 1; }
  constexpr bool has_value() const noexcept { return this->m_Index == 1; }
  constexpr bool is_some() const noexcept { return this->m_Index == 1; }
  constexpr bool is_none() const noexcept { return this->m_Index == 0; }

  /// @brief returns the contined value
  ///
  /// if `*this` contains a value returns a refrence. Otherwise throws
  /// bad_rust_optional_access
  /// @throws bad_rust_optional_access
  constexpr T &value() &;

  /// @brief returns the contined value
  ///
  /// if `*this` contains a value returns a refrence. Otherwise throws
  /// bad_rust_optional_access
  /// @throws bad_rust_optional_access
  constexpr const T &value() const &;

  /// @brief resets the optional to an empty state
  ///
  /// if `has_value()` is true first calls the deconstructor
  constexpr void reset() noexcept;

  constexpr const T *operator->() const noexcept;
  constexpr T *operator->() noexcept;

  constexpr const T &operator*() const & noexcept;
  constexpr T &operator*() & noexcept;

  template <class U = std::remove_cv_t<T>>
  constexpr T value_or(U &&default_value) const &;

  template <class F> constexpr auto and_then(F &&f) &;
  template <class F> constexpr auto and_then(F &&f) const &;
  template <class F> constexpr auto transform(F &&f) &;

  template <class F> constexpr auto transform(F &&f) const &;

  template <class F> constexpr T &or_else(F &&f) &;
  template <class F> constexpr const T &or_else(F &&f) const &;
};


/// Compares two optional objects
template <class T, class U>
inline constexpr bool operator==(const optional<T> &lhs,
                                 const optional<U> &rhs);
template <class T, class U>
inline constexpr bool operator!=(const optional<T> &lhs,
                                 const optional<U> &rhs);
template <class T, class U>
inline constexpr bool operator<(const optional<T> &lhs,
                                const optional<U> &rhs)
// ... and many more compare operators

§rust::enm::expected

Simplicited declaration


namespace rust {
namespace enm {

template <typename E> struct bad_rust_expected_access : std::runtime_error {
  bad_rust_expected_access(const E &err)
      : std::runtime_error{"Expected is the unexpected value"}, error(err) {}
  E error;
};

template <typename T, typename E> struct expected : public variant<T, E> {
  using base = variant<T, E>;

  expected() = delete;
  expected(const expected &) = default;
  expected(expected &&) = delete;

  using base::base;
  using base::operator=;

  using Ok = T;
  using Err = E;

  constexpr explicit operator bool() { return this->m_Index == 0; }
  constexpr bool has_value() const noexcept { return this->m_Index == 0; }

  /// @brief returns the expected value
  ///
  /// @throws bad_rust_expected_access<E> with a copy of the unexpected value
  constexpr T &value() &;

  /// @brief returns the expected value
  ///
  /// @throws bad_rust_expected_access<E> with a copy of the unexpected value
  constexpr const T &value() const &;

  /// @brief returns the unexpected value
  ///
  /// if has_value() is `true`, the behavior is undefined
  constexpr E &error() &;

  /// @brief returns the unexpected value
  ///
  /// if has_value() is `true`, the behavior is undefined
  constexpr const E &error() const &;

  constexpr const T *operator->() const noexcept;
  constexpr T *operator->() noexcept;

  constexpr const T &operator*() const & noexcept;
  constexpr T &operator*() & noexcept;

  /// @brief Returns the expected value if it exists, otherwise returns
  /// `default_value`
  template <class U = std::remove_cv_t<T>>
  constexpr T value_or(U &&default_value) const &;

  /// @brief Returns the unexpected value if it exists, otherwise returns
  /// `default_value`
  template <class G = E> constexpr E error_or(G &&default_value) const &;

  template <class F> constexpr auto and_then(F &&f) &;
  template <class F> constexpr auto and_then(F &&f) const &;

  template <class F> constexpr auto transform(F &&f) &;
  template <class F> constexpr auto transform(F &&f) const &;

  template <class F> constexpr T &or_else(F &&f) &;
  template <class F> constexpr const T &or_else(F &&f) const &;

  template <class F> constexpr auto transform_error(F &&f) &;
  template <class F> constexpr auto transform_error(F &&f) const &;

};

} // namespace enm
} // namespace rust

Modules§

private
Private assert helpers

Attribute Macros§

extern_type