Skip to content

Commit 240d7d3

Browse files
authored
[workerd-cxx] add kj::Maybe to kj-rs and workerd-cxx (#42)
Adds support for `kj::Maybe` to kj-rs. `Maybe` is now available in the bridge module for shared types, references, and `Own`s.
1 parent d3ada6f commit 240d7d3

24 files changed

+1126
-8
lines changed

gen/src/write.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ fn pick_includes_and_builtins(out: &mut OutFile, apis: &[Api]) {
233233
Type::SliceRef(_) => out.builtin.rust_slice = true,
234234
Type::Array(_) => out.include.array = true,
235235
Type::Ref(_) | Type::Void(_) | Type::Ptr(_) => {}
236-
Type::Future(_) | Type::Own(_) => out.include.kj_rs = true,
236+
Type::Future(_) | Type::Maybe(_) | Type::Own(_) => out.include.kj_rs = true,
237237
}
238238
}
239239
}
@@ -1263,6 +1263,11 @@ fn write_type(out: &mut OutFile, ty: &Type) {
12631263
write_type(out, &ptr.inner);
12641264
write!(out, ">");
12651265
}
1266+
Type::Maybe(ptr) => {
1267+
write!(out, "::kj::Maybe<");
1268+
write_type(out, &ptr.inner);
1269+
write!(out, ">");
1270+
}
12661271
Type::CxxVector(ty) => {
12671272
write!(out, "::std::vector<");
12681273
write_type(out, &ty.inner);
@@ -1357,6 +1362,7 @@ fn write_space_after_type(out: &mut OutFile, ty: &Type) {
13571362
| Type::SharedPtr(_)
13581363
| Type::WeakPtr(_)
13591364
| Type::Str(_)
1365+
| Type::Maybe(_)
13601366
| Type::CxxVector(_)
13611367
| Type::RustVec(_)
13621368
| Type::SliceRef(_)
@@ -1431,6 +1437,7 @@ fn write_generic_instantiations(out: &mut OutFile) {
14311437
ImplKey::RustVec(ident) => write_rust_vec_extern(out, ident),
14321438
ImplKey::UniquePtr(ident) => write_unique_ptr(out, ident),
14331439
ImplKey::Own(ident) => write_kj_own(out, ident),
1440+
ImplKey::Maybe(ident) => write_kj_maybe(out, ident),
14341441
ImplKey::SharedPtr(ident) => write_shared_ptr(out, ident),
14351442
ImplKey::WeakPtr(ident) => write_weak_ptr(out, ident),
14361443
ImplKey::CxxVector(ident) => write_cxx_vector(out, ident),
@@ -1669,6 +1676,17 @@ fn write_kj_own(out: &mut OutFile, key: NamedImplKey) {
16691676
);
16701677
}
16711678

1679+
// Writes static assertions for Maybe
1680+
fn write_kj_maybe(out: &mut OutFile, key: NamedImplKey) {
1681+
let ident = key.rust;
1682+
let resolve = out.types.resolve(ident);
1683+
let inner = resolve.name.to_fully_qualified();
1684+
1685+
out.include.utility = true;
1686+
out.include.kj_rs = true;
1687+
writeln!(out, "static_assert(!::std::is_pointer<{}>::value, \"Maybe<T*> is not allowed in workerd-cxx. Use Maybe<T&> or Maybe<Maybe<T&>> instead.\");", inner);
1688+
}
1689+
16721690
fn write_unique_ptr(out: &mut OutFile, key: NamedImplKey) {
16731691
let ty = UniquePtr::Ident(key.rust);
16741692
write_unique_ptr_common(out, ty);

kj-rs/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use awaiter::WakerRef;
44
pub use crate::ffi::KjWaker;
55
pub use awaiter::PromiseAwaiter;
66
pub use future::FuturePollStatus;
7+
pub use maybe::repr::Maybe;
78
pub use own::repr::Own;
89
pub use promise::KjPromise;
910
pub use promise::KjPromiseNodeImpl;
@@ -13,12 +14,14 @@ pub use promise::new_callbacks_promise_future;
1314

1415
mod awaiter;
1516
mod future;
17+
pub mod maybe;
1618
mod own;
1719
mod promise;
1820
mod waker;
1921

2022
pub mod repr {
2123
pub use crate::future::repr::*;
24+
pub use crate::maybe::repr::*;
2225
pub use crate::own::repr::*;
2326
}
2427

kj-rs/maybe.rs

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
use repr::Maybe;
2+
use std::mem::MaybeUninit;
3+
4+
/// # Safety
5+
/// This trait should only be implemented in `workerd-cxx` on types
6+
/// which contain a specialization of `kj::Maybe` that needs to be represented in
7+
/// Rust.
8+
///
9+
/// This trait represents types which have a "niche", a value which represents
10+
/// an invalid instance of the type or can reasonably be interpreted as the absence
11+
/// of that type. This trait is implmented for 2 types, references and `Owns`.
12+
///
13+
/// References have a niche where they are null. It's invalid and ensured by the
14+
/// compiler that this is impossible, so we can optimize an optional type by
15+
/// eliminating a flag that checks whether the item is set or not, and instead
16+
/// checking if it is null.
17+
///
18+
/// `Own`s have a niche where the pointer to the owned data is null. This is
19+
/// a valid instance of `Own`, but was decided by the `kj` authors to represent
20+
/// `kj::none`. In Rust, it is guaranteed that an `Own` is nonnull, requiring
21+
/// `Maybe<Own<T>>` to represent a null `Own`.
22+
///
23+
/// Pointers are not optimized in this way, as `null` is a valid and meaningful
24+
/// instance of a pointer.
25+
///
26+
/// An invalid implementation of this trait for any of the 3 types it is for
27+
/// could result in undefined behavior when passed between languages.
28+
unsafe trait HasNiche: Sized {
29+
fn is_niche(value: *const Self) -> bool;
30+
}
31+
32+
// In Rust, references are not allowed to be null, so a null `MaybeUninit<&T>` is a niche
33+
unsafe impl<T> HasNiche for &T {
34+
fn is_niche(value: *const &T) -> bool {
35+
unsafe {
36+
// We must cast it as pointing to a pointer, as opposed to a reference,
37+
// because the rust compiler assumes a reference is never null, and
38+
// therefore will optimize any null check on that reference.
39+
(*(value.cast::<*const T>())).is_null()
40+
}
41+
}
42+
}
43+
44+
unsafe impl<T> HasNiche for &mut T {
45+
fn is_niche(value: *const &mut T) -> bool {
46+
unsafe {
47+
// We must cast it as pointing to a pointer, as opposed to a reference,
48+
// because the rust compiler assumes a reference is never null, and
49+
// therefore will optimize any null check on that reference.
50+
(*(value.cast::<*mut T>())).is_null()
51+
}
52+
}
53+
}
54+
55+
// In `kj`, `kj::Own<T>` are considered `none` in a `Maybe` if the data pointer is null
56+
unsafe impl<T> HasNiche for crate::repr::Own<T> {
57+
fn is_niche(value: *const crate::repr::Own<T>) -> bool {
58+
unsafe { (*value).as_ptr().is_null() }
59+
}
60+
}
61+
62+
/// Trait that is used as the bounds for what can be in a `kj_rs::Maybe`.
63+
///
64+
/// # Safety
65+
/// This trait should only be implemented from macro expansion and should
66+
/// never be manually implemented. An unsound implementation of this trait
67+
/// could result in undefined behavior when passed between languages.
68+
///
69+
/// This trait contains all behavior we need to implement `Maybe<T: MaybeItem>`
70+
/// for every `T` we use, and additionally determines the type layout of
71+
/// the `Maybe<T>`. The only information we can know about `T` comes from
72+
/// this trait, so it must be capable of handling all behavior we want in
73+
/// `kj_rs::Maybe`.
74+
///
75+
/// Every function without a default depends on `MaybeItem::Discriminant`
76+
/// and whether or not `T` implements [`HasNiche`]. Functions with defaults
77+
/// use those functions to implement shared behavior, and simplfy the actual
78+
/// `Maybe<T>` implementation.
79+
pub unsafe trait MaybeItem: Sized {
80+
type Discriminant: Copy;
81+
const NONE: Maybe<Self>;
82+
fn some(value: Self) -> Maybe<Self>;
83+
fn is_some(value: &Maybe<Self>) -> bool;
84+
fn is_none(value: &Maybe<Self>) -> bool;
85+
fn from_option(value: Option<Self>) -> Maybe<Self> {
86+
match value {
87+
None => <Self as MaybeItem>::NONE,
88+
Some(val) => <Self as MaybeItem>::some(val),
89+
}
90+
}
91+
fn drop_in_place(value: &mut Maybe<Self>) {
92+
if <Self as MaybeItem>::is_some(value) {
93+
unsafe {
94+
value.some.assume_init_drop();
95+
}
96+
}
97+
}
98+
}
99+
100+
/// Macro to implement [`MaybeItem`] for `T` which implment [`HasNiche`].
101+
/// Avoids running into generic specialization problems.
102+
macro_rules! impl_maybe_item_for_has_niche {
103+
($ty:ty) => {
104+
unsafe impl<T> MaybeItem for $ty {
105+
type Discriminant = ();
106+
107+
fn is_some(value: &Maybe<Self>) -> bool {
108+
!<$ty as HasNiche>::is_niche(value.some.as_ptr())
109+
}
110+
111+
fn is_none(value: &Maybe<Self>) -> bool {
112+
<$ty as HasNiche>::is_niche(value.some.as_ptr())
113+
}
114+
115+
const NONE: Maybe<Self> = {
116+
Maybe {
117+
is_set: (),
118+
some: MaybeUninit::zeroed(),
119+
}
120+
};
121+
122+
fn some(value: Self) -> Maybe<Self> {
123+
Maybe {
124+
is_set: (),
125+
some: MaybeUninit::new(value)
126+
}
127+
}
128+
}
129+
};
130+
($ty:ty, $($tail:ty),+) => {
131+
impl_maybe_item_for_has_niche!($ty);
132+
impl_maybe_item_for_has_niche!($($tail),*);
133+
};
134+
}
135+
136+
/// Macro to implement [`MaybeItem`] for primitives
137+
/// Avoids running into generic specialization problems.
138+
macro_rules! impl_maybe_item_for_primitive {
139+
($ty:ty) => {
140+
unsafe impl MaybeItem for $ty {
141+
type Discriminant = bool;
142+
143+
fn is_some(value: &Maybe<Self>) -> bool {
144+
value.is_set
145+
}
146+
147+
fn is_none(value: &Maybe<Self>) -> bool {
148+
!value.is_set
149+
}
150+
151+
const NONE: Maybe<Self> = {
152+
Maybe {
153+
is_set: false,
154+
some: MaybeUninit::uninit(),
155+
}
156+
};
157+
158+
fn some(value: Self) -> Maybe<Self> {
159+
Maybe {
160+
is_set: true,
161+
some: MaybeUninit::new(value)
162+
}
163+
}
164+
}
165+
};
166+
($ty:ty, $($tail:ty),+) => {
167+
impl_maybe_item_for_primitive!($ty);
168+
impl_maybe_item_for_primitive!($($tail),*);
169+
};
170+
}
171+
172+
impl_maybe_item_for_has_niche!(crate::Own<T>, &T, &mut T);
173+
impl_maybe_item_for_primitive!(
174+
u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, f32, f64, bool
175+
);
176+
177+
pub(crate) mod repr {
178+
use super::MaybeItem;
179+
use static_assertions::assert_eq_size;
180+
use std::fmt::Debug;
181+
use std::mem::MaybeUninit;
182+
183+
/// A [`Maybe`] represents bindings to the `kj::Maybe` class.
184+
/// It is an optional type, but represented using a struct, for alignment with kj.
185+
///
186+
/// # Layout
187+
/// In kj, `Maybe` has 3 specializations, one without niche value optimization, and
188+
/// two with it. In order to maintain an identical layout in Rust, we include an associated type
189+
/// in the [`MaybeItem`] trait, which determines the discriminant of the `Maybe<T: MaybeItem>`.
190+
///
191+
/// ## Niche Value Optimization
192+
/// This discriminant is used in tandem with the [`crate::maybe::HasNiche`] to implement
193+
/// [`MaybeItem`] properly for values which have a niche, which use a discriminant of [`()`],
194+
/// the unit type. All other types use [`bool`].
195+
#[repr(C)]
196+
pub struct Maybe<T: MaybeItem> {
197+
pub(super) is_set: T::Discriminant,
198+
pub(super) some: MaybeUninit<T>,
199+
}
200+
201+
assert_eq_size!(Maybe<isize>, [usize; 2]);
202+
assert_eq_size!(Maybe<&isize>, usize);
203+
assert_eq_size!(Maybe<crate::Own<isize>>, [usize; 2]);
204+
205+
impl<T: MaybeItem> Maybe<T> {
206+
/// # Safety
207+
/// This function shouldn't be used except by macro generation.
208+
pub unsafe fn is_set(&self) -> T::Discriminant {
209+
self.is_set
210+
}
211+
212+
/// # Safety
213+
/// This function shouldn't be used except by macro generation.
214+
#[inline]
215+
pub const unsafe fn from_parts_unchecked(
216+
is_set: T::Discriminant,
217+
some: MaybeUninit<T>,
218+
) -> Maybe<T> {
219+
Maybe { is_set, some }
220+
}
221+
222+
pub fn is_some(&self) -> bool {
223+
T::is_some(self)
224+
}
225+
226+
pub fn is_none(&self) -> bool {
227+
T::is_none(self)
228+
}
229+
230+
// # CONSTRUCTORS
231+
// These emulate Rust's enum api, which offers constructors for each variant.
232+
// This mean matching cases, syntax, and behavior.
233+
// The only place this may be an issue is pattern matching, which will not work,
234+
// but should produce an error.
235+
//
236+
// The following fails to compile:
237+
// ```{rust,compile_fail}
238+
// match maybe {
239+
// Maybe::Some(_) => ...,
240+
// Maybe::None => ...,
241+
// }
242+
// ```
243+
244+
/// The [`Maybe::Some`] function serves the same purpose as an enum constructor.
245+
///
246+
/// Constructing a `Maybe<T>::Some(val)` should only be possible with a valid
247+
/// instance of `T` from Rust.
248+
#[allow(non_snake_case)]
249+
pub fn Some(value: T) -> Maybe<T> {
250+
T::some(value)
251+
}
252+
253+
/// [`Maybe::None`] functions as a constructor for the none variant. It uses
254+
/// a `const` instead of a function to match syntax with normal Rust enums.
255+
///
256+
/// Constructing a `Maybe<T>::None` variant should always be possible from Rust.
257+
#[allow(non_upper_case_globals, dead_code)]
258+
pub const None: Maybe<T> = T::NONE;
259+
}
260+
261+
impl<T: MaybeItem> From<Maybe<T>> for Option<T> {
262+
fn from(value: Maybe<T>) -> Self {
263+
if value.is_some() {
264+
// We can't move out of value so we copy it and forget it in
265+
// order to perform a "manual" move out of value
266+
let ret = unsafe { Some(value.some.assume_init_read()) };
267+
std::mem::forget(value);
268+
ret
269+
} else {
270+
None
271+
}
272+
}
273+
}
274+
275+
impl<T: MaybeItem> From<Option<T>> for Maybe<T> {
276+
fn from(value: Option<T>) -> Self {
277+
<T as MaybeItem>::from_option(value)
278+
}
279+
}
280+
281+
impl<T: MaybeItem + Debug> Debug for Maybe<T> {
282+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283+
if self.is_none() {
284+
write!(f, "Maybe::None")
285+
} else {
286+
write!(f, "Maybe::Some({:?})", unsafe {
287+
self.some.assume_init_ref()
288+
})
289+
}
290+
}
291+
}
292+
293+
impl<T: MaybeItem> Default for Maybe<T> {
294+
fn default() -> Self {
295+
T::NONE
296+
}
297+
}
298+
299+
impl<T: MaybeItem> Drop for Maybe<T> {
300+
fn drop(&mut self) {
301+
T::drop_in_place(self);
302+
}
303+
}
304+
}

0 commit comments

Comments
 (0)