From c1f94f929c59e864ee2a33948e75c43411391705 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 1 Mar 2026 00:10:34 +0100 Subject: [PATCH 1/4] Remove Copy impl on top-level handles Not an operation that can be supported when we reference-count these. --- CHANGELOG.md | 1 + src/borrowed.rs | 4 ++-- src/lib.rs | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cdfa87..4d2d5d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - **Breaking:** Remove deprecated `HasRawWindowHandle` and `HasRawDisplayHandle` traits. - **Breaking:** Remove `UiKitWindowHandle::ui_view_controller` field, retrieve this from the UIView's responder chain instead. - **Breaking:** Merge `RawWindowHandle` and `WindowHandle<'_>`. +- **Breaking:** Remove `Copy` impl on `WindowHandle<'_>` and `DisplayHandle<'_>`. * Improve documentation on AppKit and UIKit handles. ## 0.6.2 (2024-05-17) diff --git a/src/borrowed.rs b/src/borrowed.rs index 684746e..8dcaee8 100644 --- a/src/borrowed.rs +++ b/src/borrowed.rs @@ -67,7 +67,7 @@ impl HasDisplayHandle for alloc::sync::Arc { impl<'a> HasDisplayHandle for DisplayHandle<'a> { fn display_handle(&self) -> Result, HandleError> { - Ok(*self) + Ok(self.clone()) } } @@ -133,6 +133,6 @@ impl HasWindowHandle for alloc::sync::Arc { impl HasWindowHandle for WindowHandle<'_> { fn window_handle(&self) -> Result { - Ok(*self) + Ok(self.clone()) } } diff --git a/src/lib.rs b/src/lib.rs index 31483d1..d51f4c0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -84,7 +84,7 @@ use core::fmt; /// [`WindowHandle::Xlib`] on macOS, it would just be weird, and probably /// requires something like XQuartz be used). #[non_exhaustive] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum WindowHandle<'window> { /// A raw window handle for UIKit (Apple's non-macOS windowing library). /// @@ -206,7 +206,7 @@ pub enum WindowHandle<'window> { /// [`RawDisplayHandle::Xlib`] on macOS, it would just be weird, and probably /// requires something like XQuartz be used). #[non_exhaustive] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum DisplayHandle<'display> { /// A raw display handle for UIKit (Apple's non-macOS windowing library). /// From 52cb367946bb9f7775f9cfade7ad94802670d59d Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 1 Mar 2026 02:25:26 +0100 Subject: [PATCH 2/4] Make AppKit and UIKit handles reference-counted --- Cargo.toml | 19 +++++ src/appkit.rs | 192 +++++++++++++++++++++++++++++++++++++++++++------- src/lib.rs | 8 +-- src/uikit.rs | 178 +++++++++++++++++++++++++++++++++++++++------- 4 files changed, 341 insertions(+), 56 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9644270..f34f136 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,25 @@ features = ["HtmlCanvasElement", "OffscreenCanvas", "Window", "Document"] [target.'cfg(target_family = "wasm")'.dev-dependencies] wasm-bindgen-test = "0.3" +[target.'cfg(target_vendor = "apple")'.dev-dependencies] +objc2 = "0.6.4" + +[target.'cfg(all(target_vendor = "apple", not(target_os = "macos")))'.dev-dependencies] +objc2-ui-kit = { version = "0.3.2", default-features = false, features = [ + "std", + "UIResponder", + "UIView", + "UIWindow", +] } + +[target.'cfg(target_os = "macos")'.dev-dependencies] +objc2-app-kit = { version = "0.3.2", default-features = false, features = [ + "std", + "NSResponder", + "NSView", + "NSWindow", +] } + [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] diff --git a/src/appkit.rs b/src/appkit.rs index 2527e24..74ce151 100644 --- a/src/appkit.rs +++ b/src/appkit.rs @@ -1,6 +1,7 @@ use core::ffi::c_void; -use core::marker::PhantomData; +use core::mem::ManuallyDrop; use core::ptr::NonNull; +use core::{fmt, hash}; use super::DisplayHandle; @@ -50,75 +51,214 @@ impl DisplayHandle<'static> { /// Getting the view from a [`WindowHandle`][crate::WindowHandle]. /// /// ```no_run -/// # fn inner() { +/// # fn main() { /// #![cfg(target_os = "macos")] -/// # #[cfg(requires_objc2)] /// use objc2::MainThreadMarker; -/// # #[cfg(requires_objc2)] /// use objc2::rc::Retained; -/// # #[cfg(requires_objc2)] /// use objc2_app_kit::NSView; /// use raw_window_handle::WindowHandle; /// -/// let handle: WindowHandle<'_>; // Get the window handle from somewhere else +/// let handle: WindowHandle<'_>; // Get the window handle from somewhere /// # handle = unimplemented!(); /// match handle { -/// # #[cfg(requires_objc2)] /// WindowHandle::AppKit(handle) => { /// assert!(MainThreadMarker::new().is_some(), "can only access AppKit handles on the main thread"); -/// let ns_view = handle.ns_view().as_ptr(); -/// // SAFETY: The pointer came from `WindowHandle`, which ensures -/// // that the `AppKitWindowHandle` contains a valid pointer to an -/// // `NSView`. +/// let ns_view = handle.into_ns_view().cast::().as_ptr(); +/// // SAFETY: The pointer is valid, and has +1 retain count from above. /// // Unwrap is fine, since the pointer came from `NonNull`. -/// let ns_view: Retained = unsafe { Retained::retain(ns_view.cast()) }.unwrap(); +/// let ns_view = unsafe { Retained::from_raw(ns_view) }.unwrap(); /// // Do something with the NSView here, like getting the `NSWindow` /// let ns_window = ns_view.window().expect("view was not installed in a window"); /// } /// handle => unreachable!("unknown handle {handle:?} for platform"), /// } /// # } +/// # +/// # #[cfg(not(target_os = "macos"))] +/// # fn main() {} /// ``` -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct AppKitWindowHandle<'window> { +pub struct AppKitWindowHandle { ns_view: NonNull, - _marker: PhantomData<&'window ()>, + // objc_retain + retain: unsafe extern "C-unwind" fn(ns_view: *mut c_void) -> *mut c_void, + // objc_release + release: unsafe extern "C-unwind" fn(ns_view: *mut c_void), +} + +impl Clone for AppKitWindowHandle { + #[inline] + fn clone(&self) -> Self { + // SAFETY: The view pointer is guaranteed to be valid. + let ns_view = unsafe { (self.retain)(self.ns_view.as_ptr()) }; + Self { + ns_view: NonNull::new(ns_view).expect("retain returned NULL pointer"), + retain: self.retain, + release: self.release, + } + } } -impl AppKitWindowHandle<'_> { +impl Drop for AppKitWindowHandle { + #[inline] + fn drop(&mut self) { + // SAFETY: The view pointer is guaranteed to be valid. + unsafe { (self.release)(self.ns_view.as_ptr()) } + } +} + +impl AppKitWindowHandle { /// Create a new handle to a view. /// /// # Safety /// - /// `ns_view` must be a valid pointer to a `NSView`, and must remain valid for the lifetime of - /// this type. + /// `ns_view` must be a valid pointer to a `NSView` with +1 retain count, and the function + /// pointers must correctly increase / decrease the retain count of the view. /// /// # Example /// - /// Create a handle from the content view of a `NSWindow`. + /// Create a handle from the content view of a `NSWindow` using `objc2`. /// - /// ```ignore + /// ``` + /// # fn main() { + /// #![cfg(target_os = "macos")] /// use std::ptr::NonNull; + /// use std::ffi::c_void; /// use objc2::rc::Retained; /// use objc2_app_kit::{NSWindow, NSView}; /// use raw_window_handle::AppKitWindowHandle; /// - /// let ns_window: Retained = ...; - /// let ns_view: Retained = window.contentView(); - /// let ns_view: NonNull = NonNull::from(&*ns_view); - /// let handle = unsafe { AppKitWindowHandle::new(ns_view.cast()) }; + /// // NSWindow gotten from somewhere. + /// let window: Retained; + /// # window = unsafe { objc2_app_kit::NSWindow::new(objc2::MainThreadMarker::new().unwrap()) }; + /// + /// // Use the window's content view. + /// let ns_view = window.contentView().unwrap(); + /// + /// // Helper functions to retain/release the view. + /// unsafe extern "C-unwind" fn retain(ns_view: *mut c_void) -> *mut c_void { + /// // SAFETY: Upheld by the caller that the pointer is a `NSView`. + /// // Unwrapping is fine, the pointer should be non-null. + /// let ns_view = unsafe { Retained::retain(ns_view.cast::()) }.unwrap(); + /// Retained::into_raw(ns_view).cast::() + /// } + /// unsafe extern "C-unwind" fn release(ns_view: *mut c_void) { + /// // SAFETY: Upheld by the caller that the pointer is a `NSView`. + /// // Unwrapping is fine, the pointer should be non-null. + /// let _ = unsafe { Retained::from_raw(ns_view.cast::()) }.unwrap(); + /// } + /// + /// // Pass +1 retain count. + /// let ns_view: NonNull = NonNull::new(Retained::into_raw(ns_view)).unwrap().cast(); + /// + /// // SAFETY: The view is valid and has +1 retain count, and the function pointers are correct. + /// let handle = unsafe { AppKitWindowHandle::new(ns_view, retain, release) }; + /// + /// // Handle can be cloned, which bumps the reference-count. + /// let handle2 = handle.clone(); + /// # } + /// # + /// # #[cfg(not(target_os = "macos"))] + /// # fn main() {} /// ``` - pub unsafe fn new(ns_view: NonNull) -> Self { + /// + /// Create a handle from an unretained `NSWindow` pointer you have from somewhere else, without + /// dependencies. + /// + /// ``` + /// # fn main() { + /// #![cfg(target_os = "macos")] + /// use std::ptr::NonNull; + /// use std::ffi::c_void; + /// use raw_window_handle::AppKitWindowHandle; + /// + /// // Link directly to the Objective-C runtime functions. + /// #[link(name = "objc", kind = "dylib")] + /// unsafe extern "C-unwind" { + /// fn objc_retain(obj: *mut c_void) -> *mut c_void; + /// fn objc_release(obj: *mut c_void); + /// } + /// + /// // NSView pointer gotten from somewhere. + /// let ns_view: NonNull; + /// # let view = unsafe { objc2_app_kit::NSView::new(objc2::MainThreadMarker::new().unwrap()) }; + /// # ns_view = NonNull::from(&*view).cast(); + /// + /// // Increase the reference-count of the view. + /// let ns_view = NonNull::new(unsafe { objc_retain(ns_view.as_ptr()) }).unwrap(); + /// + /// // SAFETY: The view is valid and has +1 retain count, and the function pointers are correct. + /// let handle = unsafe { AppKitWindowHandle::new(ns_view, objc_retain, objc_release) }; + /// + /// // Handle can be cloned, which bumps the reference-count. + /// let handle2 = handle.clone(); + /// # } + /// # + /// # #[cfg(not(target_os = "macos"))] + /// # fn main() {} + /// ``` + #[inline] + pub unsafe fn new( + ns_view: NonNull, + retain: unsafe extern "C-unwind" fn(ns_view: *mut c_void) -> *mut c_void, + release: unsafe extern "C-unwind" fn(ns_view: *mut c_void), + ) -> Self { Self { ns_view, - _marker: PhantomData, + retain, + release, } } + /// TODO: Should we expose platform-specific methods like this? + #[cfg(target_vendor = "apple")] + pub unsafe fn new2(ns_view: NonNull) -> Self { + #[link(name = "objc", kind = "dylib")] + unsafe extern "C-unwind" { + fn objc_retain(obj: *mut c_void) -> *mut c_void; + fn objc_release(obj: *mut c_void); + } + + unsafe { Self::new(ns_view, objc_retain, objc_release) } + } + /// A pointer to an `NSView` object. /// /// The pointer is guaranteed to be valid for at least as long as `self`. + #[inline] pub fn ns_view(&self) -> NonNull { self.ns_view } + + /// A retained pointer to an `NSView` object. + /// + /// The pointer has +1 retain count, and should be released by the caller. + #[inline] + pub fn into_ns_view(self) -> NonNull { + // Pass +1 retain count to the caller. + ManuallyDrop::new(self).ns_view + } +} + +impl PartialEq for AppKitWindowHandle { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.ns_view == other.ns_view + } +} + +impl Eq for AppKitWindowHandle {} + +impl hash::Hash for AppKitWindowHandle { + #[inline] + fn hash(&self, state: &mut H) { + self.ns_view.hash(state); + } +} + +impl fmt::Debug for AppKitWindowHandle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("AppKitWindowHandle") + .field("ns_view", &self.ns_view) + .finish() + } } diff --git a/src/lib.rs b/src/lib.rs index d51f4c0..7d8cf85 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -94,13 +94,13 @@ pub enum WindowHandle<'window> { /// /// Note that Mac Catalyst (`$arch-apple-ios-macabi` targets), can use /// UIKit *or* AppKit. - UiKit(UiKitWindowHandle<'window>), + UiKit(UiKitWindowHandle), /// A raw window handle for AppKit. /// /// ## Availability Hints /// This variant is used on macOS, although Mac Catalyst can also use it /// despite being `target_os = "ios"`. - AppKit(AppKitWindowHandle<'window>), + AppKit(AppKitWindowHandle), /// A raw window handle for the Redox operating system. /// /// ## Availability Hints @@ -358,8 +358,8 @@ from_impl!(DisplayHandle, Web, WebDisplayHandle); from_impl!(DisplayHandle, Android, AndroidDisplayHandle); from_impl!(DisplayHandle, Haiku, HaikuDisplayHandle); -from_impl!(WindowHandle, UiKit, UiKitWindowHandle<'a>); -from_impl!(WindowHandle, AppKit, AppKitWindowHandle<'a>); +from_impl!(WindowHandle, UiKit, UiKitWindowHandle); +from_impl!(WindowHandle, AppKit, AppKitWindowHandle); from_impl!(WindowHandle, Orbital, OrbitalWindowHandle<'a>); from_impl!(WindowHandle, OhosNdk, OhosNdkWindowHandle<'a>); from_impl!(WindowHandle, Xlib, XlibWindowHandle); diff --git a/src/uikit.rs b/src/uikit.rs index bbe61ae..aa79495 100644 --- a/src/uikit.rs +++ b/src/uikit.rs @@ -1,6 +1,7 @@ use core::ffi::c_void; -use core::marker::PhantomData; +use core::mem::ManuallyDrop; use core::ptr::NonNull; +use core::{fmt, hash}; use super::DisplayHandle; @@ -50,33 +51,30 @@ impl DisplayHandle<'static> { /// Getting the view from a [`WindowHandle`][crate::WindowHandle]. /// /// ```no_run -/// # fn inner() { -/// #![cfg(any(target_os = "ios", target_os = "tvos", target_os = "watchos", target_os = "xros"))] -/// # #[cfg(requires_objc2)] +/// # fn main() { +/// #![cfg(all(target_vendor = "apple", not(target_os = "macos")))] /// use objc2::MainThreadMarker; -/// # #[cfg(requires_objc2)] /// use objc2::rc::Retained; -/// # #[cfg(requires_objc2)] /// use objc2_ui_kit::UIView; /// use raw_window_handle::WindowHandle; /// /// let handle: WindowHandle<'_>; // Get the window handle from somewhere else /// # handle = unimplemented!(); /// match handle { -/// # #[cfg(requires_objc2)] -/// WindowHandle::UIKit(handle) => { +/// WindowHandle::UiKit(handle) => { /// assert!(MainThreadMarker::new().is_some(), "can only access UIKit handles on the main thread"); -/// let ui_view = handle.ui_view().as_ptr(); -/// // SAFETY: The pointer came from `WindowHandle`, which ensures -/// // that the `UiKitWindowHandle` contains a valid pointer to an -/// // `UIView`. +/// let ui_view = handle.into_ui_view().cast::().as_ptr(); +/// // SAFETY: The pointer is valid, and has +1 retain count from above. /// // Unwrap is fine, since the pointer came from `NonNull`. -/// let ui_view: Retained = unsafe { Retained::retain(ui_view.cast()) }.unwrap(); +/// let ui_view = unsafe { Retained::from_raw(ui_view) }.unwrap(); /// // Do something with the UIView here. /// } /// handle => unreachable!("unknown handle {handle:?} for platform"), /// } /// # } +/// # +/// # #[cfg(not(all(target_vendor = "apple", not(target_os = "macos"))))] +/// # fn main() {} /// ``` /// /// Get a pointer to an `UIViewController` object by traversing the `UIView`'s responder chain: @@ -102,45 +100,173 @@ impl DisplayHandle<'static> { /// /// // Use found_controller here. /// ``` -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct UiKitWindowHandle<'window> { +pub struct UiKitWindowHandle { ui_view: NonNull, - _marker: PhantomData<&'window ()>, + // objc_retain + retain: unsafe extern "C-unwind" fn(ui_view: *mut c_void) -> *mut c_void, + // objc_release + release: unsafe extern "C-unwind" fn(ui_view: *mut c_void), +} + +impl Clone for UiKitWindowHandle { + #[inline] + fn clone(&self) -> Self { + // SAFETY: The view pointer is guaranteed to be valid. + let ui_view = unsafe { (self.retain)(self.ui_view.as_ptr()) }; + Self { + ui_view: NonNull::new(ui_view).expect("retain returned NULL pointer"), + retain: self.retain, + release: self.release, + } + } } -impl UiKitWindowHandle<'_> { +impl Drop for UiKitWindowHandle { + #[inline] + fn drop(&mut self) { + // SAFETY: The view pointer is guaranteed to be valid. + unsafe { (self.release)(self.ui_view.as_ptr()) } + } +} + +impl UiKitWindowHandle { /// Create a new handle to a view. /// /// # Safety /// - /// `ui_view` must be a valid pointer to a `UIView`, and must remain valid for the lifetime of - /// this type. + /// `ui_view` must be a valid pointer to a `UIView` with +1 retain count, and the function + /// pointers must correctly increase / decrease the retain count of the view. /// /// # Example /// - /// Create a handle from a `UIView`. + /// Create a handle from a `UIView` using `objc2`. /// - /// ```ignore + /// ``` + /// # fn main() { + /// #![cfg(all(target_vendor = "apple", not(target_os = "macos")))] /// use std::ptr::NonNull; + /// use std::ffi::c_void; /// use objc2::rc::Retained; /// use objc2_ui_kit::UIView; /// use raw_window_handle::UiKitWindowHandle; /// - /// let ui_view: Retained = ...; - /// let ui_view: NonNull = NonNull::from(&*ui_view); - /// let handle = unsafe { UiKitWindowHandle::new(ui_view.cast()) }; + /// // UIView gotten from somewhere. + /// let view: Retained; + /// # view = unsafe { objc2_ui_kit::UIView::new(objc2::MainThreadMarker::new().unwrap()) }; + /// + /// // Helper functions to retain/release the view. + /// unsafe extern "C-unwind" fn retain(ui_view: *mut c_void) -> *mut c_void { + /// // SAFETY: Upheld by the caller that the pointer is a `UIView`. + /// // Unwrapping is fine, the pointer should be non-null. + /// let ui_view = unsafe { Retained::retain(ui_view.cast::()) }.unwrap(); + /// Retained::into_raw(ui_view).cast::() + /// } + /// unsafe extern "C-unwind" fn release(ui_view: *mut c_void) { + /// // SAFETY: Upheld by the caller that the pointer is a `UIView`. + /// // Unwrapping is fine, the pointer should be non-null. + /// let _ = unsafe { Retained::from_raw(ui_view.cast::()) }.unwrap(); + /// } + /// + /// // Pass +1 retain count. + /// let ui_view: NonNull = NonNull::new(Retained::into_raw(view)).unwrap().cast(); + /// + /// // SAFETY: The view is valid and has +1 retain count, and the function pointers are correct. + /// let handle = unsafe { UiKitWindowHandle::new(ui_view, retain, release) }; + /// + /// // Handle can be cloned, which bumps the reference-count. + /// let handle2 = handle.clone(); + /// # } + /// # + /// # #[cfg(not(all(target_vendor = "apple", not(target_os = "macos"))))] + /// # fn main() {} + /// ``` + /// + /// Create a handle from an unretained `UIWindow` pointer you have from somewhere else, without + /// dependencies. + /// + /// ``` + /// # fn main() { + /// #![cfg(all(target_vendor = "apple", not(target_os = "macos")))] + /// use std::ptr::NonNull; + /// use std::ffi::c_void; + /// use raw_window_handle::UiKitWindowHandle; + /// + /// // Link directly to the Objective-C runtime functions. + /// #[link(name = "objc", kind = "dylib")] + /// unsafe extern "C-unwind" { + /// fn objc_retain(obj: *mut c_void) -> *mut c_void; + /// fn objc_release(obj: *mut c_void); + /// } + /// + /// // UIView pointer gotten from somewhere. + /// let ui_view: NonNull; + /// # let view = unsafe { objc2_ui_kit::UIView::new(objc2::MainThreadMarker::new().unwrap()) }; + /// # ui_view = NonNull::from(&*view).cast(); + /// + /// // Increase the reference-count of the view. + /// let ui_view = NonNull::new(unsafe { objc_retain(ui_view.as_ptr()) }).unwrap(); + /// + /// // SAFETY: The view is valid and has +1 retain count, and the function pointers are correct. + /// let handle = unsafe { UiKitWindowHandle::new(ui_view, objc_retain, objc_release) }; + /// + /// // Handle can be cloned, which bumps the reference-count. + /// let handle2 = handle.clone(); + /// # } + /// # + /// # #[cfg(not(all(target_vendor = "apple", not(target_os = "macos"))))] + /// # fn main() {} /// ``` - pub unsafe fn new(ui_view: NonNull) -> Self { + #[inline] + pub unsafe fn new( + ui_view: NonNull, + retain: unsafe extern "C-unwind" fn(ui_view: *mut c_void) -> *mut c_void, + release: unsafe extern "C-unwind" fn(ui_view: *mut c_void), + ) -> Self { Self { ui_view, - _marker: PhantomData, + retain, + release, } } /// A pointer to an `UIView` object. /// /// The pointer is guaranteed to be valid for at least as long as `self`. + #[inline] pub fn ui_view(&self) -> NonNull { self.ui_view } + + /// A retained pointer to an `UIView` object. + /// + /// The pointer has +1 retain count, and should be released by the caller. + #[inline] + pub fn into_ui_view(self) -> NonNull { + // Pass +1 retain count to the caller. + ManuallyDrop::new(self).ui_view + } +} + +impl PartialEq for UiKitWindowHandle { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.ui_view == other.ui_view + } +} + +impl Eq for UiKitWindowHandle {} + +impl hash::Hash for UiKitWindowHandle { + #[inline] + fn hash(&self, state: &mut H) { + self.ui_view.hash(state); + } +} + +impl fmt::Debug for UiKitWindowHandle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("UiKitWindowHandle") + .field("ui_view", &self.ui_view) + .finish() + } } From 09ae1786d93dcf67f3842b4ac40b358153394ac5 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 1 Mar 2026 02:08:30 +0100 Subject: [PATCH 3/4] Make the Android handle reference-counted --- Cargo.toml | 3 ++ src/android.rs | 135 ++++++++++++++++++++++++++++++++++++++++--------- src/lib.rs | 4 +- 3 files changed, 117 insertions(+), 25 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f34f136..e85bbd8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,9 @@ objc2-app-kit = { version = "0.3.2", default-features = false, features = [ "NSWindow", ] } +[target.'cfg(target_os = "android")'.dev-dependencies] +ndk = "0.9.0" + [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] diff --git a/src/android.rs b/src/android.rs index bd76b4f..f4c3779 100644 --- a/src/android.rs +++ b/src/android.rs @@ -1,6 +1,6 @@ use core::ffi::c_void; -use core::marker::PhantomData; use core::ptr::NonNull; +use core::{fmt, hash}; use super::DisplayHandle; @@ -40,35 +40,92 @@ impl DisplayHandle<'static> { } /// Raw window handle for Android NDK. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct AndroidNdkWindowHandle<'window> { +pub struct AndroidNdkWindowHandle { a_native_window: NonNull, - _marker: PhantomData<&'window ()>, + // ANativeWindow_acquire + acquire: unsafe extern "C" fn(a_native_window: NonNull), + // ANativeWindow_release + release: unsafe extern "C" fn(a_native_window: NonNull), +} + +impl Clone for AndroidNdkWindowHandle { + #[inline] + fn clone(&self) -> Self { + // SAFETY: The window pointer is valid. + unsafe { (self.acquire)(self.a_native_window) }; + Self { + a_native_window: self.a_native_window, + acquire: self.acquire, + release: self.release, + } + } } -impl AndroidNdkWindowHandle<'_> { +impl Drop for AndroidNdkWindowHandle { + #[inline] + fn drop(&mut self) { + // SAFETY: The window pointer is valid. + unsafe { (self.release)(self.a_native_window) }; + } +} + +impl AndroidNdkWindowHandle { /// Create a new handle to an `ANativeWindow`. /// /// # Safety /// - /// `a_native_window` must be a valid pointer to a `ANativeWindow`, and must remain valid for - /// the lifetime of this type. + /// `a_native_window` must be a valid pointer to a `ANativeWindow`, and the given function + /// pointers must correctly acquire / release the window. + /// + /// This function takes ownership of the pointer. /// /// # Example /// + /// Create a handle using the `ndk` crate. + /// /// ``` - /// # use core::ptr::NonNull; - /// # use raw_window_handle::AndroidNdkWindowHandle; - /// # type ANativeWindow = (); - /// # - /// let ptr: NonNull; - /// # ptr = NonNull::from(&()); - /// let handle = unsafe { AndroidNdkWindowHandle::new(ptr.cast()) }; + /// # fn inner() { + /// #![cfg(target_os = "android")] + /// use std::ffi::c_void; + /// use std::mem::{self, ManuallyDrop}; + /// use std::ptr::NonNull; + /// use ndk::native_window::NativeWindow; + /// use raw_window_handle::AndroidNdkWindowHandle; + /// + /// // Window gotten from somewhere (for example using `android-activity`). + /// let window: NativeWindow; + /// window = unimplemented!(); + /// + /// // Helper functions to acquire/release the window. + /// unsafe extern "C" fn acquire(a_native_window: NonNull) { + /// // SAFETY: Upheld by the caller that the pointer is a `ANativeWindow`. + /// mem::forget(unsafe { NativeWindow::clone_from_ptr(a_native_window.cast()) }); + /// } + /// unsafe extern "C" fn release(a_native_window: NonNull) { + /// // SAFETY: Upheld by the caller that the pointer is a `ANativeWindow`. + /// let _ = unsafe { NativeWindow::from_ptr(a_native_window.cast()) }; + /// } + /// + /// // Pass reference count to `AndroidNdkWindowHandle`. + /// let a_native_window: NonNull = ManuallyDrop::new(window).ptr().cast(); + /// + /// // SAFETY: The window is valid and the function pointers are correct. + /// let handle = unsafe { AndroidNdkWindowHandle::new(a_native_window, acquire, release) }; + /// + /// // Handle can be cloned, which acquires it, and releases it when dropped. + /// let handle2 = handle.clone(); + /// # } /// ``` - pub unsafe fn new(a_native_window: NonNull) -> Self { + #[inline] + pub unsafe fn new( + a_native_window: NonNull, + acquire: unsafe extern "C" fn(a_native_window: NonNull), + release: unsafe extern "C" fn(a_native_window: NonNull), + ) -> Self { Self { a_native_window, - _marker: PhantomData, + acquire, + release, } } @@ -79,15 +136,47 @@ impl AndroidNdkWindowHandle<'_> { /// # Example /// /// ``` - /// # use core::ptr::NonNull; - /// # use raw_window_handle::AndroidNdkWindowHandle; - /// # type ANativeWindow = (); - /// # - /// # let handle = unsafe { AndroidNdkWindowHandle::new(NonNull::dangling()) }; - /// let ptr = handle.a_native_window(); - /// let ptr = ptr.cast::(); + /// # fn inner() { + /// #![cfg(target_os = "android")] + /// use ndk::native_window::NativeWindow; + /// use raw_window_handle::AndroidNdkWindowHandle; + /// + /// // Gotten from somewhere. + /// let handle: AndroidNdkWindowHandle; + /// # handle = unimplemented!(); + /// + /// // SAFETY: The pointer is a valid `ANativeWindow`. + /// let window = unsafe { NativeWindow::clone_from_ptr(handle.a_native_window().cast()) }; + /// + /// // Do stuff with `window` here. + /// # } /// ``` + #[inline] pub fn a_native_window(&self) -> NonNull { self.a_native_window } } + +impl PartialEq for AndroidNdkWindowHandle { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.a_native_window == other.a_native_window + } +} + +impl Eq for AndroidNdkWindowHandle {} + +impl hash::Hash for AndroidNdkWindowHandle { + #[inline] + fn hash(&self, state: &mut H) { + self.a_native_window.hash(state); + } +} + +impl fmt::Debug for AndroidNdkWindowHandle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("AndroidNdkWindowHandle") + .field("a_native_window", &self.a_native_window) + .finish() + } +} diff --git a/src/lib.rs b/src/lib.rs index 7d8cf85..5d08e38 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -171,7 +171,7 @@ pub enum WindowHandle<'window> { /// /// ## Availability Hints /// This variant is used on Android targets. - AndroidNdk(AndroidNdkWindowHandle<'window>), + AndroidNdk(AndroidNdkWindowHandle), /// A raw window handle for Haiku. /// /// ## Availability Hints @@ -375,7 +375,7 @@ from_impl!( WebOffscreenCanvas, WebOffscreenCanvasWindowHandle<'a> ); -from_impl!(WindowHandle, AndroidNdk, AndroidNdkWindowHandle<'a>); +from_impl!(WindowHandle, AndroidNdk, AndroidNdkWindowHandle); from_impl!(WindowHandle, Haiku, HaikuWindowHandle<'a>); #[cfg(test)] From efdd06b5811348a3a893941f21596bddd24d90d5 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 1 Mar 2026 02:17:30 +0100 Subject: [PATCH 4/4] Make the Wayland handles reference-counted --- src/lib.rs | 8 +- src/wayland.rs | 287 ++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 251 insertions(+), 44 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5d08e38..647fb88 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -131,7 +131,7 @@ pub enum WindowHandle<'window> { /// ## Availability Hints /// This variant should be expected anywhere Wayland works, which is /// currently some subset of unix systems. - Wayland(WaylandWindowHandle<'window>), + Wayland(WaylandWindowHandle), /// A raw window handle for the Linux Kernel Mode Set/Direct Rendering Manager /// /// ## Availability Hints @@ -253,7 +253,7 @@ pub enum DisplayHandle<'display> { /// ## Availability Hints /// This variant should be expected anywhere Wayland works, which is /// currently some subset of unix systems. - Wayland(WaylandDisplayHandle<'display>), + Wayland(WaylandDisplayHandle), /// A raw display handle for the Linux Kernel Mode Set/Direct Rendering Manager /// /// ## Availability Hints @@ -350,7 +350,7 @@ from_impl!(DisplayHandle, Orbital, OrbitalDisplayHandle); from_impl!(DisplayHandle, Ohos, OhosDisplayHandle); from_impl!(DisplayHandle, Xlib, XlibDisplayHandle<'a>); from_impl!(DisplayHandle, Xcb, XcbDisplayHandle<'a>); -from_impl!(DisplayHandle, Wayland, WaylandDisplayHandle<'a>); +from_impl!(DisplayHandle, Wayland, WaylandDisplayHandle); from_impl!(DisplayHandle, Drm, DrmDisplayHandle<'a>); from_impl!(DisplayHandle, Gbm, GbmDisplayHandle<'a>); from_impl!(DisplayHandle, Windows, WindowsDisplayHandle); @@ -364,7 +364,7 @@ from_impl!(WindowHandle, Orbital, OrbitalWindowHandle<'a>); from_impl!(WindowHandle, OhosNdk, OhosNdkWindowHandle<'a>); from_impl!(WindowHandle, Xlib, XlibWindowHandle); from_impl!(WindowHandle, Xcb, XcbWindowHandle); -from_impl!(WindowHandle, Wayland, WaylandWindowHandle<'a>); +from_impl!(WindowHandle, Wayland, WaylandWindowHandle); from_impl!(WindowHandle, Drm, DrmWindowHandle); from_impl!(WindowHandle, Gbm, GbmWindowHandle<'a>); from_impl!(WindowHandle, Win32, Win32WindowHandle); diff --git a/src/wayland.rs b/src/wayland.rs index 2bcc5c5..03492ae 100644 --- a/src/wayland.rs +++ b/src/wayland.rs @@ -1,85 +1,292 @@ use core::ffi::c_void; -use core::marker::PhantomData; use core::ptr::NonNull; +use core::{fmt, hash}; -/// Raw display handle for Wayland. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct WaylandDisplayHandle<'display> { - display: NonNull, - _marker: PhantomData<&'display ()>, +/// Display handle for Wayland. +/// +/// See [`WaylandWindowHandle`] for discussion about the design of this. +pub struct WaylandDisplayHandle { + info: *const (), + increment_strong_count: unsafe fn(info: *const ()), + decrement_strong_count: unsafe fn(info: *const ()), + get_display: unsafe fn(info: *const ()) -> NonNull, } -impl WaylandDisplayHandle<'_> { +impl Clone for WaylandDisplayHandle { + #[inline] + fn clone(&self) -> Self { + // SAFETY: The info pointer is valid. + unsafe { (self.increment_strong_count)(self.info) }; + Self { + info: self.info, + increment_strong_count: self.increment_strong_count, + decrement_strong_count: self.decrement_strong_count, + get_display: self.get_display, + } + } +} + +impl Drop for WaylandDisplayHandle { + #[inline] + fn drop(&mut self) { + // SAFETY: The info pointer is valid. + unsafe { (self.decrement_strong_count)(self.info) }; + } +} + +impl WaylandDisplayHandle { /// Create a new display handle. /// /// # Safety /// - /// `display` must be a valid pointer to a `wl_display` and must remain valid for the lifetime - /// of this type. + /// Similar to [`WaylandWindowHandle::new`]. /// /// # Example /// /// ``` - /// # use core::ffi::c_void; - /// # use core::ptr::NonNull; - /// # use raw_window_handle::WaylandDisplayHandle; - /// # - /// let display: NonNull; - /// # display = NonNull::from(&()).cast(); - /// let handle = unsafe { WaylandDisplayHandle::new(display) }; + /// todo!() /// ``` - pub unsafe fn new(display: NonNull) -> Self { + #[inline] + pub unsafe fn new( + info: *const (), + increment_strong_count: unsafe fn(info: *const ()), + decrement_strong_count: unsafe fn(info: *const ()), + get_display: unsafe fn(info: *const ()) -> NonNull, + ) -> Self { + Self { + info, + increment_strong_count, + decrement_strong_count, + get_display, + } + } + + /// Create an unretained handle to a display. + /// + /// # Safety + /// + /// Similar to [`WaylandWindowHandle::new_unchecked`]. + #[inline] + pub unsafe fn new_unchecked(wl_display: NonNull) -> Self { + let info = wl_display.cast().as_ptr(); + + // These are intentionally no-ops, the caller ensures that the surface is alive. + fn increment_strong_count(_info: *const ()) {} + fn decrement_strong_count(_info: *const ()) {} + + fn get_display(info: *const ()) -> NonNull { + NonNull::new(info.cast_mut().cast()).unwrap() + } + Self { - display, - _marker: PhantomData, + info, + increment_strong_count, + decrement_strong_count, + get_display, } } /// A pointer to a `wl_display`. /// /// The pointer is guaranteed to be valid for at least as long as `self`. + /// + /// # Example + /// + /// ``` + /// todo!() + /// ``` pub fn display(&self) -> NonNull { - self.display + // SAFETY: The info pointer is valid. + unsafe { (self.get_display)(self.info) } + } +} + +impl PartialEq for WaylandDisplayHandle { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.display() == other.display() + } +} + +impl Eq for WaylandDisplayHandle {} + +impl hash::Hash for WaylandDisplayHandle { + #[inline] + fn hash(&self, state: &mut H) { + self.display().hash(state); + } +} + +impl fmt::Debug for WaylandDisplayHandle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("WaylandDisplayHandle") + .field("display", &self.display()) + .finish() + } +} + +/// Window handle for Wayland. +/// +/// libwayland proxies, unlike most other platforms' handles, are neither reference-counted nor an +/// ID. This is problematic for us, because we'd really prefer [`WindowHandle`][crate::WindowHandle] +/// to not contain a lifetime parameter or generic (this makes usage much simpler for the end user). +/// +/// To solve this, we provide two options for constructing this handle: +/// - [`WaylandWindowHandle::new`], which requires that you reference-count the handle and delay +/// destruction of the underlying `wl_proxy` until the last reference to the `WaylandWindowHandle` +/// is dropped. You can think of this as storing `Arc`. +/// - [`WaylandWindowHandle::new_unchecked`], which still requires that you keep the surface alive +/// for as long as any `WaylandWindowHandle` exists, but since it's done without +/// reference-counting, you must unsafely assert this. This option can only be safely used if you +/// know exactly how the handle will be used. +pub struct WaylandWindowHandle { + info: *const (), + increment_strong_count: unsafe fn(info: *const ()), + decrement_strong_count: unsafe fn(info: *const ()), + // TODO: Should we store a function to get the surface pointer, or just the surface pointer + // itself? The user is required to always return the same pointer, so either option is valid. + get_surface: unsafe fn(info: *const ()) -> NonNull, +} + +impl Clone for WaylandWindowHandle { + #[inline] + fn clone(&self) -> Self { + // SAFETY: The info pointer is valid. + unsafe { (self.increment_strong_count)(self.info) }; + Self { + info: self.info, + increment_strong_count: self.increment_strong_count, + decrement_strong_count: self.decrement_strong_count, + get_surface: self.get_surface, + } } } -/// Raw window handle for Wayland. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct WaylandWindowHandle<'window> { - surface: NonNull, - _marker: PhantomData<&'window ()>, +impl Drop for WaylandWindowHandle { + #[inline] + fn drop(&mut self) { + // SAFETY: The info pointer is valid. + unsafe { (self.decrement_strong_count)(self.info) }; + } } -impl WaylandWindowHandle<'_> { +impl WaylandWindowHandle { /// Create a new handle to a surface. /// /// # Safety /// - /// `display` must be a valid pointer to a `wl_surface` and must remain valid for the lifetime - /// of this type. + /// `info` must be a valid pointer to a something that can be reference-counted with the given + /// functions, and the `get_surface` function must return a `wl_surface` pointer that is valid + /// for as long as the `info` pointer is alive. + /// + /// # Example + /// + /// ``` + /// use std::ptr::NonNull; + /// use std::ffi::c_void; + /// use std::sync::Arc; + /// use raw_window_handle::WaylandWindowHandle; + /// + /// // wl_surface pointer gotten from somewhere. + /// let wl_surface: NonNull; + /// # wl_surface = NonNull::dangling(); + /// + /// todo!(); + /// ``` + #[inline] + pub unsafe fn new( + info: *const (), + increment_strong_count: unsafe fn(info: *const ()), + decrement_strong_count: unsafe fn(info: *const ()), + get_surface: unsafe fn(info: *const ()) -> NonNull, + ) -> Self { + Self { + info, + increment_strong_count, + decrement_strong_count, + get_surface, + } + } + + /// Create an unretained handle to a surface. + /// + /// This is intended as an "escape hook" for users that want to use `raw-window-handle`, but do + /// not want (or cannot) add reference-counting to their handles. + /// + /// It is strongly recommended to use [`WaylandWindowHandle::new`] instead. + /// + /// # Safety + /// + /// The given `wl_surface` must be valid for the entire duration of `WaylandWindowHandle`, **and + /// any of its clones**. + /// + /// This is impossible to ensure in general (since this type does not contain a lifetime + /// parameter), so as a library author, you must not expose the returned instance safely to your + /// users. /// /// # Example /// /// ``` - /// # use core::ffi::c_void; - /// # use core::ptr::NonNull; - /// # use raw_window_handle::WaylandWindowHandle; - /// # - /// let surface: NonNull; - /// # surface = NonNull::from(&()).cast(); - /// let handle = unsafe { WaylandWindowHandle::new(surface) }; + /// todo!(); /// ``` - pub unsafe fn new(surface: NonNull) -> Self { + #[inline] + pub unsafe fn new_unchecked(wl_surface: NonNull) -> Self { + let info = wl_surface.cast().as_ptr(); + + // These are intentionally no-ops, the caller ensures that the surface is alive. + fn increment_strong_count(_info: *const ()) {} + fn decrement_strong_count(_info: *const ()) {} + + fn get_surface(info: *const ()) -> NonNull { + NonNull::new(info.cast_mut().cast()).unwrap() + } + Self { - surface, - _marker: PhantomData, + info, + increment_strong_count, + decrement_strong_count, + get_surface, } } + // TODO: pub unsafe fn from_arc(Arc NonNull>) ? + /// A pointer to a `wl_surface`. /// - /// The pointer is guaranteed to be valid for at least as long as `self`. + /// The pointer is guaranteed to be valid for at least as long as this type is alive. + /// + /// # Example + /// + /// ``` + /// todo!() + /// ``` + #[inline] pub fn surface(&self) -> NonNull { - self.surface + // SAFETY: The info pointer is valid. + unsafe { (self.get_surface)(self.info) } + } +} + +impl PartialEq for WaylandWindowHandle { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.surface() == other.surface() + } +} + +impl Eq for WaylandWindowHandle {} + +impl hash::Hash for WaylandWindowHandle { + #[inline] + fn hash(&self, state: &mut H) { + self.surface().hash(state); + } +} + +impl fmt::Debug for WaylandWindowHandle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("WaylandWindowHandle") + .field("surface", &self.surface()) + .finish() } }