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

Conversation

LordGoatius
Copy link

No description provided.

@LordGoatius LordGoatius force-pushed the jostler/2025-06-30-kj-maybe branch 6 times, most recently from e674fb2 to 2944364 Compare July 14, 2025 21:23
@LordGoatius LordGoatius marked this pull request as ready for review July 16, 2025 17:42
// Static var to return non-dangling pointer without heap allocating
int64_t var = 14;

// Returns a heap pointer

Choose a reason for hiding this comment

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

Nit: s/heap pointer/pointer-to-static/

kj-rs/maybe.rs Outdated
fn is_niche(value: &MaybeUninit<Self>) -> bool;
}

// In `kj`, null pointers are considered niche, and `kj::none` if null

Choose a reason for hiding this comment

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

It doesn't look like there's actually any specialization of Maybe<T*>. In fact, when I add this to kj/common-test.c++, it passes:

    Maybe<int*> m = static_cast<int*>(nullptr);
    KJ_EXPECT(m != kj::none);
    auto* p = KJ_ASSERT_NONNULL(m);
    KJ_EXPECT(p == nullptr);

(And yes, this is the opposite semantics of Maybe<Own<T>>, /sigh.)

FWIW, grepping for Maybe<[^>]+\*> in our codebase yields no results, so it looks like we don't actually use Maybe<T*> anywhere. Which makes sense, because we should probably be using Maybe<T&>, or a raw pointer, in such cases.

I wonder if we should just static_assert in our generated code that no one is using Maybe<T*>, with an error message suggesting the author use Maybe<T&>? That would avoid having to deal with this case.

Copy link
Author

Choose a reason for hiding this comment

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

A static assert is reasonable, and I can remove the Rust treatment of T* as special.

Copy link
Author

Choose a reason for hiding this comment

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

We can also change the check_type function to give an error here, and recommend Maybe<T&>

kj-rs/own.rs Outdated
}

/// This type allows me to expose the same API that [`NonNull`] does while maintaining the invariant

Choose a reason for hiding this comment

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

I feel like this sentence might have been cut off.

Copy link
Author

Choose a reason for hiding this comment

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

I think this was meant for the newtype I made to replace NonNull in the Own definition, it must've found its way there when I was shuffling things around for that.


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.

kj-rs/maybe.rs Outdated

impl_maybe_item_for_has_niche!(crate::Own<T>, &T, &mut T, *const T, *mut T);
impl_maybe_item_for_primitive!(
u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, usize, isize, bool

Choose a reason for hiding this comment

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

f32, f64, and char, too?

I guess () wouldn't be representable in C++ (Maybe<void>).

gen/src/write.rs Outdated
@@ -1669,6 +1676,17 @@ fn write_kj_own(out: &mut OutFile, key: NamedImplKey) {
);
}

// cxx boilerplate function for possible drop trait
// TODO: Add asserts and function generation

Choose a reason for hiding this comment

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

I mention elsewhere that perhaps an assertion could be that no one is actually using Maybe<T*>.

@@ -647,7 +650,7 @@ fn expand_cxx_function_shim(efn: &ExternFn, types: &Types) -> TokenStream {
let call = if indirect_return {
let ret = expand_extern_type(efn.ret.as_ref().unwrap(), types, true);
setup.extend(quote_spanned! {span=>
let mut __return = ::cxx::core::mem::MaybeUninit::<#ret>::uninit();
let mut __return = ::cxx::core::mem::MaybeUninit::<#ret>::zeroed();

Choose a reason for hiding this comment

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

This was related to the non-deterministic test failure you mentioned, right?

I think it'd be really good to understand this better, since it looks like this affects much more than just the kj::Maybe integration.

Copy link
Author

Choose a reason for hiding this comment

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

Yeah, this is the change I made that fixed the non-deterministic test failure. It's probably possible to scope this change to only happen if the return type is Maybe, if necessary. I would like to understand the cause of the bug better as well, because this could be unnecessary, but it's the only thing that fixed it, or even got close.

Copy link
Author

Choose a reason for hiding this comment

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

It's possible this was from the incorrect Maybe pointer code too. I'm testing that right now.

syntax/check.rs Outdated
}
Type::Ptr(_ptr) => {
return;
// TODO: ("Reminder to check pointer type later")

Choose a reason for hiding this comment

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

Reminder to make sure these TODOs are addressed. Should we be calling check_type_ptr() and check_type_ref()? I see those two functions do 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 think until we have a good reason to not follow cxx's rules around pointer/reference types using those functions is probably best, yeah.

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?

fn test_some_ptr() {
let maybe = ffi::return_maybe_ptr_some();
assert!(maybe.is_some());
assert!(!maybe.is_none());

Choose a reason for hiding this comment

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

Can we also assert that the pointed-to-value is indeed 14? I have a suspicion it won't be, due to my observation that there's no actual Maybe<T*> specialization.

Copy link
Author

Choose a reason for hiding this comment

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

Yeah, that's definitely a good idea.

@LordGoatius LordGoatius force-pushed the jostler/2025-06-30-kj-maybe branch from 2944364 to 3990b31 Compare July 18, 2025 19:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants