Repository navigation
Implement owned window handles using a Waker-type API #188
Description
Activity
I'll add what I also noted in our meeting that this design can still be used by non-refcounting producer crates, it'll just "push" the unsafety to them:
impl WindowThatDoesNotReferenceCount { /// # Safety /// /// The handle must be dropped before `&self`. /// /// In a sense, there's an invisible `'self` lifetime on the /// `OwnedWindowHandle` returned from this function. pub unsafe fn create_handle(&self) -> OwnedWindowHandle { // These are intentional no-ops here, this producer does not support refcounting. unsafe fn retain(data: NonNull<c_void>) -> NonNull<()> { data } unsafe fn release(_data: NonNull<c_void>) {} unsafe fn get_handle(data: NonNull<c_void>) -> Result<WindowHandle<'_>, HandleError> { let this = unsafe { data.cast::<WindowThatDoesNotReferenceCount>().as_ref() }; Ok(WaylandHandle::new(this.surface.object_id_stuff().as_ptr().cast())) } OwnedWindowHandle::new( data: NonNull::new(self).cast(), retain, release, get_handle, ) } }
I think we need to be clear on what exactly the advantage of
OwnedWindowHandleover only exposingRc<dyn AsWindowHandle>is?Is it that you can avoid an allocation, by e.g. wrapping
objc2::rc::Retained<NSView>directly toOwnedWindowHandle, without wrapping it inRc<Retained<NSView>>? If so, then we need to consider whether that's worth the complexity?Another thing to consider: Should the handles weakly or strongly reference?
An alternative would be to push the refcounting into each struct instead:
// Basically `Retained<NSView>`. pub struct OwnedAppKitHandle { ns_view: NonNull<c_void>, retain: unsafe fn(NonNull<c_void>) -> NonNull<c_void>, release: unsafe fn(NonNull<c_void>), // No need for `get_handle` } impl OwnedAppKitHandle { // User guarantees the view and functions are safe. pub unsafe fn new(...) -> Self { ... } } pub struct OwnedWin32Handle { hwnd: *mut c_void, } impl OwnedWin32Handle { // Pointer is just an integer, it's not unsound if you pass a bogus value. pub fn new(hwnd: *mut c_void) -> Self { ... } } pub enum OwnedWindowHandle { AppKit(OwnedAppKitHandle), Win32(OwnedWin32Handle), // ... }
The advantage here could be that handles that are just integers wouldn't need any reference-counting.
Additionally, our total API surface could be minimized quite a lot (IMO there's not really a need for
WindowHandle<'_>andRawWindowHandleif you have a goodOwnedWindowHandle).This would also semantically allow consumer crates to not need to store AppKit handles - users are free to deallocate
winit::Windowand callNSWindow.close(),Retained<NSView>will still remain valid (at least I think so, though I should probably research this a bit).A disadvantage would be that this is somewhat in the "weak reference" category; the window can be closed while in use (we just prevent it from being unsound).
- added 2 commits that reference this issue
on Mar 1, 2026 Something we discussed in the meeting yesterday was that there's a few ways of doing reference-counted handles. I'll try to elaborate a bit more on that:
The first one, let's call it "outer" reference-counting, is to reference-count the entire thing that provides the
WindowHandle<'_>. For the purposes of this discussion, it is effectively:enum WindowHandle<'window> { Android(&'window ANativeWindow), AppKit(&'window NSView), Win32(HWND), Wayland(&'window wl_surface), // ... } struct OwnedWindowHandle(Arc<dyn Fn() -> WindowHandle<'_>>);
As a producer crate, you have two options for how to use this:
// Option A: Wrap your entire window state in an `Arc`. let window: Arc<Window> = unimplemented!(); let handle = OwnedWindowHandle(Arc::new(|| { let window = window; // handle stores `window` cfg_select! { target_os = "android" => WindowHandle::Android(&window.a_native_window), target_os = "macos" => WindowHandle::AppKit(&window.view), target_os = "windows" => WindowHandle::Win32(*window.hwnd), feature = "wayland" => WindowHandle::Wayland(&window.wl_surface), _ => todo!(), } })); // Option B: Wrap just the inner parts. let window: Window = unimplemented!(); let handle = OwnedWindowHandle(cfg_select! { target_os = "android" => { let a_native_window = window.a_native_window.clone(); Arc::new(|| WindowHandle::Android(&a_native_window)) // handle stores `ndk::NativeWindow` }, target_os = "macos" => { let view = window.view.retain(); Arc::new(|| WindowHandle::AppKit(&view)) // handle stores `Retained<NSView>` }, target_os = "windows" => { let hwnd = window.hwnd; Arc::new(|| WindowHandle::Win32(hwnd)) // handle stores `HWND` }, feature = "wayland" => { let wl_surface = window.wl_surface.clone(); Arc::new(|| WindowHandle::Wayland(&wl_surface)) // handle stores `WlSurface` }, _ => todo!(), });
The second way to model this, let's call it "inner" reference-counting, in a sense codifies option B. You can view it as:
enum OwnedWindowHandle { Android(NativeWindow), AppKit(Retained<NSView>), Win32(HWND), Wayland(Arc<dyn Fn() -> *mut wl_surface>), // ... }
As a producer crate, you then use it as:
// Option B: Wrap just the inner parts. let window: Window = unimplemented!(); let handle = cfg_select! { target_os = "android" => OwnedWindowHandle::Android(window.a_native_window.clone()), target_os = "macos" => OwnedWindowHandle::AppKit(window.view.retain()), target_os = "windows" => OwnedWindowHandle::Win32(*window.hwnd), feature = "wayland" => OwnedWindowHandle::Wayland(Arc::clone(window.wl_surface)), _ => todo!(), };
Hopefully you can see that outer reference-counting is in a sense more powerful than inner reference-counting, since as a producer, you can pick and choose between opton A and B.
I hope that makes sense so far?
An important thing is that the semantics are different between option A and B!
For option A, we also keep the entire window alive, whereas for option B, we don't have to. This matters in particular if you drop the Winit window before the surface.
Platform What happens with option B if you drop the window before the surface AppKit NSViewdoesn't keepNSWindowalive, so the window would deallocate. You can still render, it'll just never show up.UIKit Same as ^ Web Same as ^ Android Same as ^? Wayland Unsure? I think wl_surface.destroywould be called, so you'd get an error next time you tried to render?Windows The HWND becomes invalid, so you'd (probably?) get an error next time you render (or render into a new window if the ID was reused for a new one) X11 Same as ^? (Note that none of these are unsound, because the handle is still kept alive on the platforms where that matters).
Reacted by Marijn SuijtenThis seems like a platform-specific mess, so what is even the advantage of option B? Well, mostly that option A also has disadvantages!
For producers
First, for a producer crate to actually implement it, its window has to be reference-counted. This isn't really a problem for Winit (nor SDL,
sdl3::video::WindowcontainsRc<WindowContext>), but it does prevent a more Rusty windowing library from making windows take&mutto e.g. change the title; everything in the windowing library has to use interior mutability, because you have to reference-count everything in it. With option B, only the inner handle has to use reference-counting, the windowing crate's own state doesn't have to be touched.Concretely, in Winit's case it means an API like:
trait Window { fn owned_window_handle(self: Rc<Self>) -> OwnedWindowHandle; // ... }
For consumers
More importantly is the effect on consumer crates: With option A, the consumer has to keep
OwnedWindowHandlearound (at least to preserve option A semantics). This means that Softbuffer would have to look like this:struct Surface { window: OwnedWindowHandle, platform: platform::Surface, }
This has implications for thread-safety, as
OwnedWindowHandleisn't actually thread-safe:- AppKit/UIKit: Must be used on the main thread, including retain/release. Can be worked around with
libdispatch, by effectively blocking waiting for the main thread uponDrop. - Win32: Windows have "thread affinity"; certain functionality can only be used on the thread that the window was created on, including destroying the window, see
DestroyWindow. See also - Web: TODO.
- Others: Thread-safe?
With option B, we can instead do something like:
enum Surface { Android(NativeWindow), CoreGraphics(Retained<CALayer>), // AppKit + UIKit become this Win32(HWND), Wayland(Arc<dyn Fn() -> *mut wl_surface>), // Still has to store `WaylandWindowHandle`. // ... }
And that would be thread-safe (!) because:
- AppKit/UIKit:
CALayeris thread-safe, and can be rendered into freely from another thread. - Win32: Rendering functionality on
HWNDcan be used in a thread-safe manner (at least the DX11 stuff that wgpu uses can, unsure if Gdi can though). - Web: TODO.
See also #197.
- AppKit/UIKit: Must be used on the main thread, including retain/release. Can be worked around with
Additionally, option B seems like it'd work better on Android, since Winit's window != surface, so fundamentally,
winit::Windowalready does not allow keepingANativeWindowalive?Unless we allow
get_handleto change the value it returns, then I guess option A might work? It'd needsoftbufferto callget_handleall the time, and re-initialize it the window if it changes, which I'm not sure is the behavior we'd want?See also #200.
Reacted by Marijn SuijtenIn summary:
- If we choose to go with the semantics in option A, to wrap the entire window in
Rc/Arc, then Add reference counted handles #207 is the way to go. - If we choose to go with the semantics in option B, to only wrap the surface, then we can still go with that PR, but I think that Make handles reference-counted #206 would be the better choice (since it enforces those semantics).
- If we choose to go with the semantics in option A, to wrap the entire window in
Goals
Window Type Independence
The current setup relies on lifetimes. E.g. window handles encode the lifetime of the window to keep things sound. In practice, users just wrap windows in
RcorArcbecause self-referential structs are not a thing or because they want to enable multi-threading without scoped threads.Interior Mutability
For this to work in practive, windows have to use interior mutability. This doesn't just cause subtle bugs, but is not really representative of whats actually happening. The Rust way is to either take
&mut selfor let users manage this themselves, e.g.RefCell<Window>.Send + SyncTo allow off-thread rendering window types have to be
Send + Sync. But for most producer libraries with most backends this is not representative. E.g. Winit has put a great amount of effort into adding workarounds to make this happen. However, these workarounds hide the real cost from users, e.g.libdispatch. See rust-windowing/winit#3435.Off-Thread Surface Creation
To allow rendering on a different thread, we need a way to create a surface/swapchain on a different thread. In practice this means that window handles have to be
Send + Syncor be able to be created on a different thread. See #197.Window Drop Safety
On most backends, destroying the window while still rendering to it or holding a surface/swapchain is safe. On some other platforms windows are just reference counted and the surface/swapchain keeps the actual window alive. The last major holdup is Wayland, where its not clear just yet. See #182.
So in practice, this issue depends on the solution.
Window Destruction Control
The idea of the window handle keeping the window alive takes control away from the producer, preventing it from actually being able to close the window. Ideally the producer could at least detect it and either provide some feedback or not allow destruction until all references are dropped.
Consistent Handle
On some backends, when windows are destroyed and new ones created, the same handle could be re-used for a different window. In the worst case this could be seen as some sort of UB. In the base case it would be inconsistent behavior between backends. We have so far identified this to be possible on X11 and Wayland. Windows has a generation counter, even if that is an implementation detail (#1, #2, #3).
Cross-Backend Consistency
While backends can differ wildly in behavior and requirements, the whole point of RWH, Winit and Softbuffer is to provide a consistent cross-backend API to the user, while these libraries can handle all the backend-specific behavior. To the user, the behavior must be as predictable and consistent, as much as possible at least.
Solutions
Ownership
This is the current solution, with the only issue being window type dependence. The only to use this today without having to wrap the window type somehow is to not store thing in structs, otherwise creating self-referential structs, and for off-thread rendering use scoped threads.
We could consider keeping this solution around regardless of what other solution we end up with. The handle with the lifetime is the most accurate Rusty representation of what is actually going on even if its quite difficult to use because of Rusts ownership model. Notwithstanding the
HasWindowHandletrait.It should be noted that this is also the only solution which makes the transfer between producer and consumer as consistent as possible. While we can keep handles working after the window is destroyed, its unclear if we can keep the same behavior if the window is destroyed before the handle is successfully delivered to the consumer.
Reference-Counted Keep-Alive
The idea is if windows are destroyed, the handle will keep the window alive. While this gets rid of the lifetime, it does take control away from the producer.
But this can't work for all backends. E.g. on Windows the window can't just be kept alive by a handle, the message queue has to be handled otherwise many APIs simply don't work. Destroying a window on Windows also has to happen on the origin thread. So some backends will have to rely on the fact that windows are safe to destroy and APIs will just return errors.
Reference-Counted Control
The proposal is that consumers are responsible for preventing the window to be destroyed if a reference is still alive. In practice this might be designed as consumers increasing the reference counter through the window handle. Window destruction is then prevented on the consumer side until all references are released again.
This can be a bit awkward if window destruction relies on
Drop. However this is not without precedence, e.g. Std'sFilecan be dropped, but errors would be ignored. Instead to properly close aFileusers need to callFile::sync_all()first to handle errors.Reference-Counter Hybrid
For backends like Windows we could use a different model, like "Reference-Counted Control" while following other models with other backends. This lets us shift around pros and contras in favor of cross-platform consistency.
Comparison
Ownership RC Keep-Alive RC Control RC Hybrid Window Type Independence ❌ ✅ ✅ ✅ Off-Thread Surface Creation ✅ ✅ ✅ ✅ Window Destruction Control ✅ ❌ ✅ ❌ Consistent Handle ✅ ❌ ✅ ✅ Cross-Backend Consistency ✅ ❌ ✅ ❌ Reacted by Mads MarquartI want to keep collecting the current state and all our proposed solutions in this post and especially in that comparison table. Maybe opening a separate issue might be better. Please let me know if you want anything changed!
Based on this I'm in favor of the "RC Control" solution, which we briefly discussed in the last meeting. It seems to really cover all our bases.
I'm also generally concerned about how we transfer a window handle to the consumer without the lifetime. So maybe we will need some sort of borrowed to owned handle upgrade after all.
To allow rendering on a different thread, we need a way to create a surface/swapchain on a different thread.
Are you sure about this statement? On some platforms it seems that surface/swapchain creation must happen on the "main" thread or one associated with the window, but after that either the swapchain is thread-safe to be "rendered" (acquire next image, present) from multiple threads, or the "current backbuffer texture" is?
Another thing to consider: Should the handles weakly or strongly reference?
My take is that they need to be strong pointers. Specifically for Wayland and Android, they contain pointers to memory that need to be kept alive while they're used in C APIs. If the Wayland window can get
destroy()ed (which frees the underlyingwl_proxy) or the Android window can get deallocated, it would make it unsound to build these APIs on these primitives.Hopefully you can see that outer reference-counting is in a sense more powerful than inner reference-counting, since as a producer, you can pick and choose between opton A and B.
My personal preference is for outer reference counting. Once we release
raw-window-handleversion 1.0.0, our goal should be to never have another breaking change again. Having divergent callbacks andCloneimplementations for each window handle type seems like it could become a semver hazard. Although inner refcounting gives us finer granularity, I worry that this granularity could lead to forced breaking changes down the line.In addition, outer refcounting would integrate much more nicely with a C API.
Unsure? I think
wl_surface.destroywould be called, so you'd get an error next time you tried to render?wl_surface.destroydeallocates the underlying proxy. If this function is getting called at any point, I don't think Option B can be used safely for Wayland.There is also the issue of
wayland-backendallowing you to call destructors on Wayland proxies at any point in time, which is its own issue.it does prevent a more Rusty windowing library from making windows take
&mutto e.g. change the title; everything in the windowing library has to use interior mutability, because you have to reference-count everything in itI don't think so? If you wanted to, you could have something like this:
struct MyWindow { inner: Arc<wayland_client::Surface>, title: String, } impl From<&MyWindow> for rwh::OwnedWindowHandle { fn from(window: &MyWindow) -> Self { // Assumes wayland_client::Surface implements AsWindowHandle. Self::from(self.inner.clone()) } } impl MyWindow { fn set_title(&mut self, new_title: String) { self.title = new_title; self.inner.set_title(&self.title); } }
...where the required window state is kept inside of a refcounted container, and the mutable state can be kept separately.
This has implications for thread-safety, as
OwnedWindowHandleisn't actually thread-safe:In practice, I don't think this is an issue? In #207 specifically, I have a
SyncWindowHandlethat aims to represent anOwnedWindowHandlewhere the refcounting is guaranteed to be thread-safe. The goal being that a producer can be thread-safe or thread-unsafe, but then the consumer can decide which version it wants to go with. So in the provided example, if you wanted your producer implementation to be thread-safe, you would have aSyncWindowHandle.There is the overhead of having an additional copy of the window handle pointer. However, I think a strategy like this is required even if you go with Option B. You still need to create the
CALayerin this analogy.- Win32: Rendering functionality on
HWNDcan be used in a thread-safe manner (at least the DX11 stuff that wgpu uses can, unsure if Gdi can though).
GDI device contexts are thread unsafe, and have the same property as window handles where they can only be dropped on their origin threads.
Unless we allow
get_handleto change the value it returns, then I guess option A might work? It'd needsoftbufferto callget_handleall the time, and re-initialize it the window if it changes, which I'm not sure is the behavior we'd want?I think if we
acquirethe Android window, this shouldn't be an issue.- Win32: Rendering functionality on
16 remaining items
- added 9 commits that reference this issue
on Mar 15, 2026
In today's discussion, we discussed moving
raw-window-handleto have an API similar to theWakerAPI in the standard library. Effectively, we'd have a cheaply cloneable "owned" window handle with facilities to increase and decrease its refcount. It would be implemented like so:Effectively, rather than using
HasRawWindowHandle, consumers would acceptInto<OwnedWindowHandle>orInto<ThreadSafeOwnedWindowHandle>, depending on their requirements. Then, the consumer APIs could very easily just wrap aroundOwnedWindowHandlewithout imposing any lifetime or type requirements on their type. Meanwhile, on the producer, you could very easily implementimpl From<&winit::Window> for OwnedWindowHandleto use it with any graphics API.The cons are that we impose ref-counting behavior on all of our backends. This isn't a problem for
winit, but may be a problem for producers based on C, likesdl. In addition, we lose the ability of windows to contain non-'staticdata.See also #182.