Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bindings for Option and std::optional #1

Merged
merged 3 commits into from
Feb 28, 2024
Merged

Bindings for Option and std::optional #1

merged 3 commits into from
Feb 28, 2024

Conversation

kuecks
Copy link

@kuecks kuecks commented Oct 12, 2023

Transferring over from dtolnay#897

@kuecks kuecks changed the title mvp Bindings for Option and std::optional Oct 13, 2023

template <typename T>
Option<T>::~Option() noexcept {
this->drop();
Copy link
Author

Choose a reason for hiding this comment

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

I'm not sure if I could get away with:

if (inner.empty != 0) {
    inner.value.~T()
}

(or something like that at least) or really what the implications of that are

Choose a reason for hiding this comment

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

This bit is tricky. For Option<T>::Option(Option&& other), you're missing some calls to destructors (or assuming a previously constructed object. This works if the type T is trivially constructible, but is potentially UB if we ever relax that constraint.

inner.value = std::move(other.inner.value);

This assumes that value was previously constructed, which is not the case as value is a member of a union. Use new (&inner.value) T(std::move(other.inner.value)) instead.

other.inner.empty = 0;

This does not call the destructor of other.inner.value, which is potentially UB. Use reset/drop here.

If you introduce a construct private helper, you'll be able to use that across multiple places (e.g. if you plan to expose an assign member function. You might find folly::Optional as good inspiration here.

template <typename T>
Option<T>::Option(Option&& other) noexcept {
  if (other.has_value()) {
    construct(std::move(other.inner.value));
    other.reset();
  }
}

template <typename T>
Option<T>::~Option() noexcept {
  this->reset();
}

template <typename T>
Option<T>::reset() noexcept {
  this->drop();
}

template <typename... Args>
Option<T>::construct(Args&&... args) noexcept {
    new (&inner.value) T(std::forward<Args>(args)...);
    assert(innner.empty != 0);
}

Copy link
Author

Choose a reason for hiding this comment

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

Option<T>::Option(Option&& other) is a constructor so this doesn't exist yet, but you are correct for (currently missing) operator=(Option&&). Thanks for the placement new, i definitely dont understand that but other bindings are doing something similar

#[cfg(feature = "alloc")]
impl<T: Sized> OptionType for Box<T> {}

impl<T> RustOption<T> {
Copy link
Author

Choose a reason for hiding this comment

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

Oops all of these are supposed to have T: OptionType

--> src/rust_option.rs
|
| assert!(core::mem::size_of::<Option<U>>() == core::mem::size_of::<usize>());
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the evaluated program panicked at 'assertion failed: core::mem::size_of::<Option<U>>() == core::mem::size_of::<usize>()', $DIR/src/rust_option.rs:38:17
Copy link
Author

Choose a reason for hiding this comment

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

This one is annoying because the error message depends on the line of code in rust_option.rs. Ideally we could ignore the line number? Not sure if that is possible


// ABI compatible with C++ rust::Option<T> (not necessarily core::option::Option<T>).
#[repr(C)]
pub struct RustOption<T: OptionType> {
Copy link
Author

Choose a reason for hiding this comment

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

we need the bound on the struct itself (rather than just the impls) because we need it for the Drop impl

Copy link

@capickett capickett left a comment

Choose a reason for hiding this comment

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

Looks great - the change request is mostly for UB in the C++ bindings, otherwise I have non-blocking questions.

Additionally, how should we handle documentation? I noticed that there's no doc page for Option in this PR. Should we maintain a page, similar to the rest of the bindings? https://cxx.rs/bindings.html

tests/ui/option_string.stderr Show resolved Hide resolved
Comment on lines 120 to 127
rust::Option<const Shared*> c_return_rust_ref_option_shared();
rust::Option<Shared*> c_return_rust_mut_option_shared();
rust::Option<Shared*> c_return_rust_pin_mut_option_shared();
rust::Option<const C*> c_return_rust_ref_option_opaque();
rust::Option<C*> c_return_rust_pin_mut_option_opaque();
rust::Option<const uint8_t*> c_return_rust_ref_option_native();
rust::Option<uint8_t*> c_return_rust_mut_option_native();
rust::Option<uint8_t*> c_return_rust_pin_mut_option_native();

Choose a reason for hiding this comment

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

I know that std::optional does not support optional<T&>. However, since we are providing our own type, we can support rust::Option<const Shared&> etc, which would provide a nice ergonomics benefit

Copy link
Author

Choose a reason for hiding this comment

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

I think that didn't work but I can try again

Copy link

@capickett capickett Oct 19, 2023

Choose a reason for hiding this comment

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

@kuecks to do this, you'll need a specialization for rust::Option<T&> that uses a raw pointer as the field in storage, but otherwise exposes T& APIs. I'm also okay if you'd like to punt on this feedback for later / separate PR / never.

Copy link
Author

Choose a reason for hiding this comment

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

Hrmm rather than that, maybe it would make more sense to introduce a new type, rust::OptionRef<T>. Potentially rust::Option<T> would also be renamed to rust::OptionBox<T> as well, or maybe I can restrict rust::Option<T> to only exist when T == Box

Choose a reason for hiding this comment

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

syntax/parse.rs Outdated
Comment on lines 1285 to 1294
} else if ident == "Option" {
if generic.args.len() == 1 {
if let GenericArgument::Type(arg) = &generic.args[0] {
let inner = parse_type(arg)?;
return Ok(Type::RustOption(Box::new(Ty1 {
name: ident,
langle: generic.lt_token,
inner,
rangle: generic.gt_token,
})));
}
} else {
panic!("{}", generic.args.len());
}

Choose a reason for hiding this comment

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

Panic here seems inappropriate, and inconsistent with the patten employed by the rest of the types.

Suggested change
} else if ident == "Option" {
if generic.args.len() == 1 {
if let GenericArgument::Type(arg) = &generic.args[0] {
let inner = parse_type(arg)?;
return Ok(Type::RustOption(Box::new(Ty1 {
name: ident,
langle: generic.lt_token,
inner,
rangle: generic.gt_token,
})));
}
} else {
panic!("{}", generic.args.len());
}
} else if ident == "Option" && generic.args.len() == 1 {
if let GenericArgument::Type(arg) = &generic.args[0] {
let inner = parse_type(arg)?;
return Ok(Type::RustOption(Box::new(Ty1 {
name: ident,
langle: generic.lt_token,
inner,
rangle: generic.gt_token,
})));
}

mod private {
pub trait Sealed {}
}
pub trait OptionType: private::Sealed {}

Choose a reason for hiding this comment

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

nit-pick on the naming: Calling this OptionTarget would keep it consistent with UniquePtrTarget and SharedPtrTarget.

include/cxx.h Outdated
Option(Option&&) noexcept;
Option(T&&) noexcept;
~Option() noexcept;
void drop() noexcept;

Choose a reason for hiding this comment

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

Looking at other types, like rust::String and rust::Vec, we usually expose "C++-like" APIs. I would call this reset() instead, to align with https://en.cppreference.com/w/cpp/utility/optional


template <typename T>
Option<T>::~Option() noexcept {
this->drop();

Choose a reason for hiding this comment

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

This bit is tricky. For Option<T>::Option(Option&& other), you're missing some calls to destructors (or assuming a previously constructed object. This works if the type T is trivially constructible, but is potentially UB if we ever relax that constraint.

inner.value = std::move(other.inner.value);

This assumes that value was previously constructed, which is not the case as value is a member of a union. Use new (&inner.value) T(std::move(other.inner.value)) instead.

other.inner.empty = 0;

This does not call the destructor of other.inner.value, which is potentially UB. Use reset/drop here.

If you introduce a construct private helper, you'll be able to use that across multiple places (e.g. if you plan to expose an assign member function. You might find folly::Optional as good inspiration here.

template <typename T>
Option<T>::Option(Option&& other) noexcept {
  if (other.has_value()) {
    construct(std::move(other.inner.value));
    other.reset();
  }
}

template <typename T>
Option<T>::~Option() noexcept {
  this->reset();
}

template <typename T>
Option<T>::reset() noexcept {
  this->drop();
}

template <typename... Args>
Option<T>::construct(Args&&... args) noexcept {
    new (&inner.value) T(std::forward<Args>(args)...);
    assert(innner.empty != 0);
}

include/cxx.h Outdated

template <typename T>
const T *Option<T>::operator->() const {
return &inner.value;

Choose a reason for hiding this comment

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

Might be nice to add some debug check for has_value() where you assume value is set.

Copy link
Author

Choose a reason for hiding this comment

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

do you know how to do debug checks portably? I can't tell if I can rely on NDEBUG

Choose a reason for hiding this comment

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

An assert() would be good enough IMO.

@kuecks
Copy link
Author

kuecks commented Oct 20, 2023

Looks great - the change request is mostly for UB in the C++ bindings, otherwise I have non-blocking questions.

Additionally, how should we handle documentation? I noticed that there's no doc page for Option in this PR. Should we maintain a page, similar to the rest of the bindings? https://cxx.rs/bindings.html

Yeah I was waiting for review on the implementation before I cluttered up the pr with documentation, but I will definitely be adding that in the next stage. Would definitely be nice if we can maintain a version of cxx.rs

void reset();
void set(T&&) noexcept;
private:
void* empty_;
Copy link
Author

Choose a reason for hiding this comment

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

In this update, I decided to never access the union directly from C++ and instead always access it in Rust. I then realizes that means I don't need the union at all on the cpp side? But maybe it's more clear if I keep it regardless

@kuecks
Copy link
Author

kuecks commented Jan 17, 2024

There are 2 commits now, the first explicitly does not support Option<&Vec> and Option<&String> and the second adds support for those


pub fn from_ref_option_vec_ref(v: Option<&Vec<T>>) -> Self {
let _: () = assert_option_safe::<&Vec<T>>();
Self::from_option_ref(unsafe { core::mem::transmute::<Option<&Vec<T>>, Option<&RustVec<T>>>(v) })
Copy link
Author

Choose a reason for hiding this comment

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

Changes like these are the scariest to me, but I think they are ok? I was looking at the RustVec implementation for guidance: https://github.com/dtolnay/cxx/blob/master/src/rust_vec.rs#L29


writeln!(out, "template <>");
begin_function_definition(out);
writeln!(out, "void Option<{0}>::set({0}&& value) noexcept {{", inner);
Copy link
Author

Choose a reason for hiding this comment

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

This rvalue reference also seemed sketchy to me, I think it would probably be best to just do everything by value since we know we are just dealing with pointers on the C++ side anyway? Maybe this works though

OptionInner::RustBox(_) => quote! {
#[doc(hidden)]
#[export_name = #link_set]
unsafe extern "C" fn #local_set #impl_generics(this: *mut ::cxx::private::RustOption<#ty>, value: *mut ::core::mem::MaybeUninit<#ty>) {
Copy link
Author

Choose a reason for hiding this comment

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

Here is the actual implementation side of the spooky rvalue reference I mention above, it causes me to take in something line *mut MaybeUninit<&T> which feels weird. Likely I should just be taking in MaybeUninit<&T> (by passing the pointer by value)

Copy link

@capickett capickett left a comment

Choose a reason for hiding this comment

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

Amazing work - my main thought was around how we might support passing Optional<i32> etc over FFI, where the primitive is held inside the option by value. But that is not blocking IMO, and we can start here.

Comment on lines +834 to +835
Option(Option&&) noexcept;
Option(T&&) noexcept;

Choose a reason for hiding this comment

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

Do you want to support copy operations as well? If not, explicitly deleting them can provide a nicer compiler diagnostic:

  // Option is not copyable
  Option(const Option&) = delete;
  Option(Option&&) noexcept;
  Option(T&&) noexcept;

Or if you do, I would add them here:

  Option(const Option&) noexcept;
  Option(Option&&) noexcept;
  Option(const T&) noexcept;
  Option(T&&) noexcept;

T *operator->();
T &operator*();

Option<T>& operator=(Option&&) noexcept;

Choose a reason for hiding this comment

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

Same comment as earlier with copy constructor

Comment on lines +869 to +872
template <> \
void Option<CXX_TYPE *>::set(CXX_TYPE *&& value) noexcept { \
return cxxbridge1$rust_option$##RUST_TYPE##$set(this, std::move(value)); \
} \

Choose a reason for hiding this comment

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

I was a bit surprised to see Option<CXX_TYPE *> here. Do we support rust::Option<int32_t> for instance? Or would it have to instead be rust::Option<int32_t *>? It feels a bit odd to see the latter coming from other c++ codebases

Copy link
Author

Choose a reason for hiding this comment

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

No only pointers because we rely on the niche optimization :( I will try to support Option for builtin types like ints and bools in a follow up but I have no idea if it's possible so no promises

}
}

/// SAFETY: Pointer must have been constructed as a Box

Choose a reason for hiding this comment

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

I'd expand on these safety comments slightly. self must have been constructed as a Option<Box<T>>. Later on, the "Pointer must not be aliased and must have been constructed as an Option<&mut T>"

{
pub fn from_option_ref(other: Option<&'a T>) -> Self {
let _: () = assert_option_safe::<&T>();
unsafe { core::mem::transmute::<Option<&'a T>, RustOption<&'a T>>(other) }

Choose a reason for hiding this comment

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

Earlier on your documented "ABI compatible with C++ rust::Option (not necessarily core::option::Option)." but here and throughout I see transmutations between RustOption and Option. Is the documentation wrong / imprecise?

Copy link
Author

Choose a reason for hiding this comment

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

This documentation is the same as for Vec which does the same kind of transmutes. It was explained to me as the layout is the same for our RustOption and std::option::Option, but std::option::Option doesn't necessarily have the same calling convention as RustOption. That is, it could be passed in via registers instead of on the stack which would break the C++ side and is part of the ABI contract

@kuecks
Copy link
Author

kuecks commented Feb 26, 2024

Screenshot 2024-02-26 at 11 27 57 AM
Screenshot 2024-02-26 at 11 28 03 AM

Added docs to the book, see also the link to the option docs in the nav pane on the left

@capickett
Copy link

@kuecks can you add a note for the &Option<T> vs Option<&T> restriction? Otherwise LGTM!

@kuecks kuecks merged commit fd03e02 into facebookexperimental:master Feb 28, 2024
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants