-
Notifications
You must be signed in to change notification settings - Fork 1
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
Conversation
|
||
template <typename T> | ||
Option<T>::~Option() noexcept { | ||
this->drop(); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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);
}
There was a problem hiding this comment.
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
src/rust_option.rs
Outdated
#[cfg(feature = "alloc")] | ||
impl<T: Sized> OptionType for Box<T> {} | ||
|
||
impl<T> RustOption<T> { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
src/rust_option.rs
Outdated
|
||
// ABI compatible with C++ rust::Option<T> (not necessarily core::option::Option<T>). | ||
#[repr(C)] | ||
pub struct RustOption<T: OptionType> { |
There was a problem hiding this comment.
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
There was a problem hiding this 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/ffi/tests.h
Outdated
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(); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rust::OptionRef<T>
seems reasonable as well. There's prior art for that in other codebases:
- Chromium https://chromium.googlesource.com/chromium/src/base/+/refs/heads/main/types/optional_ref.h
- TypeSafe https://github.com/foonathan/type_safe/blob/main/include/type_safe/optional_ref.hpp
Some more discussion here https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p1683r0.html
syntax/parse.rs
Outdated
} 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()); | ||
} |
There was a problem hiding this comment.
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.
} 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, | |
}))); | |
} |
src/rust_option.rs
Outdated
mod private { | ||
pub trait Sealed {} | ||
} | ||
pub trait OptionType: private::Sealed {} |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
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_; |
There was a problem hiding this comment.
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
There are 2 commits now, the first explicitly does not support Option<&Vec> and Option<&String> and the second adds support for those |
src/rust_option.rs
Outdated
|
||
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) }) |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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>) { |
There was a problem hiding this comment.
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)
There was a problem hiding this 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.
Option(Option&&) noexcept; | ||
Option(T&&) noexcept; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
template <> \ | ||
void Option<CXX_TYPE *>::set(CXX_TYPE *&& value) noexcept { \ | ||
return cxxbridge1$rust_option$##RUST_TYPE##$set(this, std::move(value)); \ | ||
} \ |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
src/rust_option.rs
Outdated
} | ||
} | ||
|
||
/// SAFETY: Pointer must have been constructed as a Box |
There was a problem hiding this comment.
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) } |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 can you add a note for the |
Is this being merged into the main repository as well anytime soon? |
Transferring over from dtolnay#897