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

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
232 changes: 232 additions & 0 deletions kj-rs/maybe.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
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 {
fn is_niche(value: &MaybeUninit<Self>) -> bool;
}

// 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
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 {
type Discriminant: Copy;
fn is_some(value: &Maybe<Self>) -> bool;
fn is_none(value: &Maybe<Self>) -> bool;
fn from_option(value: Option<Self>) -> Maybe<Self>;
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`]
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)
}

fn from_option(value: Option<Self>) -> Maybe<Self> {
match value {
None => Maybe {
is_set: (),
some: MaybeUninit::zeroed(),
},
Some(val) => Maybe {
is_set: (),
some: MaybeUninit::new(val),
}
}
}
}
};
($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
}

fn from_option(value: Option<Self>) -> Maybe<Self> {
match value {
None => Maybe {
is_set: false,
some: MaybeUninit::zeroed(),
},
Some(val) => Maybe {
is_set: true,
some: MaybeUninit::new(val),
}
}
}
}
};
($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, char, 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 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)
}
}

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

Choose a reason for hiding this comment

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

Is this because value isn't a mutable type? Is that a limitation of the From trait?

Copy link
Author

Choose a reason for hiding this comment

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

No, it's because we're trying to move out of a MaybeUninit, and T isn't necessarily copy. It's a limitation of unions in Rust.

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, "kj::None")
} else {
write!(f, "kj::Some({:?})", unsafe { self.some.assume_init_ref() })

Choose a reason for hiding this comment

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

Nit for discussion: Perhaps these should be lowercased, like kj::none (the value instead of its type kj::None) and kj::some() (the function instead of kj::Some, which doesn't actually exist)?

Copy link
Author

Choose a reason for hiding this comment

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

I tried to have it more accurately represent Rust naming conventions here, with capitalized variants, as if it was a Rust enum. That's ideally the way we want to work with Maybe, but it does raise questions about when to use what conventions, where, and how. Keeping it consistent with C++ is probably worth it, since that's where we're migrating from, but should that change in the future?

}
}
}

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, "kj::None")
} else {
write!(f, "kj::Some({})", unsafe { self.some.assume_init_ref() })
}
}
}

impl<T: MaybeItem> Drop for Maybe<T> {
fn drop(&mut self) {
T::drop_in_place(self);
}
}
}
39 changes: 36 additions & 3 deletions kj-rs/own.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,51 @@
//! The `workerd-cxx` module containing the [`Own<T>`] type, which is bindings to the `kj::Own<T>` C++ type

use static_assertions::{assert_eq_align, assert_eq_size};
use std::{fmt, marker::PhantomData};

assert_eq_size!(repr::Own<()>, [*const (); 2]);
assert_eq_align!(repr::Own<()>, *const ());

/// When we want to use an `Own`, we want the guarantee of being not null only
/// in direct `Own<T>`, not Maybe<Own<T>>, and using a [`NonNull`] in `Own`
/// but allowing Nulls for Niche Value Optimization is undefined behavior.

Choose a reason for hiding this comment

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

This makes me curious, do you know how Option<Box<T>> handles this? I would guess it encounters the same issues.

Copy link
Author

Choose a reason for hiding this comment

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

Option<Box<T>> being an enum means the Rust compiler knows it can optimize, but since Maybe is a struct we have to manually control this behavior. This makes me wonder if it's actually possible to use an enum... I'm going to do some testing on that side of things. I'm still inclined to say no, since using an enum may result in Rust optimizing things when we don't want it to. The current solution maintains absolute control over layout.

#[repr(transparent)]
pub(crate) struct NonNullExceptMaybe<T>(pub(crate) *mut T, PhantomData<T>);

impl<T> NonNullExceptMaybe<T> {
pub fn as_ptr(&self) -> *const T {
self.0.cast()
}

pub unsafe fn as_ref(&self) -> &T {
// Safety:
// This value will only be null when in a [`Maybe<T>`], which does niche value optimization
// for a null pointer, so the inner [`Own<T>`] can never be accessed if it is null
unsafe { self.0.as_ref().unwrap() }
}

pub unsafe fn as_mut(&mut self) -> &mut T {
// Safety:
// This value will only be null when in a [`Maybe<T>`], which does niche value optimization
// for a null pointer, so the inner [`Own<T>`] can never be accessed if it is null
unsafe { self.0.as_mut().unwrap() }
}
}

impl<T> fmt::Pointer for NonNullExceptMaybe<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Pointer::fmt(&self.0, f)
}
}

pub mod repr {
use super::NonNullExceptMaybe;
use std::ffi::c_void;
use std::fmt::{self, Debug, Display};
use std::hash::{Hash, Hasher};
use std::ops::Deref;
use std::ops::DerefMut;
use std::pin::Pin;
use std::ptr::NonNull;

/// A [`Own<T>`] represents the `kj::Own<T>`. It is a smart pointer to an opaque C++ type.
/// Safety:
Expand All @@ -22,8 +55,8 @@ pub mod repr {
/// to Rust
#[repr(C)]
pub struct Own<T> {
disposer: *const c_void,
ptr: NonNull<T>,
pub(crate) disposer: *const c_void,
pub(crate) ptr: NonNullExceptMaybe<T>,
}

/// Public-facing Own api
Expand Down
Loading