Skip to content

[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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion gen/src/write.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ fn pick_includes_and_builtins(out: &mut OutFile, apis: &[Api]) {
Type::SliceRef(_) => out.builtin.rust_slice = true,
Type::Array(_) => out.include.array = true,
Type::Ref(_) | Type::Void(_) | Type::Ptr(_) => {}
Type::Future(_) | Type::Own(_) => out.include.kj_rs = true,
Type::Future(_) | Type::Maybe(_) | Type::Own(_) => out.include.kj_rs = true,
}
}
}
Expand Down Expand Up @@ -1263,6 +1263,11 @@ fn write_type(out: &mut OutFile, ty: &Type) {
write_type(out, &ptr.inner);
write!(out, ">");
}
Type::Maybe(ptr) => {
write!(out, "::kj::Maybe<");
write_type(out, &ptr.inner);
write!(out, ">");
}
Type::CxxVector(ty) => {
write!(out, "::std::vector<");
write_type(out, &ty.inner);
Expand Down Expand Up @@ -1357,6 +1362,7 @@ fn write_space_after_type(out: &mut OutFile, ty: &Type) {
| Type::SharedPtr(_)
| Type::WeakPtr(_)
| Type::Str(_)
| Type::Maybe(_)
| Type::CxxVector(_)
| Type::RustVec(_)
| Type::SliceRef(_)
Expand Down Expand Up @@ -1431,6 +1437,7 @@ fn write_generic_instantiations(out: &mut OutFile) {
ImplKey::RustVec(ident) => write_rust_vec_extern(out, ident),
ImplKey::UniquePtr(ident) => write_unique_ptr(out, ident),
ImplKey::Own(ident) => write_kj_own(out, ident),
ImplKey::Maybe(ident) => write_kj_maybe(out, ident),
ImplKey::SharedPtr(ident) => write_shared_ptr(out, ident),
ImplKey::WeakPtr(ident) => write_weak_ptr(out, ident),
ImplKey::CxxVector(ident) => write_cxx_vector(out, ident),
Expand Down Expand Up @@ -1669,6 +1676,17 @@ fn write_kj_own(out: &mut OutFile, key: NamedImplKey) {
);
}

// Writes static assertions for Maybe
fn write_kj_maybe(out: &mut OutFile, key: NamedImplKey) {
let ident = key.rust;
let resolve = out.types.resolve(ident);
let inner = resolve.name.to_fully_qualified();

out.include.utility = true;
out.include.kj_rs = true;
writeln!(out, "static_assert(!::std::is_pointer<{}>::value, \"Maybe<T*> is not supported in workerd-cxx\");", inner);
Copy link
Collaborator

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.

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 a Maybe<T&> or a Maybe<Maybe<T&>> instead?". Maybe the static assertion should suggest using one of those.

Copy link
Author

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.

Copy link
Author

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 using Maybe<&T> instead

}

fn write_unique_ptr(out: &mut OutFile, key: NamedImplKey) {
let ty = UniquePtr::Ident(key.rust);
write_unique_ptr_common(out, ty);
Expand Down
3 changes: 3 additions & 0 deletions kj-rs/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use awaiter::WakerRef;
pub use crate::ffi::KjWaker;
pub use awaiter::PromiseAwaiter;
pub use future::FuturePollStatus;
pub use maybe::repr::Maybe;
pub use own::repr::Own;
pub use promise::KjPromise;
pub use promise::KjPromiseNodeImpl;
Expand All @@ -13,12 +14,14 @@ pub use promise::new_callbacks_promise_future;

mod awaiter;
mod future;
pub mod maybe;
mod own;
mod promise;
mod waker;

pub mod repr {
pub use crate::future::repr::*;
pub use crate::maybe::repr::*;
pub use crate::own::repr::*;
}

Expand Down
273 changes: 273 additions & 0 deletions kj-rs/maybe.rs
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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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;
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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: kj::Maybe<T&> uses a niche to represent kj::none because c++ references can't be null.

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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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`]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can't you just say something like?

impl <T: HasNiche> MaybeItem<T>

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
);

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
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);
}
}
}
Loading