Skip to content

Add reference counted window handles#207

Open
notgull wants to merge 3 commits intomasterfrom
notgull/wakerification
Open

Add reference counted window handles#207
notgull wants to merge 3 commits intomasterfrom
notgull/wakerification

Conversation

@notgull
Copy link
Member

@notgull notgull commented Mar 1, 2026

Fixes #188

@MarijnS95
Copy link
Member

Probably opened in a hurry to compare, but do note (in the description) that this seems to be an alternative approach to #206 (is sort-of a duplicate)?

@notgull
Copy link
Member Author

notgull commented Mar 3, 2026

This is meant to be an implementation of "outer reference counting", as opposed to #206, which is inner reference counting.

@notgull notgull force-pushed the notgull/wakerification branch 2 times, most recently from 144f938 to ff925be Compare March 6, 2026 03:25
@notgull notgull marked this pull request as ready for review March 6, 2026 03:28
@notgull notgull requested review from kchibisov and madsmtm and removed request for madsmtm March 7, 2026 07:25
@madsmtm
Copy link
Member

madsmtm commented Mar 7, 2026

I feel like we've overcome the hard part in #188, deciding on the semantics (Reference-Counted Control), now it's "just" the syntax.

Something that I feel we need to at least consider here is: What is the value of OwnedWindowHandle with a vtable etc. over... simply exposing it as Arc<dyn HasWindowHandle>?

I imagine the answer to be "it allows the user to implement the reference-counting in a different manner" (including in C probably), i.e. we're not tied to Arc or Rc. Are there other benefits?

The downsides as I see from that is mainly that you cannot express Arc<dyn HasWindowHandle + Send + Sync> (so we need workarounds for that), and that you cannot express Rc<dyn HasWindowHandle> (so we need a separate type for that).

The natural question then becomes: Are the benefits worth it compared to the downsides?

