-
Notifications
You must be signed in to change notification settings - Fork 1
[workerd-cxx] Add kj::Maybe type to kj-rs #42
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,273 @@ | ||
use repr::Maybe; | ||
use std::mem::MaybeUninit; | ||
|
||
/// # Safety | ||
/// This trait should only be implemented in `workerd-cxx` on types | ||
/// which contain a specialization of `kj::Maybe` that needs to be represented in | ||
/// Rust. | ||
unsafe trait HasNiche: Sized { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's add a bit more documentation here: describe niche opt with a sentence or two. |
||
fn is_niche(value: &MaybeUninit<Self>) -> bool; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if input parameter type is correct? Can it be ever uninitialized memory? What does the method implementor supposed to do? Maybe this should be a raw pointer to Self or something like that? |
||
} | ||
|
||
// In Rust, references are not allowed to be null, so a null `MaybeUninit<&T>` is a niche | ||
unsafe impl<T> HasNiche for &T { | ||
fn is_niche(value: &MaybeUninit<&T>) -> bool { | ||
unsafe { | ||
// This is to avoid potentially zero-initalizing a reference, which is always undefined behavior | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this makes it look like all this niche thing is because of Rust. Instead these comments should say something like: |
||
std::mem::transmute_copy::<MaybeUninit<&T>, MaybeUninit<*const T>>(value) | ||
.assume_init() | ||
.is_null() | ||
} | ||
} | ||
} | ||
|
||
unsafe impl<T> HasNiche for &mut T { | ||
fn is_niche(value: &MaybeUninit<&mut T>) -> bool { | ||
unsafe { | ||
// This is to avoid potentially zero-initalizing a reference, which is always undefined behavior | ||
std::mem::transmute_copy::<MaybeUninit<&mut T>, MaybeUninit<*mut T>>(value) | ||
.assume_init() | ||
.is_null() | ||
} | ||
} | ||
} | ||
|
||
// In `kj`, `kj::Own<T>` are considered `none` in a `Maybe` if the pointer is null | ||
unsafe impl<T> HasNiche for crate::repr::Own<T> { | ||
fn is_niche(value: &MaybeUninit<crate::repr::Own<T>>) -> bool { | ||
unsafe { value.assume_init_ref().as_ptr().is_null() } | ||
} | ||
} | ||
|
||
/// Trait that is used as the bounds for what can be in a Maybe | ||
/// | ||
/// # Safety | ||
/// This trait should only be implemented from macro expansion and should | ||
/// never be manually implemented. | ||
pub unsafe trait MaybeItem: Sized { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's discuss this on a call? I'd still like us to avoid value traits. |
||
type Discriminant: Copy; | ||
const NONE: Maybe<Self>; | ||
fn is_some(value: &Maybe<Self>) -> bool; | ||
fn is_none(value: &Maybe<Self>) -> bool; | ||
fn some(value: Self) -> Maybe<Self>; | ||
fn from_option(value: Option<Self>) -> Maybe<Self> { | ||
match value { | ||
None => Self::NONE, | ||
Some(val) => Self::some(val), | ||
} | ||
} | ||
fn drop_in_place(value: &mut Maybe<Self>) { | ||
if <Self as MaybeItem>::is_some(value) { | ||
unsafe { | ||
value.some.assume_init_drop(); | ||
} | ||
} | ||
} | ||
} | ||
|
||
/// Macro to implement [`MaybeItem`] for `T` which implment [`HasNiche`] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can't you just say something like?
|
||
macro_rules! impl_maybe_item_for_has_niche { | ||
($ty:ty) => { | ||
unsafe impl<T> MaybeItem for $ty { | ||
type Discriminant = (); | ||
|
||
fn is_some(value: &Maybe<Self>) -> bool { | ||
!<$ty as HasNiche>::is_niche(&value.some) | ||
} | ||
|
||
fn is_none(value: &Maybe<Self>) -> bool { | ||
<$ty as HasNiche>::is_niche(&value.some) | ||
} | ||
|
||
const NONE: Maybe<Self> = { | ||
Maybe { | ||
is_set: (), | ||
some: MaybeUninit::zeroed(), | ||
} | ||
}; | ||
|
||
fn some(value: Self) -> Maybe<Self> { | ||
Maybe { | ||
is_set: (), | ||
some: MaybeUninit::new(value) | ||
} | ||
} | ||
} | ||
}; | ||
($ty:ty, $($tail:ty),+) => { | ||
impl_maybe_item_for_has_niche!($ty); | ||
impl_maybe_item_for_has_niche!($($tail),*); | ||
}; | ||
} | ||
|
||
/// Macro to implement [`MaybeItem`] for primitives | ||
macro_rules! impl_maybe_item_for_primitive { | ||
($ty:ty) => { | ||
unsafe impl MaybeItem for $ty { | ||
type Discriminant = bool; | ||
|
||
fn is_some(value: &Maybe<Self>) -> bool { | ||
value.is_set | ||
} | ||
|
||
fn is_none(value: &Maybe<Self>) -> bool { | ||
!value.is_set | ||
} | ||
|
||
const NONE: Maybe<Self> = { | ||
Maybe { | ||
is_set: false, | ||
some: MaybeUninit::uninit(), | ||
} | ||
}; | ||
|
||
fn some(value: Self) -> Maybe<Self> { | ||
Maybe { | ||
is_set: true, | ||
some: MaybeUninit::new(value) | ||
} | ||
} | ||
} | ||
}; | ||
($ty:ty, $($tail:ty),+) => { | ||
impl_maybe_item_for_primitive!($ty); | ||
impl_maybe_item_for_primitive!($($tail),*); | ||
}; | ||
} | ||
|
||
impl_maybe_item_for_has_niche!(crate::Own<T>, &T, &mut T); | ||
impl_maybe_item_for_primitive!( | ||
u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, f32, f64, bool | ||
harrishancock marked this conversation as resolved.
Show resolved
Hide resolved
|
||
); | ||
|
||
pub(crate) mod repr { | ||
use super::MaybeItem; | ||
use static_assertions::assert_eq_size; | ||
use std::fmt::{Debug, Display}; | ||
use std::mem::MaybeUninit; | ||
|
||
/// A [`Maybe`] represents bindings to the `kj::Maybe` class. | ||
/// It is an optional type, but represented using a struct, for alignment with kj. | ||
/// | ||
/// # Layout | ||
/// In kj, `Maybe` has 3 specializations, one without niche value optimization, and | ||
/// two with it. In order to maintain an identical layout in Rust, we include an associated type | ||
/// in the [`MaybeItem`] trait, which determines the discriminant of the `Maybe<T: MaybeItem>`. | ||
/// | ||
/// ## Niche Value Optimization | ||
/// This discriminant is used in tandem with the [`crate::maybe::HasNiche`] to implement | ||
/// [`MaybeItem`] properly for values which have a niche, which use a discriminant of [`()`], | ||
/// the unit type. All other types use [`bool`]. | ||
#[repr(C)] | ||
pub struct Maybe<T: MaybeItem> { | ||
pub(super) is_set: T::Discriminant, | ||
pub(super) some: MaybeUninit<T>, | ||
} | ||
|
||
assert_eq_size!(Maybe<isize>, [usize; 2]); | ||
assert_eq_size!(Maybe<&isize>, usize); | ||
assert_eq_size!(Maybe<crate::Own<isize>>, [usize; 2]); | ||
|
||
impl<T: MaybeItem> Maybe<T> { | ||
/// # Safety | ||
/// This function shouldn't be used except by macro generation. | ||
pub unsafe fn is_set(&self) -> T::Discriminant { | ||
self.is_set | ||
} | ||
|
||
/// # Safety | ||
/// This function shouldn't be used except by macro generation. | ||
pub const unsafe fn from_parts_unchecked( | ||
is_set: T::Discriminant, | ||
some: MaybeUninit<T>, | ||
) -> Maybe<T> { | ||
Maybe { is_set, some } | ||
} | ||
|
||
pub fn is_some(&self) -> bool { | ||
T::is_some(self) | ||
} | ||
|
||
pub fn is_none(&self) -> bool { | ||
T::is_none(self) | ||
} | ||
|
||
// # CONSTRUCTORS | ||
// These emulate Rust's enum api, which offers constructors for each variant. | ||
// This mean matching cases, syntax, and behavior. | ||
// The only place this may be an issue is pattern matching, which will not work, | ||
// but should produce an error. | ||
// | ||
// The following fails to compile: | ||
// ```rust,compile_fail | ||
// match maybe { | ||
// Maybe::Some(_) => ..., | ||
// Maybe::None => ..., | ||
// } | ||
// ``` | ||
|
||
/// The [`Maybe::Some`] function serves the same purpose as an enum constructor. | ||
/// | ||
/// Constructing a `Maybe<T>::Some(val)` should only be possible with a valid | ||
/// instance of `T` from Rust. | ||
#[allow(non_snake_case)] | ||
pub fn Some(value: T) -> Maybe<T> { | ||
T::some(value) | ||
} | ||
|
||
/// The [`Maybe::None`] functions as a constructor for the none variant. It uses | ||
/// a `const` instead of a function to match syntax with normal Rust enums. | ||
/// | ||
/// Constructing a `Maybe<T>::None` variant should always be possible from Rust. | ||
#[allow(non_upper_case_globals, dead_code)] | ||
pub const None: Maybe<T> = T::NONE; | ||
} | ||
|
||
impl<T: MaybeItem> From<Maybe<T>> for Option<T> { | ||
fn from(value: Maybe<T>) -> Self { | ||
if value.is_some() { | ||
// We can't move out of value so we copy it and forget it in | ||
// order to perform a "manual" move out of value | ||
harrishancock marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let ret = unsafe { Some(value.some.assume_init_read()) }; | ||
std::mem::forget(value); | ||
ret | ||
} else { | ||
None | ||
} | ||
} | ||
} | ||
|
||
impl<T: MaybeItem> From<Option<T>> for Maybe<T> { | ||
fn from(value: Option<T>) -> Self { | ||
<T as MaybeItem>::from_option(value) | ||
} | ||
} | ||
|
||
impl<T: MaybeItem + Debug> Debug for Maybe<T> { | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
if self.is_none() { | ||
write!(f, "Maybe::None") | ||
} else { | ||
write!(f, "Maybe::Some({:?})", unsafe { | ||
self.some.assume_init_ref() | ||
}) | ||
} | ||
} | ||
} | ||
|
||
impl<T: MaybeItem + Display> Display for Maybe<T> { | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
if self.is_none() { | ||
write!(f, "Maybe::None") | ||
} else { | ||
write!(f, "Maybe::Some({})", unsafe { self.some.assume_init_ref() }) | ||
} | ||
} | ||
} | ||
|
||
impl<T: MaybeItem> Drop for Maybe<T> { | ||
fn drop(&mut self) { | ||
T::drop_in_place(self); | ||
} | ||
} | ||
} |
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 would be a great place to put a comment explaining what's missing/needs to be done.
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, to be true to cxx-rs' original philosophy, any functions handling these values would need to be marked unsafe: https://cxx.rs/binding/rawptr.html
But, I suggested adding this static assertion because I suspect there's no use case. That is, we don't appear to have any
Maybe<T*>
in our codebases at the moment, and if I saw one, I would immediately ask, "why not use aMaybe<T&>
or aMaybe<Maybe<T&>>
instead?". Maybe the static assertion should suggest using one of those.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.
The reported error message from Rust does suggest using a
Maybe<&T>
, the static assertion just checks on the C++ side.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.
If we want to allow unsafe use, we need to remove the assertion and Rust check though, and just report an error when a user tries to use a
Maybe<*(const|mut)T>
without unsafe, and suggest usingMaybe<&T>
instead