diff --git a/gen/src/write.rs b/gen/src/write.rs index 5dd5cc29f..52c03b0e6 100644 --- a/gen/src/write.rs +++ b/gen/src/write.rs @@ -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, } } } @@ -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); @@ -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(_) @@ -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), @@ -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 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); diff --git a/kj-rs/lib.rs b/kj-rs/lib.rs index e32d631b6..0988cc77d 100644 --- a/kj-rs/lib.rs +++ b/kj-rs/lib.rs @@ -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; @@ -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::*; } diff --git a/kj-rs/maybe.rs b/kj-rs/maybe.rs new file mode 100644 index 000000000..4e07e4e1a --- /dev/null +++ b/kj-rs/maybe.rs @@ -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 { + fn is_niche(value: &MaybeUninit) -> bool; +} + +// In Rust, references are not allowed to be null, so a null `MaybeUninit<&T>` is a niche +unsafe impl 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<*const T>>(value) + .assume_init() + .is_null() + } + } +} + +unsafe impl 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>>(value) + .assume_init() + .is_null() + } + } +} + +// In `kj`, `kj::Own` are considered `none` in a `Maybe` if the pointer is null +unsafe impl HasNiche for crate::repr::Own { + fn is_niche(value: &MaybeUninit>) -> 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; + const NONE: Maybe; + fn is_some(value: &Maybe) -> bool; + fn is_none(value: &Maybe) -> bool; + fn some(value: Self) -> Maybe; + fn from_option(value: Option) -> Maybe { + match value { + None => Self::NONE, + Some(val) => Self::some(val), + } + } + fn drop_in_place(value: &mut Maybe) { + if ::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 MaybeItem for $ty { + type Discriminant = (); + + fn is_some(value: &Maybe) -> bool { + !<$ty as HasNiche>::is_niche(&value.some) + } + + fn is_none(value: &Maybe) -> bool { + <$ty as HasNiche>::is_niche(&value.some) + } + + const NONE: Maybe = { + Maybe { + is_set: (), + some: MaybeUninit::zeroed(), + } + }; + + fn some(value: Self) -> Maybe { + 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) -> bool { + value.is_set + } + + fn is_none(value: &Maybe) -> bool { + !value.is_set + } + + const NONE: Maybe = { + Maybe { + is_set: false, + some: MaybeUninit::uninit(), + } + }; + + fn some(value: Self) -> Maybe { + 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, &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`. + /// + /// ## 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 { + pub(super) is_set: T::Discriminant, + pub(super) some: MaybeUninit, + } + + assert_eq_size!(Maybe, [usize; 2]); + assert_eq_size!(Maybe<&isize>, usize); + assert_eq_size!(Maybe>, [usize; 2]); + + impl Maybe { + /// # 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, + ) -> Maybe { + 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::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::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::None` variant should always be possible from Rust. + #[allow(non_upper_case_globals, dead_code)] + pub const None: Maybe = T::NONE; + } + + impl From> for Option { + fn from(value: Maybe) -> 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 From> for Maybe { + fn from(value: Option) -> Self { + ::from_option(value) + } + } + + impl Debug for Maybe { + 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 Display for Maybe { + 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 Drop for Maybe { + fn drop(&mut self) { + T::drop_in_place(self); + } + } +} diff --git a/kj-rs/own.rs b/kj-rs/own.rs index 24fdd6f2e..e805a6626 100644 --- a/kj-rs/own.rs +++ b/kj-rs/own.rs @@ -1,18 +1,51 @@ //! The `workerd-cxx` module containing the [`Own`] type, which is bindings to the `kj::Own` 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`, not Maybe>, and using a [`NonNull`] in `Own` +/// but allowing Nulls for Niche Value Optimization is undefined behavior. +#[repr(transparent)] +pub(crate) struct NonNullExceptMaybe(pub(crate) *mut T, PhantomData); + +impl NonNullExceptMaybe { + 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`], which does niche value optimization + // for a null pointer, so the inner [`Own`] 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`], which does niche value optimization + // for a null pointer, so the inner [`Own`] can never be accessed if it is null + unsafe { self.0.as_mut().unwrap() } + } +} + +impl fmt::Pointer for NonNullExceptMaybe { + 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`] represents the `kj::Own`. It is a smart pointer to an opaque C++ type. /// Safety: @@ -22,8 +55,8 @@ pub mod repr { /// to Rust #[repr(C)] pub struct Own { - disposer: *const c_void, - ptr: NonNull, + pub(crate) disposer: *const c_void, + pub(crate) ptr: NonNullExceptMaybe, } /// Public-facing Own api diff --git a/kj-rs/tests/BUILD.bazel b/kj-rs/tests/BUILD.bazel index 7636d884d..57022816b 100644 --- a/kj-rs/tests/BUILD.bazel +++ b/kj-rs/tests/BUILD.bazel @@ -37,6 +37,7 @@ rust_test( crate = "tests", deps = [ ":test-promises", + ":test-maybe", ], ) @@ -45,7 +46,9 @@ rust_cxx_bridge( src = "lib.rs", hdrs = [ "test-promises.h", - "test-own.h" + "test-maybe.h", + "test-own.h", + "shared.h" ], include_prefix = "kj-rs-demo", deps = [ @@ -69,6 +72,21 @@ cc_library( ], ) +cc_library( + name = "test-maybe", + srcs = [ + "test-maybe.c++", + ], + linkstatic = select({ + "@platforms//os:windows": True, + "//conditions:default": False, + }), + visibility = ["//visibility:public"], + deps = [ + ":bridge", + ], +) + cc_test( name = "awaitables-cc-test", size = "medium", @@ -83,6 +101,7 @@ cc_test( ":tests", ":bridge", ":test-promises", + ":test-maybe", "//third-party:runtime", "@capnp-cpp//src/kj:kj-test", ], diff --git a/kj-rs/tests/awaitables-cc-test.c++ b/kj-rs/tests/awaitables-cc-test.c++ index 274d8d093..b0e8ddc07 100644 --- a/kj-rs/tests/awaitables-cc-test.c++ +++ b/kj-rs/tests/awaitables-cc-test.c++ @@ -4,6 +4,7 @@ #include "kj-rs-demo/test-promises.h" #include "kj-rs/awaiter.h" #include "kj-rs/future.h" +#include "kj-rs/tests/lib.rs.h" #include "kj-rs/waker.h" #include diff --git a/kj-rs/tests/lib.rs b/kj-rs/tests/lib.rs index aaba6f18f..f54978be1 100644 --- a/kj-rs/tests/lib.rs +++ b/kj-rs/tests/lib.rs @@ -5,8 +5,13 @@ #![allow(clippy::cast_possible_truncation)] #![allow(clippy::should_panic_without_expect)] #![allow(clippy::missing_panics_doc)] +#![allow(clippy::must_use_candidate)] +#![allow(clippy::cast_possible_truncation)] +#![allow(clippy::should_panic_without_expect)] +#![allow(clippy::missing_panics_doc)] mod test_futures; +mod test_maybe; mod test_own; use test_futures::{ @@ -16,6 +21,11 @@ use test_futures::{ new_waking_future_void, new_wrapped_waker_future_void, }; +use test_maybe::{ + take_maybe_own, take_maybe_own_ret, take_maybe_ref, take_maybe_ref_ret, take_maybe_shared, + take_maybe_shared_ret, +}; + use kj_rs::repr::Own; type Result = std::io::Result; @@ -79,6 +89,98 @@ mod ffi { fn get_null() -> Own; } + unsafe extern "C++" { + include!("kj-rs-demo/test-maybe.h"); + + #[allow(dead_code)] + fn test_maybe_reference_shared_own_driver(); + + #[allow(dead_code)] + fn return_maybe() -> Maybe; + #[allow(dead_code)] + fn return_maybe_none() -> Maybe; + #[allow(dead_code)] + fn return_maybe_ref_some<'a>() -> Maybe<&'a i64>; + #[allow(dead_code)] + fn return_maybe_ref_none<'a>() -> Maybe<&'a i64>; + #[allow(dead_code)] + fn return_maybe_shared_some() -> Maybe; + #[allow(dead_code)] + fn return_maybe_shared_none() -> Maybe; + #[allow(dead_code)] + fn return_maybe_own_some() -> Maybe>; + #[allow(dead_code)] + fn return_maybe_own_none() -> Maybe>; + #[allow(dead_code)] + fn take_maybe_own_cxx(own: Maybe>); + } + + unsafe extern "C++" { + include!("kj-rs-demo/test-maybe.h"); + + #[allow(dead_code)] + fn test_maybe_u8_some() -> Maybe; + #[allow(dead_code)] + fn test_maybe_u16_some() -> Maybe; + #[allow(dead_code)] + fn test_maybe_u32_some() -> Maybe; + #[allow(dead_code)] + fn test_maybe_u64_some() -> Maybe; + #[allow(dead_code)] + fn test_maybe_usize_some() -> Maybe; + #[allow(dead_code)] + fn test_maybe_i8_some() -> Maybe; + #[allow(dead_code)] + fn test_maybe_i16_some() -> Maybe; + #[allow(dead_code)] + fn test_maybe_i32_some() -> Maybe; + #[allow(dead_code)] + fn test_maybe_i64_some() -> Maybe; + #[allow(dead_code)] + fn test_maybe_isize_some() -> Maybe; + #[allow(dead_code)] + fn test_maybe_f32_some() -> Maybe; + #[allow(dead_code)] + fn test_maybe_f64_some() -> Maybe; + #[allow(dead_code)] + fn test_maybe_bool_some() -> Maybe; + #[allow(dead_code)] + fn test_maybe_u8_none() -> Maybe; + #[allow(dead_code)] + fn test_maybe_u16_none() -> Maybe; + #[allow(dead_code)] + fn test_maybe_u32_none() -> Maybe; + #[allow(dead_code)] + fn test_maybe_u64_none() -> Maybe; + #[allow(dead_code)] + fn test_maybe_usize_none() -> Maybe; + #[allow(dead_code)] + fn test_maybe_i8_none() -> Maybe; + #[allow(dead_code)] + fn test_maybe_i16_none() -> Maybe; + #[allow(dead_code)] + fn test_maybe_i32_none() -> Maybe; + #[allow(dead_code)] + fn test_maybe_i64_none() -> Maybe; + #[allow(dead_code)] + fn test_maybe_isize_none() -> Maybe; + #[allow(dead_code)] + fn test_maybe_f32_none() -> Maybe; + #[allow(dead_code)] + fn test_maybe_f64_none() -> Maybe; + #[allow(dead_code)] + fn test_maybe_bool_none() -> Maybe; + } + + extern "Rust" { + fn take_maybe_own_ret(val: Maybe>) -> Maybe>; + fn take_maybe_own(val: Maybe>); + unsafe fn take_maybe_ref_ret<'a>(val: Maybe<&'a u64>) -> Maybe<&'a u64>; + fn take_maybe_ref(val: Maybe<&u64>); + fn take_maybe_shared_ret(val: Maybe) -> Maybe; + fn take_maybe_shared(val: Maybe); + } + enum CloningAction { None, CloneSameThread, diff --git a/kj-rs/tests/shared.h b/kj-rs/tests/shared.h new file mode 100644 index 000000000..035d7d440 --- /dev/null +++ b/kj-rs/tests/shared.h @@ -0,0 +1,5 @@ +#pragma once + +namespace kj_rs_demo { +struct Shared; +} diff --git a/kj-rs/tests/test-maybe.c++ b/kj-rs/tests/test-maybe.c++ new file mode 100644 index 000000000..fc75af642 --- /dev/null +++ b/kj-rs/tests/test-maybe.c++ @@ -0,0 +1,166 @@ +#include "test-maybe.h" + +#include "kj-rs-demo/lib.rs.h" +#include "kj-rs/tests/lib.rs.h" +#include "kj/common.h" +#include "kj/debug.h" +#include "kj/memory.h" + +#include + +namespace kj_rs_demo { + +kj::Maybe return_maybe_shared_some() { + return kj::Maybe(Shared{14}); +} + +kj::Maybe return_maybe_shared_none() { + return kj::Maybe(kj::none); +} + +kj::Maybe return_maybe() { + kj::Maybe ret = kj::some(14); + return kj::mv(ret); +} + +kj::Maybe return_maybe_none() { + kj::Maybe ret = kj::none; + return kj::mv(ret); +} + +// Static var to return non-dangling pointer without heap allocating +int64_t var = 14; + +static_assert(sizeof(kj::Maybe) == sizeof(const int64_t&)); +kj::Maybe return_maybe_ref_none() { + kj::Maybe ret = kj::none; + return kj::mv(ret); +} + +kj::Maybe return_maybe_ref_some() { + const int64_t& val = var; + kj::Maybe ret = kj::some(val); + return kj::mv(ret); +} + +kj::Maybe> return_maybe_own_none() { + kj::Maybe> maybe = kj::none; + return kj::mv(maybe); +} + +kj::Maybe> return_maybe_own_some() { + kj::Maybe> ret = kj::heap(14); + return kj::mv(ret); +} + +void take_maybe_own_cxx(kj::Maybe> maybe) { + KJ_IF_SOME(val, maybe) { + KJ_ASSERT(val->getData() == 14); + } +} + +void test_maybe_reference_shared_own_driver() { + kj::Maybe> maybe_own_some = return_maybe_own_some(); + uint64_t num = 15; + kj::Maybe maybe_ref_some = kj::Maybe(num); + kj::Maybe maybe_shared_some = return_maybe_shared_some(); + + auto maybe_own = take_maybe_own_ret(kj::mv(maybe_own_some)); + KJ_IF_SOME(i, maybe_own) { + KJ_ASSERT(i->getData() == 42); + } else { + KJ_FAIL_ASSERT("Not reached"); + } + take_maybe_own(kj::mv(maybe_own)); + + auto maybe_ref = take_maybe_ref_ret(kj::mv(maybe_ref_some)); + take_maybe_ref(kj::mv(maybe_ref)); + + auto maybe_shared = take_maybe_shared_ret(kj::mv(maybe_shared_some)); + KJ_IF_SOME(_, maybe_shared) { + KJ_FAIL_ASSERT("Returns none, so unreached"); + } + take_maybe_shared(kj::mv(maybe_shared)); +} + +kj::Maybe test_maybe_u8_some() { + return kj::some(234); +} +kj::Maybe test_maybe_u16_some() { + return kj::some(235); +} +kj::Maybe test_maybe_u32_some() { + return kj::some(236); +} +kj::Maybe test_maybe_u64_some() { + return kj::some(237); +} +kj::Maybe test_maybe_usize_some() { + return kj::some(238); +} +kj::Maybe test_maybe_i8_some() { + return kj::some(97); +} +kj::Maybe test_maybe_i16_some() { + return kj::some(240); +} +kj::Maybe test_maybe_i32_some() { + return kj::some(241); +} +kj::Maybe test_maybe_i64_some() { + return kj::some(242); +} +kj::Maybe test_maybe_isize_some() { + return kj::some(243); +} +kj::Maybe test_maybe_f32_some() { + return kj::some(244.678); +} +kj::Maybe test_maybe_f64_some() { + return kj::some(245.678); +} +kj::Maybe test_maybe_bool_some() { + return kj::some(false); +} + +kj::Maybe test_maybe_u8_none() { + return kj::none; +} +kj::Maybe test_maybe_u16_none() { + return kj::none; +} +kj::Maybe test_maybe_u32_none() { + return kj::none; +} +kj::Maybe test_maybe_u64_none() { + return kj::none; +} +kj::Maybe test_maybe_usize_none() { + return kj::none; +} +kj::Maybe test_maybe_i8_none() { + return kj::none; +} +kj::Maybe test_maybe_i16_none() { + return kj::none; +} +kj::Maybe test_maybe_i32_none() { + return kj::none; +} +kj::Maybe test_maybe_i64_none() { + return kj::none; +} +kj::Maybe test_maybe_isize_none() { + return kj::none; +} +kj::Maybe test_maybe_f32_none() { + return kj::none; +} +kj::Maybe test_maybe_f64_none() { + return kj::none; +} +kj::Maybe test_maybe_bool_none() { + return kj::none; +} + +} // namespace kj_rs_demo diff --git a/kj-rs/tests/test-maybe.h b/kj-rs/tests/test-maybe.h new file mode 100644 index 000000000..5b992806a --- /dev/null +++ b/kj-rs/tests/test-maybe.h @@ -0,0 +1,53 @@ +#pragma once + +#include "kj-rs-demo/lib.rs.h" +#include "kj/common.h" + +#include + +namespace kj_rs_demo { + +kj::Maybe return_maybe_shared_some(); +kj::Maybe return_maybe_shared_none(); + +kj::Maybe return_maybe(); +kj::Maybe return_maybe_none(); + +kj::Maybe return_maybe_ref_some(); +kj::Maybe return_maybe_ref_none(); + +kj::Maybe> return_maybe_own_none(); +kj::Maybe> return_maybe_own_some(); + +void take_maybe_own_cxx(kj::Maybe> maybe); + +void test_maybe_reference_shared_own_driver(); + +kj::Maybe test_maybe_u8_some(); +kj::Maybe test_maybe_u16_some(); +kj::Maybe test_maybe_u32_some(); +kj::Maybe test_maybe_u64_some(); +kj::Maybe test_maybe_usize_some(); +kj::Maybe test_maybe_i8_some(); +kj::Maybe test_maybe_i16_some(); +kj::Maybe test_maybe_i32_some(); +kj::Maybe test_maybe_i64_some(); +kj::Maybe test_maybe_isize_some(); +kj::Maybe test_maybe_f32_some(); +kj::Maybe test_maybe_f64_some(); +kj::Maybe test_maybe_bool_some(); +kj::Maybe test_maybe_u8_none(); +kj::Maybe test_maybe_u16_none(); +kj::Maybe test_maybe_u32_none(); +kj::Maybe test_maybe_u64_none(); +kj::Maybe test_maybe_usize_none(); +kj::Maybe test_maybe_i8_none(); +kj::Maybe test_maybe_i16_none(); +kj::Maybe test_maybe_i32_none(); +kj::Maybe test_maybe_i64_none(); +kj::Maybe test_maybe_isize_none(); +kj::Maybe test_maybe_f32_none(); +kj::Maybe test_maybe_f64_none(); +kj::Maybe test_maybe_bool_none(); + +} // namespace kj_rs_demo diff --git a/kj-rs/tests/test-promises.c++ b/kj-rs/tests/test-promises.c++ index 0bc610098..73eec4865 100644 --- a/kj-rs/tests/test-promises.c++ +++ b/kj-rs/tests/test-promises.c++ @@ -1,3 +1,5 @@ +#include "kj-rs/tests/lib.rs.h" + #include #include diff --git a/kj-rs/tests/test-promises.h b/kj-rs/tests/test-promises.h index b92442a70..e06b8c2d1 100644 --- a/kj-rs/tests/test-promises.h +++ b/kj-rs/tests/test-promises.h @@ -1,9 +1,11 @@ #pragma once -#include "kj-rs-demo/lib.rs.h" +#include "shared.h" #include +#include + namespace kj_rs_demo { kj::Promise new_ready_promise_void(); diff --git a/kj-rs/tests/test_futures.rs b/kj-rs/tests/test_futures.rs index b7b5a9628..9040ab072 100644 --- a/kj-rs/tests/test_futures.rs +++ b/kj-rs/tests/test_futures.rs @@ -149,10 +149,7 @@ pub async fn new_layered_ready_future_void() -> Result<()> { } // From example at https://doc.rust-lang.org/std/future/fn.poll_fn.html#capturing-a-pinned-state -async fn naive_select( - a: impl Future, - b: impl Future, -) -> T { +async fn naive_select(a: impl Future, b: impl Future) -> T { let (mut a, mut b) = (pin!(a), pin!(b)); future::poll_fn(move |cx| { if let Poll::Ready(r) = a.as_mut().poll(cx) { diff --git a/kj-rs/tests/test_maybe.rs b/kj-rs/tests/test_maybe.rs new file mode 100644 index 000000000..0bb880320 --- /dev/null +++ b/kj-rs/tests/test_maybe.rs @@ -0,0 +1,238 @@ +use crate::ffi::{OpaqueCxxClass, Shared}; +use kj_rs::{ + maybe::MaybeItem, + repr::{Maybe, Own}, +}; +use std::{cmp::PartialEq, fmt::Debug}; + +pub fn take_maybe_own_ret(val: Maybe>) -> Maybe> { + let mut option: Option> = val.into(); + if let Some(val) = &mut option { + val.as_mut().set_data(42); + } + + option.into() +} + +pub fn take_maybe_own(val: Maybe>) { + let option: Option> = val.into(); + // Own gets destoyed at end of `if let` block, because it takes ownership of `option` + if let Some(own) = option { + assert_eq!(own.get_data(), 42); + } +} + +/// # Safety: Uses a reference in a function that can be called from C++, which is opaque +/// to the Rust compiler, so it cannot verify lifetime requirements +pub unsafe fn take_maybe_ref_ret<'a>(val: Maybe<&'a u64>) -> Maybe<&'a u64> { + let option: Option<&u64> = val.into(); + if let Some(num) = &option { + assert_eq!(**num, 15); + } + option.into() +} + +pub fn take_maybe_ref(val: Maybe<&u64>) { + let mut option: Option<&u64> = val.into(); + // Pure Rust at this point, but just in case + if let Some(val) = option.take() { + assert_eq!(*val, 15); + } +} + +pub fn take_maybe_shared_ret(val: Maybe) -> Maybe { + let mut option: Option = val.into(); + if let Some(mut shared) = option.take() { + shared.i = 18; + } + option.into() +} + +pub fn take_maybe_shared(val: Maybe) { + let _: Option = val.into(); +} + +#[allow(clippy::needless_pass_by_value, dead_code)] +fn test_maybe_some(val: Maybe, num: T) { + assert!(val.is_some()); + let opt: Option = val.into(); + assert_eq!(opt.unwrap(), num); +} + +#[allow(clippy::needless_pass_by_value, dead_code)] +fn test_maybe_none(val: Maybe) { + assert!(val.is_none()); +} + +#[cfg(test)] +pub mod tests { + use super::{test_maybe_none, test_maybe_some}; + use crate::ffi::{self, OpaqueCxxClass, Shared}; + use kj_rs::repr::{Maybe, Own}; + + #[test] + fn test_some() { + let maybe: Maybe = ffi::return_maybe(); + assert!(!maybe.is_none()); + } + + #[test] + fn test_none() { + let maybe: Maybe = ffi::return_maybe_none(); + assert!(maybe.is_none()); + } + + #[test] + fn test_none_ref() { + let maybe = ffi::return_maybe_ref_none(); + assert!(maybe.is_none()); + } + + #[test] + fn test_none_ref_opt() { + let maybe = ffi::return_maybe_ref_none(); + let maybe: Option<&i64> = maybe.into(); + assert!(maybe.is_none()); + } + + #[test] + fn test_some_ref() { + let maybe = ffi::return_maybe_ref_some(); + assert!(maybe.is_some()); + } + + #[test] + fn test_some_ref_opt() { + let maybe = ffi::return_maybe_ref_some(); + let maybe: Option<&i64> = maybe.into(); + assert!(maybe.is_some()); + } + + #[test] + fn test_some_shared() { + let maybe: Maybe = ffi::return_maybe_shared_some(); + assert!(!maybe.is_none()); + let opt: Option = maybe.into(); + assert!(opt.is_some()); + assert_eq!(opt.unwrap().i, 14); + } + + #[test] + fn test_none_shared() { + let maybe: Maybe = ffi::return_maybe_shared_none(); + assert!(maybe.is_none()); + let opt: Option = maybe.into(); + assert!(opt.is_none()); + } + + #[test] + fn test_some_own() { + let maybe = ffi::return_maybe_own_some(); + assert!(!maybe.is_none()); + let opt: Option> = maybe.into(); + assert!(opt.is_some()); + assert_eq!(opt.unwrap().get_data(), 14); + } + + #[test] + fn test_none_own() { + let maybe = ffi::return_maybe_own_none(); + assert!(maybe.is_none()); + let opt: Option> = maybe.into(); + assert!(opt.is_none()); + } + + #[test] + fn test_some_own_maybe() { + let maybe = ffi::return_maybe_own_some(); + assert!(!maybe.is_none()); + assert!(maybe.is_some()); + } + + #[test] + fn test_none_own_maybe() { + let maybe = ffi::return_maybe_own_none(); + assert!(maybe.is_none()); + assert!(!maybe.is_some()); + } + + #[test] + fn test_primitive_types() { + macro_rules! Maybe { + ($ty:ty) => { + let (some, none): (Maybe<$ty>, Maybe<$ty>) = unsafe {( + Maybe::from_parts_unchecked(true, std::mem::MaybeUninit::new(<$ty>::default())), + Maybe::from_parts_unchecked(false, std::mem::MaybeUninit::uninit()), + )}; + + assert!(some.is_some()); + assert!(!some.is_none()); + assert!(!none.is_some()); + assert!(none.is_none()); + + let opt: Option<$ty> = some.into(); + assert_eq!(opt.unwrap(), <$ty>::default()); + }; + ($ty:ty, $($tail:ty),+) => { + Maybe!($ty); + Maybe!($($tail),*); + } + } + Maybe!( + u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, bool + ); + + test_maybe_some(ffi::test_maybe_u8_some(), 234); + test_maybe_some(ffi::test_maybe_u16_some(), 235); + test_maybe_some(ffi::test_maybe_u32_some(), 236); + test_maybe_some(ffi::test_maybe_u64_some(), 237); + test_maybe_some(ffi::test_maybe_usize_some(), 238); + test_maybe_some(ffi::test_maybe_i8_some(), 97); + test_maybe_some(ffi::test_maybe_i16_some(), 240); + test_maybe_some(ffi::test_maybe_i32_some(), 241); + test_maybe_some(ffi::test_maybe_i64_some(), 242); + test_maybe_some(ffi::test_maybe_isize_some(), 243); + test_maybe_some(ffi::test_maybe_f32_some(), 244.678); + test_maybe_some(ffi::test_maybe_f64_some(), 245.678); + test_maybe_some(ffi::test_maybe_bool_some(), false); + + test_maybe_none(ffi::test_maybe_u8_none()); + test_maybe_none(ffi::test_maybe_u16_none()); + test_maybe_none(ffi::test_maybe_u32_none()); + test_maybe_none(ffi::test_maybe_u64_none()); + test_maybe_none(ffi::test_maybe_usize_none()); + test_maybe_none(ffi::test_maybe_i8_none()); + test_maybe_none(ffi::test_maybe_i16_none()); + test_maybe_none(ffi::test_maybe_i32_none()); + test_maybe_none(ffi::test_maybe_i64_none()); + test_maybe_none(ffi::test_maybe_isize_none()); + test_maybe_none(ffi::test_maybe_f32_none()); + test_maybe_none(ffi::test_maybe_f64_none()); + test_maybe_none(ffi::test_maybe_bool_none()); + } + + #[test] + fn test_pass_cxx() { + let maybe = ffi::return_maybe_own_some(); + ffi::take_maybe_own_cxx(maybe); + } + + #[test] + fn test_pass_rust() { + let mut own = ffi::cxx_kj_own(); + own.pin_mut().set_data(14); + let maybe_some: Maybe> = Maybe::Some(own); + let maybe_none: Maybe> = Maybe::None; + + assert!(maybe_some.is_some()); + assert!(maybe_none.is_none()); + + ffi::take_maybe_own_cxx(maybe_some); + ffi::take_maybe_own_cxx(maybe_none); + } + + #[test] + fn test_maybe_driver() { + ffi::test_maybe_reference_shared_own_driver(); + } +} diff --git a/macro/src/expand.rs b/macro/src/expand.rs index 7a3d2428e..d82f27874 100644 --- a/macro/src/expand.rs +++ b/macro/src/expand.rs @@ -128,7 +128,10 @@ fn expand(ffi: Module, doc: Doc, attrs: OtherAttrs, apis: &[Api], types: &Types) ImplKey::CxxVector(ident) => { expanded.extend(expand_cxx_vector(ident, explicit_impl, types)); } - // We do not need to generate code on the rust side for [`kj_rs::Own`] + ImplKey::Maybe(ident) => { + expanded.extend(expand_kj_maybe(ident, explicit_impl, types)); + } + // We do not yet need to generate code on the rust side for [`kj_rs::Own`] ImplKey::Own(_) => (), } } @@ -1798,6 +1801,49 @@ fn expand_weak_ptr(key: NamedImplKey, types: &Types, explicit_impl: Option<&Impl } } +fn expand_kj_maybe(key: NamedImplKey, explicit_impl: Option<&Impl>, types: &Types) -> TokenStream { + let elem = key.rust; + let resolve = types.resolve(elem); + + let (_, ty_generics) = generics::split_for_impl(key, explicit_impl, resolve); + + let begin_span = explicit_impl.map_or(key.begin_span, |explicit| explicit.impl_token.span); + let end_span = explicit_impl.map_or(key.end_span, |explicit| explicit.brace_token.span.join()); + let unsafe_token = format_ident!("unsafe", span = begin_span); + + quote_spanned! {end_span => + #[automatically_derived] + #unsafe_token impl ::kj_rs::maybe::MaybeItem for #elem #ty_generics { + type Discriminant = bool; + + fn is_some(value: &::kj_rs::Maybe) -> bool { + unsafe { + value.is_set() + } + } + + fn is_none(value: &::kj_rs::Maybe) -> bool { + unsafe { + !value.is_set() + } + } + + const NONE: ::kj_rs::Maybe = unsafe { + ::kj_rs::Maybe::from_parts_unchecked(false, ::std::mem::MaybeUninit::uninit()) + }; + + fn some(value: Self) -> ::kj_rs::Maybe { + unsafe { + ::kj_rs::Maybe::from_parts_unchecked( + true, + ::std::mem::MaybeUninit::new(value) + ) + } + } + } + } +} + fn expand_cxx_vector( key: NamedImplKey, explicit_impl: Option<&Impl>, diff --git a/syntax/check.rs b/syntax/check.rs index 8d2ff35be..8cb79d238 100644 --- a/syntax/check.rs +++ b/syntax/check.rs @@ -77,6 +77,7 @@ fn check_type(cx: &mut Check, ty: &Type) { Type::WeakPtr(ptr) => check_type_weak_ptr(cx, ptr), Type::CxxVector(ptr) => check_type_cxx_vector(cx, ptr), Type::Ref(ty) => check_type_ref(cx, ty), + Type::Maybe(ty) => check_type_kj_maybe(cx, ty), Type::Ptr(ty) => check_type_ptr(cx, ty), Type::Array(array) => check_type_array(cx, array), Type::Fn(ty) => check_type_fn(cx, ty), @@ -233,6 +234,45 @@ fn check_type_weak_ptr(cx: &mut Check, ptr: &Ty1) { cx.error(ptr, "unsupported weak_ptr target type"); } +fn check_type_kj_maybe(cx: &mut Check, ptr: &Ty1) { + match &ptr.inner { + Type::Ident(ident) => { + if cx.types.rust.contains(&ident.rust) { + cx.error( + ptr, + "kj::Maybe of a non-primitive Rust type is not supported yet", + ); + return; + } + + match Atom::from(&ident.rust) { + None + | Some( + Bool | U8 | U16 | U32 | U64 | Usize | I8 | I16 | I32 | I64 | Isize | F32 | F64, + ) => return, + Some(_) => {} + } + } + Type::Ptr(_) => { + cx.error( + ptr, + "kj::Maybe is not supported. Perhaps consider using Maybe", + ); + return; // Return ensures the LSP error is the more helpful error + } + Type::Ref(refr) => { + check_type_ref(cx, refr); + return; + } + Type::Own(own) => { + check_type_kj_own(cx, own); + return; + } + _ => (), + } + cx.error(ptr, "unsupported kj::Maybe target type"); +} + fn check_type_cxx_vector(cx: &mut Check, ptr: &Ty1) { if let Type::Ident(ident) = &ptr.inner { if cx.types.rust.contains(&ident.rust) { @@ -575,6 +615,7 @@ fn check_api_impl(cx: &mut Check, imp: &Impl) { | Type::RustVec(ty) | Type::UniquePtr(ty) | Type::Own(ty) + | Type::Maybe(ty) | Type::SharedPtr(ty) | Type::WeakPtr(ty) | Type::CxxVector(ty) => { @@ -710,6 +751,7 @@ fn is_unsized(cx: &mut Check, ty: &Type) -> bool { | Type::Own(_) | Type::SharedPtr(_) | Type::WeakPtr(_) + | Type::Maybe(_) | Type::Ref(_) | Type::Ptr(_) | Type::Str(_) @@ -786,6 +828,7 @@ fn describe(cx: &mut Check, ty: &Type) -> String { Type::Own(_) => "kj::Own".to_owned(), Type::SharedPtr(_) => "shared_ptr".to_owned(), Type::WeakPtr(_) => "weak_ptr".to_owned(), + Type::Maybe(_) => "kj::Maybe".to_owned(), Type::Ref(_) => "reference".to_owned(), Type::Ptr(_) => "raw pointer".to_owned(), Type::Str(_) => "&str".to_owned(), diff --git a/syntax/impls.rs b/syntax/impls.rs index 8ce98759e..01b774106 100644 --- a/syntax/impls.rs +++ b/syntax/impls.rs @@ -52,6 +52,7 @@ impl Hash for Type { Type::WeakPtr(t) => t.hash(state), Type::Ref(t) => t.hash(state), Type::Ptr(t) => t.hash(state), + Type::Maybe(t) => t.hash(state), Type::Str(t) => t.hash(state), Type::RustVec(t) => t.hash(state), Type::CxxVector(t) => t.hash(state), diff --git a/syntax/improper.rs b/syntax/improper.rs index a99e97fa6..789b2858f 100644 --- a/syntax/improper.rs +++ b/syntax/improper.rs @@ -34,6 +34,7 @@ impl<'a> Types<'a> { Type::Ref(ty) => self.determine_improper_ctype(&ty.inner), Type::Ptr(ty) => self.determine_improper_ctype(&ty.inner), Type::Array(ty) => self.determine_improper_ctype(&ty.inner), + Type::Maybe(ty) => self.determine_improper_ctype(&ty.inner), Type::Future(_) | Type::Own(_) => todo!("file a workerd-cxx ticket"), } } diff --git a/syntax/instantiate.rs b/syntax/instantiate.rs index fc568381d..575e24615 100644 --- a/syntax/instantiate.rs +++ b/syntax/instantiate.rs @@ -9,6 +9,7 @@ pub enum ImplKey<'a> { RustVec(NamedImplKey<'a>), UniquePtr(NamedImplKey<'a>), Own(NamedImplKey<'a>), + Maybe(NamedImplKey<'a>), SharedPtr(NamedImplKey<'a>), WeakPtr(NamedImplKey<'a>), CxxVector(NamedImplKey<'a>), @@ -45,6 +46,10 @@ impl Type { if let Type::Ident(ident) = &ty.inner { return Some(ImplKey::Own(NamedImplKey::new(ty, ident))); } + } else if let Type::Maybe(ty) = self { + if let Type::Ident(ident) = &ty.inner { + return Some(ImplKey::Maybe(NamedImplKey::new(ty, ident))); + } } else if let Type::SharedPtr(ty) = self { if let Type::Ident(ident) = &ty.inner { return Some(ImplKey::SharedPtr(NamedImplKey::new(ty, ident))); diff --git a/syntax/mod.rs b/syntax/mod.rs index 9b93cb3cd..bdbb9ec3e 100644 --- a/syntax/mod.rs +++ b/syntax/mod.rs @@ -301,6 +301,7 @@ pub enum Type { CxxVector(Box), Fn(Box), Void(Span), + Maybe(Box), SliceRef(Box), Array(Box), Future(Box), diff --git a/syntax/parse.rs b/syntax/parse.rs index 6576ccd65..44dd57635 100644 --- a/syntax/parse.rs +++ b/syntax/parse.rs @@ -1060,6 +1060,7 @@ fn parse_impl(cx: &mut Errors, imp: ItemImpl) -> Result { | Type::Own(ty) | Type::SharedPtr(ty) | Type::WeakPtr(ty) + | Type::Maybe(ty) | Type::CxxVector(ty) => match &ty.inner { Type::Ident(ident) => ident.generics.clone(), _ => Lifetimes::default(), @@ -1276,6 +1277,16 @@ fn parse_type_path(ty: &TypePath) -> Result { rangle: generic.gt_token, }))); } + } else if ident == "Maybe" && generic.args.len() == 1 { + if let GenericArgument::Type(arg) = &generic.args[0] { + let inner = parse_type(arg)?; + return Ok(Type::Maybe(Box::new(Ty1 { + name: ident, + langle: generic.lt_token, + inner, + rangle: generic.gt_token, + }))); + } } else if ident == "Vec" && generic.args.len() == 1 { if let GenericArgument::Type(arg) = &generic.args[0] { let inner = parse_type(arg)?; @@ -1489,6 +1500,7 @@ fn has_references_without_lifetime(ty: &Type) -> bool { | Type::Own(t) | Type::SharedPtr(t) | Type::WeakPtr(t) + | Type::Maybe(t) | Type::CxxVector(t) => has_references_without_lifetime(&t.inner), Type::Ptr(t) => has_references_without_lifetime(&t.inner), Type::Array(t) => has_references_without_lifetime(&t.inner), diff --git a/syntax/pod.rs b/syntax/pod.rs index fe56957ff..76714105b 100644 --- a/syntax/pod.rs +++ b/syntax/pod.rs @@ -31,6 +31,7 @@ impl<'a> Types<'a> { | Type::CxxVector(_) | Type::Void(_) => false, Type::Ref(_) | Type::Str(_) | Type::Fn(_) | Type::SliceRef(_) | Type::Ptr(_) => true, + Type::Maybe(ty) => self.is_guaranteed_pod(&ty.inner), Type::Array(array) => self.is_guaranteed_pod(&array.inner), Type::Future(_) => false, } diff --git a/syntax/tokens.rs b/syntax/tokens.rs index 8398ca6bf..1ff59c8fa 100644 --- a/syntax/tokens.rs +++ b/syntax/tokens.rs @@ -30,6 +30,7 @@ impl ToTokens for Type { | Type::SharedPtr(ty) | Type::WeakPtr(ty) | Type::CxxVector(ty) + | Type::Maybe(ty) | Type::RustVec(ty) => ty.to_tokens(tokens), Type::Ref(r) | Type::Str(r) => r.to_tokens(tokens), Type::Ptr(p) => p.to_tokens(tokens), @@ -78,6 +79,9 @@ impl ToTokens for Ty1 { "Box" => { tokens.extend(quote_spanned!(span=> ::cxx::alloc::boxed::)); } + "Maybe" => { + tokens.extend(quote_spanned!(span=> ::kj_rs::repr::)); + } "Vec" => { tokens.extend(quote_spanned!(span=> ::cxx::alloc::vec::)); } diff --git a/syntax/types.rs b/syntax/types.rs index ad32e49d0..2eb6c6b28 100644 --- a/syntax/types.rs +++ b/syntax/types.rs @@ -181,6 +181,7 @@ impl<'a> Types<'a> { | ImplKey::Own(ident) | ImplKey::SharedPtr(ident) | ImplKey::WeakPtr(ident) + | ImplKey::Maybe(ident) | ImplKey::CxxVector(ident) => { Atom::from(ident.rust).is_none() && !aliases.contains_key(ident.rust) } @@ -245,7 +246,7 @@ impl<'a> Types<'a> { match ty { Type::RustBox(_) | Type::UniquePtr(_) => false, Type::Array(_) => true, - Type::Future(_) | Type::Own(_) => true, + Type::Future(_) | Type::Maybe(_) | Type::Own(_) => true, _ => !self.is_guaranteed_pod(ty), } } diff --git a/syntax/visit.rs b/syntax/visit.rs index 7b4924067..4c08548f9 100644 --- a/syntax/visit.rs +++ b/syntax/visit.rs @@ -18,6 +18,7 @@ where | Type::SharedPtr(ty) | Type::WeakPtr(ty) | Type::CxxVector(ty) + | Type::Maybe(ty) | Type::RustVec(ty) => visitor.visit_type(&ty.inner), Type::Ref(r) => visitor.visit_type(&r.inner), Type::Ptr(p) => visitor.visit_type(&p.inner),