src/owned.rs Outdated
/// This window handle is guaranteed to be `Send` and `Sync`. Meaning, its
/// internal mechaisms for refcounting are thread-safe.
#[derive(Debug, Clone)]
pub struct SyncWindowHandle {
Copy link
Member

Choose a reason for hiding this comment

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

After todays meeting we need to flip this.

Copy link
Member

Choose a reason for hiding this comment

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

Actually, after rethinking this, one problem that this poses is that some backends are not Send + Sync, e.g. Web. So maybe the default should be !Send + !Sync after all?

I believe in today meeting we were wrongly assuming that the WindowHandle is just a counter, but that's obviously not true. It still has to contain some state that is able to produce the BorrowedWindowHandle/RawWindowHandle.

Copy link
Member Author

Choose a reason for hiding this comment

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

I've removed SyncWindowHandle and made WindowHandle Send + Sync. We can have further discussion on if we want a thread-unsafe handle.

Copy link
Member

Choose a reason for hiding this comment

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

The thread-unsafe handle is required. Web is unable to create a WindowHandle at all otherwise.

@daxpedda
Copy link
Member

daxpedda commented Mar 7, 2026

I feel like we've overcome the hard part in #188, deciding on the semantics (Reference-Counted Control), now it's "just" the syntax.

Something that I feel we need to at least consider here is: What is the value of OwnedWindowHandle with a vtable etc. over... simply exposing it as Arc<dyn HasWindowHandle>?

I imagine the answer to be "it allows the user to implement the reference-counting in a different manner" (including in C probably), i.e. we're not tied to Arc or Rc. Are there other benefits?

The downsides as I see from that is mainly that you cannot express Arc<dyn HasWindowHandle + Send + Sync> (so we need workarounds for that), and that you cannot express Rc<dyn HasWindowHandle> (so we need a separate type for that).

The natural question then becomes: Are the benefits worth it compared to the downsides?

I believe the downsides are nullified/minimized in that we provide very convenient conversions in RWH. If there are other common cases out there we should consider adding conversion methods in RWH to cover them.

So in my eyes the current implementation retains maximum flexibility while at the same time keeping real-life scenarios as convenient as possible.

@madsmtm
Copy link
Member

madsmtm commented Mar 7, 2026

If C interop is the main motivator, then I think that can be solved with a custom struct CArc<T: ?Sized>( retain, release, *const T ) (where T can be dyn, it's just two pointers instead of one)?


I think the main difference is if we actually expose the owned handle as the return type anywhere in our API, which I think I'm somewhat proposing in #200 - then whatever handle we use there kinda has to be the maximally flexible one (or a generic?).

(To be clear, I'm in favour of a separate Owned*Handle type, I'm just playing devil's advocate a little bit to ensure that we've written down the reasoning for it).

@notgull notgull force-pushed the notgull/wakerification branch from ff925be to 2bfe01e Compare March 7, 2026 20:06
Renames the borrowed handle types to make more sense post-#188.

- WindowHandle -> BorrowedWindowHandle
- DisplayHandle -> BorrowedDisplayHandle
- HasWindowHandle -> AsWindowHandle
- HasDisplayHandle -> AsDisplayHandle

My opinion is that these types still have relevance in a post-#188
world. For example, in cases where you don't require a persistent
object, you could have an `fn(&impl AsWindowHandle)`. This would allow
using window handles without incrementing refcounts.

Signed-off-by: John Nunley <dev@notgull.net>
@notgull notgull force-pushed the notgull/wakerification branch 2 times, most recently from 0add791 to 1cd8bd3 Compare March 8, 2026 15:03
notgull added 2 commits March 8, 2026 08:04
This adds "WindowHandle" and "DisplayHandle" types as discussed in #188.
These act similar to `Waker`'s in stdlib, as they are a wrapper around a
trait object.

Signed-off-by: John Nunley <dev@notgull.net>
This adds two ways to construct owned handles:

- Using an `&'static impl AsWindowHandle`. This is simple because
  `retain` and `release` can be implemented as no-ops. This exists to
  make the case of a singleton no-op display simpler. Each handle
  provides a `WindowHandle::unavailable()` function to demonstrate this
  in action.
- Using a reference counted container. `WindowHandle` allows `Arc<T>`.
  This allows window handles to be constructed from ref-counted
  containers quite easily.

Both of these strategies allow for window handles building on existing
implementations to be implemented in entirely safe code.

Signed-off-by: John Nunley <dev@notgull.net>
@notgull notgull force-pushed the notgull/wakerification branch from 1cd8bd3 to 4b36004 Compare March 8, 2026 15:04
@notgull
Copy link
Member Author

notgull commented Mar 8, 2026

Something that I feel we need to at least consider here is: What is the value of OwnedWindowHandle with a vtable etc. over... simply exposing it as Arc<dyn HasWindowHandle>?

The VTable gives you more flexibility over how the underlying pointer acts. For example, see the from_static function, which allows for creating a window/display handle from an &'static impl AsWindowHandle. It can be cheaply retained and released, since the static reference will always be valid. In addition it also makes it easy to safely build no-op window handles.

assert_impl_all!(WindowHandle: Send, Sync);
assert_impl_all!(DisplayHandle: Send, Sync);
assert_impl_all!(WindowVtable: Send, Sync);
assert_impl_all!(DisplayVtable: Send, Sync);
Copy link
Member

Choose a reason for hiding this comment

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

Check for UnwindSafe + RefUnwindSafe + Unpin as well?
Or them not having it, the important part is that its cross-platform consistent.

Comment on lines +46 to +47
/// - The window handle returned by `get` must be valid for the lifetime of
/// this window handle.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/// - The window handle returned by `get` must be valid for the lifetime of
/// this window handle.
/// - The window handle returned by `window_handle()` must be valid for the lifetime of
/// this window handle.

Comment on lines +53 to +54
/// will be called `N+1` times. The underlying window must not be destroyed
/// as long as this reference is alive.
Copy link
Member

Choose a reason for hiding this comment

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

We have this part twice, once under # Safety and one more time here. I'm in favor of removing it from # Safety, as that should not be unsound (AFAIU).

Probably should be in its own paragraph here.

src/owned.rs Outdated
/// This window handle is guaranteed to be `Send` and `Sync`. Meaning, its
/// internal mechaisms for refcounting are thread-safe.
#[derive(Debug, Clone)]
pub struct SyncWindowHandle {
Copy link
Member

Choose a reason for hiding this comment

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

The thread-unsafe handle is required. Web is unable to create a WindowHandle at all otherwise.

BorrowedWindowHandle::borrow_raw(handle)
})
}
}
Copy link

Choose a reason for hiding this comment

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

I'm not 100% sure from reading this - but are you sure that the returned BorrowedWindowHandle actually borrows safely from the &self in a way that blocks the original handle from being dropped?

I have a nagging feeling that if you happen to have a &'static self (easily achieved with a Box or Arc around the owned::WindowHandle) then you will get back a BorrowedWindowHandle<'static> and you will have no constraints on being able to store that statically too but then you could Drop the original window handle and then the BorrowedWindowHandle could hold an invalid pointer.

I might be misreading this, but I've certainly been bitten by similar looking mistakes in the past so my spider senses are tingling here.

I feel like the &self lifetime should be named explicitly and there should be some PhantomData in the BorrowedWindowHandle to hold that lifetime - or something like that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Implement owned window handles using a Waker-type API

5 participants