Skip to content

Implement owned window handles using a Waker-type API #188

Description

@notgull

In today's discussion, we discussed moving raw-window-handle to have an API similar to the Waker API 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:

pub struct OwnedWindowHandle {
    data: NonNull<()>,
    vtable: &'static WindowHandleVTable
}

// OwnedWindowHandle is intentionally not Send and Sync.

pub struct WindowHandleVTable {
    // Decrease refcount.
    release: unsafe fn(NonNull<()>);
    // Increase refcount.
    retain: unsafe fn(NonNull<()>) -> Result<NonNull<()>, HandleError>;
    // Get the window handle.
    get_handle: unsafe fn(NonNull<()>) -> Result<WindowHandle<'_>, HandleError>;
}

impl OwnedWindowHandle {
    pub unsafe fn new(data: NonNull<()>, vtable: &'static WindowHandleVtable) -> Self { /* ... */ }
}

pub trait AsWindowHandle {
    fn as_handle(&self) -> Result<WindowHandle<'_>, HandleError>;
}

impl<T: AsWindowHandle + 'static + ?Sized> From<Rc<T>> for OwnedWindowHandle { /* ... */ }

pub struct ThreadSafeOwnedWindowHandle {
    inner: OwnedWindowHandle
}

unsafe impl Send for ThreadSafeOwnedWindowHandle {}
unsafe impl Sync for ThreadSafeOwnedWindowHandle {}

impl From<OwnedWindowHandle> for ThreadSafeOwnedWindowHandle { /* ... */ }
impl ThreadSafeOwnedWindowHandle {
    pub unsafe fn new(o: OwnedWindowHandle) -> Self { /* ... */ }
}

Effectively, rather than using HasRawWindowHandle, consumers would accept Into<OwnedWindowHandle> or Into<ThreadSafeOwnedWindowHandle>, depending on their requirements. Then, the consumer APIs could very easily just wrap around OwnedWindowHandle without imposing any lifetime or type requirements on their type. Meanwhile, on the producer, you could very easily implement impl From<&winit::Window> for OwnedWindowHandle to 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, like sdl. In addition, we lose the ability of windows to contain non-'static data.

See also #182.

Activity

  1. madsmtm commented on Feb 28, 2026

    @madsmtm
    Member

    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,
            )
        }
    }
  2. madsmtm commented on Feb 28, 2026

    @madsmtm
    Member

    I think we need to be clear on what exactly the advantage of OwnedWindowHandle over only exposing Rc<dyn AsWindowHandle> is?

    Is it that you can avoid an allocation, by e.g. wrapping objc2::rc::Retained<NSView> directly to OwnedWindowHandle, without wrapping it in Rc<Retained<NSView>>? If so, then we need to consider whether that's worth the complexity?

  3. madsmtm commented on Feb 28, 2026

    @madsmtm
    Member

    Another thing to consider: Should the handles weakly or strongly reference?

  4. madsmtm commented on Feb 28, 2026

    @madsmtm
    Member

    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<'_> and RawWindowHandle if you have a good OwnedWindowHandle).

    This would also semantically allow consumer crates to not need to store AppKit handles - users are free to deallocate winit::Window and call NSWindow.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).

  5. madsmtm commented on Mar 1, 2026

    @madsmtm
    Member

    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?

  6. madsmtm commented on Mar 1, 2026

    @madsmtm
    Member

    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 NSView doesn't keep NSWindow alive, 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.destroy would 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).

  7. madsmtm commented on Mar 1, 2026

    @madsmtm
    Member

    This 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::Window contains Rc<WindowContext>), but it does prevent a more Rusty windowing library from making windows take &mut to 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 OwnedWindowHandle around (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 OwnedWindowHandle isn'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 upon Drop.
    • 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: CALayer is thread-safe, and can be rendered into freely from another thread.
    • Win32: Rendering functionality on HWND can 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.

  8. madsmtm commented on Mar 1, 2026

    @madsmtm
    Member

    Additionally, option B seems like it'd work better on Android, since Winit's window != surface, so fundamentally, winit::Window already does not allow keeping ANativeWindow alive?

    Unless we allow get_handle to change the value it returns, then I guess option A might work? It'd need softbuffer to call get_handle all 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.

  9. madsmtm commented on Mar 1, 2026

    @madsmtm
    Member

    In 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).
  10. daxpedda commented on Mar 1, 2026

    @daxpedda
    Member

    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 Rc or Arc because 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 self or let users manage this themselves, e.g. RefCell<Window>.

    Send + Sync

    To 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 + Sync or 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 HasWindowHandle trait.

    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's File can be dropped, but errors would be ignored. Instead to properly close a File users need to call File::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 ✅ ❌ ✅ ❌
  11. daxpedda commented on Mar 1, 2026

    @daxpedda
    Member

    I 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.

  12. MarijnS95 commented on Mar 1, 2026

    @MarijnS95
    Member

    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?

  13. notgull commented on Mar 1, 2026

    @notgull
    ContributorAuthor

    @madsmtm

    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 underlying wl_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-handle version 1.0.0, our goal should be to never have another breaking change again. Having divergent callbacks and Clone implementations 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.destroy would be called, so you'd get an error next time you tried to render?

    wl_surface.destroy deallocates 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-backend allowing 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 &mut to e.g. change the title; everything in the windowing library has to use interior mutability, because you have to reference-count everything in it

    I 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 OwnedWindowHandle isn't actually thread-safe:

    In practice, I don't think this is an issue? In #207 specifically, I have a SyncWindowHandle that aims to represent an OwnedWindowHandle where 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 a SyncWindowHandle.

    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 CALayer in this analogy.

    • Win32: Rendering functionality on HWND can 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_handle to change the value it returns, then I guess option A might work? It'd need softbuffer to call get_handle all 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 acquire the Android window, this shouldn't be an issue.

  14. 16 remaining items

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions