From ff261e61af290bc4a1d268c6ee363001319562e2 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Mon, 31 May 2021 10:05:06 +0200 Subject: [PATCH 1/5] Make autoreleasepool take the pool as a parameter The lifetime of the parameter is the lifetime of references in the pool. This allows us to bound lifetimes on autoreleased objects, thereby making them safe to return. --- CHANGELOG.md | 7 +++ README.md | 2 +- src/rc/autorelease.rs | 138 +++++++++++++++++++++++++++++++++++++----- src/rc/mod.rs | 10 +-- 4 files changed, 137 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8b1bf894..c92187fca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## Unreleased + +### Changed + +* The closure in `autoreleasepool` now takes an argument, a reference to the + pool. + ## 0.2.7 ### Fixed diff --git a/README.md b/README.md index 55fba6484..7a5b3aa4c 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ let obj = unsafe { // Cloning retains the object an additional time let cloned = obj.clone(); -autoreleasepool(|| { +autoreleasepool(|_| { // Autorelease consumes the StrongPtr, but won't // actually release until the end of an autoreleasepool cloned.autorelease(); diff --git a/src/rc/autorelease.rs b/src/rc/autorelease.rs index 17df8af39..298a5aa32 100644 --- a/src/rc/autorelease.rs +++ b/src/rc/autorelease.rs @@ -1,30 +1,140 @@ +use crate::runtime::{objc_autoreleasePoolPop, objc_autoreleasePoolPush}; use std::os::raw::c_void; -use crate::runtime::{objc_autoreleasePoolPush, objc_autoreleasePoolPop}; -// we use a struct to ensure that objc_autoreleasePoolPop during unwinding. -struct AutoReleaseHelper { +/// An Objective-C autorelease pool. +/// +/// The pool is drained when dropped. +/// +/// This is not `Send`, since `objc_autoreleasePoolPop` must be called on the +/// same thread. +/// +/// And this is not `Sync`, since you can only autorelease a reference to a +/// pool on the current thread. +/// +/// See [the clang documentation][clang-arc] and +/// [this apple article][memory-mgmt] for more information on automatic +/// reference counting. +/// +/// [clang-arc]: https://clang.llvm.org/docs/AutomaticReferenceCounting.html +/// [memory-mgmt]: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/MemoryMgmt.html +pub struct AutoreleasePool { context: *mut c_void, } -impl AutoReleaseHelper { +impl AutoreleasePool { + /// Construct a new autoreleasepool. + /// + /// Use the [`autoreleasepool`] block for a safe alternative. + /// + /// # Safety + /// + /// The caller must ensure that when handing out `&'p AutoreleasePool` to + /// functions that this is the innermost pool. + /// + /// Additionally, the pools must be dropped in the same order they were + /// created. + #[doc(alias = "objc_autoreleasePoolPush")] unsafe fn new() -> Self { - AutoReleaseHelper { context: objc_autoreleasePoolPush() } + // TODO: Make this function pub when we're more certain of the API + AutoreleasePool { + context: objc_autoreleasePoolPush(), + } } + + // TODO: Add helper functions to ensure (with debug_assertions) that the + // pool is innermost when its lifetime is tied to a reference. } -impl Drop for AutoReleaseHelper { +impl Drop for AutoreleasePool { + /// Drains the autoreleasepool. + /// + /// The [clang documentation] says that `@autoreleasepool` blocks are not + /// drained when exceptions occur because: + /// + /// > Not draining the pool during an unwind is apparently required by the + /// > Objective-C exceptions implementation. + /// + /// This was true in the past, but since [revision `371`] of + /// `objc-exception.m` (ships with MacOS 10.5) the exception is now + /// retained when `@throw` is encountered. + /// + /// Hence it is safe to drain the pool when unwinding. + /// + /// [clang documentation]: https://clang.llvm.org/docs/AutomaticReferenceCounting.html#autoreleasepool + /// [revision `371`]: https://opensource.apple.com/source/objc4/objc4-371/runtime/objc-exception.m.auto.html + #[doc(alias = "objc_autoreleasePoolPop")] fn drop(&mut self) { unsafe { objc_autoreleasePoolPop(self.context) } } } -/** -Execute `f` in the context of a new autorelease pool. The pool is drained -after the execution of `f` completes. +// TODO: +// #![feature(negative_impls)] +// #![feature(auto_traits)] +// /// A trait for the sole purpose of ensuring we can't pass an `&AutoreleasePool` +// /// through to the closure inside `autoreleasepool` +// pub unsafe auto trait AutoreleaseSafe {} +// // TODO: Unsure how negative impls work exactly +// unsafe impl !AutoreleaseSafe for AutoreleasePool {} +// unsafe impl !AutoreleaseSafe for &AutoreleasePool {} +// unsafe impl !AutoreleaseSafe for &mut AutoreleasePool {} -This corresponds to `@autoreleasepool` blocks in Objective-C and Swift. -*/ -pub fn autoreleasepool T>(f: F) -> T { - let _context = unsafe { AutoReleaseHelper::new() }; - f() +/// Execute `f` in the context of a new autorelease pool. The pool is drained +/// after the execution of `f` completes. +/// +/// This corresponds to `@autoreleasepool` blocks in Objective-C and Swift. +/// +/// The pool is passed as a reference to the enclosing function to give it a +/// lifetime parameter that autoreleased objects can refer to. +/// +/// # Examples +/// +/// ```rust +/// use objc::{class, msg_send}; +/// use objc::rc::{autoreleasepool, AutoreleasePool}; +/// use objc::runtime::Object; +/// +/// fn needs_lifetime_from_pool<'p>(_pool: &'p AutoreleasePool) -> &'p mut Object { +/// let obj: *mut Object = unsafe { msg_send![class!(NSObject), new] }; +/// let obj: *mut Object = unsafe { msg_send![obj, autorelease] }; +/// // SAFETY: Lifetime bounded by the pool +/// unsafe { &mut *obj } +/// } +/// +/// autoreleasepool(|pool| { +/// let obj = needs_lifetime_from_pool(pool); +/// // Use `obj` +/// }); +/// +/// // `obj` is deallocated when the pool ends +/// ``` +/// +/// ```rust,compile_fail +/// # use objc::{class, msg_send}; +/// # use objc::rc::{autoreleasepool, AutoreleasePool}; +/// # use objc::runtime::Object; +/// # +/// # fn needs_lifetime_from_pool<'p>(_pool: &'p AutoreleasePool) -> &'p mut Object { +/// # let obj: *mut Object = unsafe { msg_send![class!(NSObject), new] }; +/// # let obj: *mut Object = unsafe { msg_send![obj, autorelease] }; +/// # unsafe { &mut *obj } +/// # } +/// # +/// // Fails to compile because `obj` does not live long enough for us to +/// // safely take it out of the pool. +/// +/// let obj = autoreleasepool(|pool| { +/// let obj = needs_lifetime_from_pool(pool); +/// // Use `obj` +/// obj +/// }); +/// ``` +/// +/// TODO: More examples. +pub fn autoreleasepool(f: F) -> T +where + for<'p> F: FnOnce(&'p AutoreleasePool) -> T, // + AutoreleaseSafe, +{ + let pool = unsafe { AutoreleasePool::new() }; + f(&pool) } diff --git a/src/rc/mod.rs b/src/rc/mod.rs index c703f585d..cd9ddb96b 100644 --- a/src/rc/mod.rs +++ b/src/rc/mod.rs @@ -26,7 +26,7 @@ let obj = unsafe { // Cloning retains the object an additional time let cloned = obj.clone(); -autoreleasepool(|| { +autoreleasepool(|_| { // Autorelease consumes the StrongPtr, but won't // actually release until the end of an autoreleasepool cloned.autorelease(); @@ -44,9 +44,9 @@ mod strong; mod weak; mod autorelease; +pub use self::autorelease::{autoreleasepool, AutoreleasePool}; pub use self::strong::StrongPtr; pub use self::weak::WeakPtr; -pub use self::autorelease::autoreleasepool; // These tests use NSObject, which isn't present for GNUstep #[cfg(all(test, any(target_os = "macos", target_os = "ios")))] @@ -112,9 +112,9 @@ mod tests { } let cloned = obj.clone(); - autoreleasepool(|| { - obj.autorelease(); - assert!(retain_count(*cloned) == 2); + autoreleasepool(|_| { + obj.autorelease(); + assert!(retain_count(*cloned) == 2); }); // make sure that the autoreleased value has been released From 93ca34ae8f88fcebd62fd653392961b79bdc3363 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 13 Jun 2021 12:53:15 +0200 Subject: [PATCH 2/5] Add a regression test to ensure AutoreleasePool is not Send nor Sync --- src/rc/autorelease.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/rc/autorelease.rs b/src/rc/autorelease.rs index 298a5aa32..b887698c7 100644 --- a/src/rc/autorelease.rs +++ b/src/rc/autorelease.rs @@ -21,8 +21,21 @@ pub struct AutoreleasePool { context: *mut c_void, } +/// ```rust,compile_fail +/// use objc::rc::AutoreleasePool; +/// fn needs_sync() {} +/// needs_sync::(); +/// ``` +/// ```rust,compile_fail +/// use objc::rc::AutoreleasePool; +/// fn needs_send() {} +/// needs_send::(); +/// ``` +#[cfg(doctest)] +pub struct AutoreleasePoolNotSendNorSync; + impl AutoreleasePool { - /// Construct a new autoreleasepool. + /// Construct a new autorelease pool. /// /// Use the [`autoreleasepool`] block for a safe alternative. /// @@ -36,7 +49,7 @@ impl AutoreleasePool { #[doc(alias = "objc_autoreleasePoolPush")] unsafe fn new() -> Self { // TODO: Make this function pub when we're more certain of the API - AutoreleasePool { + Self { context: objc_autoreleasePoolPush(), } } From 0d256c3eeb1e381358fa9f2c0d8cdb18718d8131 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 13 Jun 2021 15:30:17 +0200 Subject: [PATCH 3/5] Add `unstable_autoreleasesafe` feature A nightly-only feature that adds the auto trait `AutoreleaseSafe` and uses it to prevent calling `autoreleasepool` with closures that capture an outer `AutoreleasePool`. This fixes on nightly the unsoundness hole preventing `autoreleasepool` from being used to safely construct references to autoreleased objects. --- Cargo.toml | 1 + src/lib.rs | 2 + src/rc/autorelease.rs | 206 ++++++++++++++++++++++++++++-------------- 3 files changed, 141 insertions(+), 68 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3a7f2b8c2..1570560d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ exclude = [ [features] exception = ["objc_exception"] verify_message = [] +unstable_autoreleasesafe = [] [dependencies] malloc_buf = "1.0" diff --git a/src/lib.rs b/src/lib.rs index 5f2d4e9f1..f71ba8cfa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -63,6 +63,8 @@ The bindings can be used on Linux or *BSD utilizing the #![crate_name = "objc"] #![crate_type = "lib"] +#![cfg_attr(feature = "unstable_autoreleasesafe", feature(negative_impls, auto_traits))] + #![warn(missing_docs)] extern crate malloc_buf; diff --git a/src/rc/autorelease.rs b/src/rc/autorelease.rs index b887698c7..5c1ac494e 100644 --- a/src/rc/autorelease.rs +++ b/src/rc/autorelease.rs @@ -81,73 +81,143 @@ impl Drop for AutoreleasePool { } } -// TODO: -// #![feature(negative_impls)] -// #![feature(auto_traits)] -// /// A trait for the sole purpose of ensuring we can't pass an `&AutoreleasePool` -// /// through to the closure inside `autoreleasepool` -// pub unsafe auto trait AutoreleaseSafe {} -// // TODO: Unsure how negative impls work exactly -// unsafe impl !AutoreleaseSafe for AutoreleasePool {} -// unsafe impl !AutoreleaseSafe for &AutoreleasePool {} -// unsafe impl !AutoreleaseSafe for &mut AutoreleasePool {} +/// A trait for the sole purpose of ensuring we can't pass an `&AutoreleasePool` +/// through to the closure inside `autoreleasepool` +#[cfg(feature = "unstable_autoreleasesafe")] +pub unsafe auto trait AutoreleaseSafe {} +#[cfg(feature = "unstable_autoreleasesafe")] +impl !AutoreleaseSafe for AutoreleasePool {} -/// Execute `f` in the context of a new autorelease pool. The pool is drained -/// after the execution of `f` completes. -/// -/// This corresponds to `@autoreleasepool` blocks in Objective-C and Swift. -/// -/// The pool is passed as a reference to the enclosing function to give it a -/// lifetime parameter that autoreleased objects can refer to. -/// -/// # Examples -/// -/// ```rust -/// use objc::{class, msg_send}; -/// use objc::rc::{autoreleasepool, AutoreleasePool}; -/// use objc::runtime::Object; -/// -/// fn needs_lifetime_from_pool<'p>(_pool: &'p AutoreleasePool) -> &'p mut Object { -/// let obj: *mut Object = unsafe { msg_send![class!(NSObject), new] }; -/// let obj: *mut Object = unsafe { msg_send![obj, autorelease] }; -/// // SAFETY: Lifetime bounded by the pool -/// unsafe { &mut *obj } -/// } -/// -/// autoreleasepool(|pool| { -/// let obj = needs_lifetime_from_pool(pool); -/// // Use `obj` -/// }); -/// -/// // `obj` is deallocated when the pool ends -/// ``` -/// -/// ```rust,compile_fail -/// # use objc::{class, msg_send}; -/// # use objc::rc::{autoreleasepool, AutoreleasePool}; -/// # use objc::runtime::Object; -/// # -/// # fn needs_lifetime_from_pool<'p>(_pool: &'p AutoreleasePool) -> &'p mut Object { -/// # let obj: *mut Object = unsafe { msg_send![class!(NSObject), new] }; -/// # let obj: *mut Object = unsafe { msg_send![obj, autorelease] }; -/// # unsafe { &mut *obj } -/// # } -/// # -/// // Fails to compile because `obj` does not live long enough for us to -/// // safely take it out of the pool. -/// -/// let obj = autoreleasepool(|pool| { -/// let obj = needs_lifetime_from_pool(pool); -/// // Use `obj` -/// obj -/// }); -/// ``` -/// -/// TODO: More examples. -pub fn autoreleasepool(f: F) -> T -where - for<'p> F: FnOnce(&'p AutoreleasePool) -> T, // + AutoreleaseSafe, -{ - let pool = unsafe { AutoreleasePool::new() }; - f(&pool) +#[cfg(feature = "unstable_autoreleasesafe")] +macro_rules! fn_autoreleasepool { + {$(#[$fn_meta:meta])* $v:vis fn $fn:ident($f:ident) $b:block} => { + $(#[$fn_meta])* + $v fn $fn($f: F) -> T + where + for<'p> F: FnOnce(&'p AutoreleasePool) -> T + AutoreleaseSafe, + { + $b + } + } +} + +#[cfg(not(feature = "unstable_autoreleasesafe"))] +macro_rules! fn_autoreleasepool { + {$(#[$fn_meta:meta])* $v:vis fn $fn:ident($f:ident) $b:block} => { + $(#[$fn_meta])* + $v fn $fn($f: F) -> T + where + for<'p> F: FnOnce(&'p AutoreleasePool) -> T, + { + $b + } + } +} + +fn_autoreleasepool!( + /// Execute `f` in the context of a new autorelease pool. The pool is + /// drained after the execution of `f` completes. + /// + /// This corresponds to `@autoreleasepool` blocks in Objective-C and + /// Swift. + /// + /// The pool is passed as a reference to the enclosing function to give it + /// a lifetime parameter that autoreleased objects can refer to. + /// + /// The given reference must not be used in an inner `autoreleasepool`, + /// doing so will be a compile error in a future release. You can test + /// this guarantee with the `unstable_autoreleasesafe` crate feature on + /// nightly Rust. + /// + /// So using `autoreleasepool` is unsound right now because of this + /// specific problem. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```rust + /// use objc::{class, msg_send}; + /// use objc::rc::{autoreleasepool, AutoreleasePool}; + /// use objc::runtime::Object; + /// + /// fn needs_lifetime_from_pool<'p>(_pool: &'p AutoreleasePool) -> &'p mut Object { + /// let obj: *mut Object = unsafe { msg_send![class!(NSObject), new] }; + /// let obj: *mut Object = unsafe { msg_send![obj, autorelease] }; + /// // SAFETY: Lifetime bounded by the pool + /// unsafe { &mut *obj } + /// } + /// + /// autoreleasepool(|pool| { + /// // Create `obj` and autorelease it to the pool + /// let obj = needs_lifetime_from_pool(pool); + /// // ... use `obj` here + /// // `obj` is deallocated when the pool ends + /// }); + /// ``` + /// + /// Fails to compile because `obj` does not live long enough for us to + /// safely take it out of the pool: + /// + /// ```rust,compile_fail + /// # use objc::{class, msg_send}; + /// # use objc::rc::{autoreleasepool, AutoreleasePool}; + /// # use objc::runtime::Object; + /// # + /// # fn needs_lifetime_from_pool<'p>(_pool: &'p AutoreleasePool) -> &'p mut Object { + /// # let obj: *mut Object = unsafe { msg_send![class!(NSObject), new] }; + /// # let obj: *mut Object = unsafe { msg_send![obj, autorelease] }; + /// # unsafe { &mut *obj } + /// # } + /// # + /// let obj = autoreleasepool(|pool| { + /// let obj = needs_lifetime_from_pool(pool); + /// // Use `obj` + /// obj + /// }); + /// ``` + /// + /// Incorrect usage which causes undefined behaviour: + /// + #[cfg_attr(feature = "unstable_autoreleasesafe", doc = "```rust,compile_fail")] + #[cfg_attr(not(feature = "unstable_autoreleasesafe"), doc = "```rust")] + /// # use objc::{class, msg_send}; + /// # use objc::rc::{autoreleasepool, AutoreleasePool}; + /// # use objc::runtime::Object; + /// # + /// # fn needs_lifetime_from_pool<'p>(_pool: &'p AutoreleasePool) -> &'p mut Object { + /// # let obj: *mut Object = unsafe { msg_send![class!(NSObject), new] }; + /// # let obj: *mut Object = unsafe { msg_send![obj, autorelease] }; + /// # unsafe { &mut *obj } + /// # } + /// # + /// autoreleasepool(|outer_pool| { + /// let obj = autoreleasepool(|inner_pool| { + /// let obj = needs_lifetime_from_pool(outer_pool); + /// obj + /// }); + /// // `obj` can wrongly be used here because it's lifetime was + /// // assigned to the outer pool, even though it was released by the + /// // inner pool already. + /// }); + /// ``` + pub fn autoreleasepool(f) { + let pool = unsafe { AutoreleasePool::new() }; + f(&pool) + } +); + +#[cfg(all(test, feature = "unstable_autoreleasesafe"))] +mod tests { + use super::AutoreleaseSafe; + use crate::runtime::Object; + + fn requires_autoreleasesafe() {} + + #[test] + fn test_autoreleasesafe() { + requires_autoreleasesafe::(); + requires_autoreleasesafe::<*mut Object>(); + requires_autoreleasesafe::<&mut Object>(); + } } From 894110305f11726f9bfcb04fd6f940b77120eb60 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Mon, 14 Jun 2021 10:55:18 +0200 Subject: [PATCH 4/5] Document and export AutoreleaseSafe --- src/rc/autorelease.rs | 19 +++++++++++++++++-- src/rc/mod.rs | 2 ++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/rc/autorelease.rs b/src/rc/autorelease.rs index 5c1ac494e..c165f8af4 100644 --- a/src/rc/autorelease.rs +++ b/src/rc/autorelease.rs @@ -81,13 +81,27 @@ impl Drop for AutoreleasePool { } } -/// A trait for the sole purpose of ensuring we can't pass an `&AutoreleasePool` -/// through to the closure inside `autoreleasepool` #[cfg(feature = "unstable_autoreleasesafe")] +/// Marks types that are safe to pass across the closure in an +/// [`autoreleasepool`]. +/// +/// This is implemented for all types except [`AutoreleasePool`]. +/// +/// You should not need to implement this trait yourself. +/// +/// # Safety +/// +/// Must not be implemented for types that interract with the autorelease pool +/// - so if you reimplement the `AutoreleasePool` struct, this should be +/// negatively implemented for that. +// TODO: We can technically make this private, but should we? pub unsafe auto trait AutoreleaseSafe {} #[cfg(feature = "unstable_autoreleasesafe")] impl !AutoreleaseSafe for AutoreleasePool {} +// We use a macro here so that the function documentation is included whether +// the feature is enabled or not. + #[cfg(feature = "unstable_autoreleasesafe")] macro_rules! fn_autoreleasepool { {$(#[$fn_meta:meta])* $v:vis fn $fn:ident($f:ident) $b:block} => { @@ -201,6 +215,7 @@ fn_autoreleasepool!( /// // inner pool already. /// }); /// ``` + #[doc(alias = "@autoreleasepool")] pub fn autoreleasepool(f) { let pool = unsafe { AutoreleasePool::new() }; f(&pool) diff --git a/src/rc/mod.rs b/src/rc/mod.rs index cd9ddb96b..a6dabd3e4 100644 --- a/src/rc/mod.rs +++ b/src/rc/mod.rs @@ -44,6 +44,8 @@ mod strong; mod weak; mod autorelease; +#[cfg(feature = "unstable_autoreleasesafe")] +pub use self::autorelease::AutoreleaseSafe; pub use self::autorelease::{autoreleasepool, AutoreleasePool}; pub use self::strong::StrongPtr; pub use self::weak::WeakPtr; From 18d55abd74a2f39081cbbe1e756fd877924614c6 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Mon, 14 Jun 2021 17:10:02 +0200 Subject: [PATCH 5/5] Add debug assertions to prevent autoreleasepool unsoundness Prevent using the lifetime from an outer pool when an inner pool is active. --- src/rc/autorelease.rs | 124 ++++++++++++++++++++++++++++++++---------- 1 file changed, 96 insertions(+), 28 deletions(-) diff --git a/src/rc/autorelease.rs b/src/rc/autorelease.rs index c165f8af4..5b87527fd 100644 --- a/src/rc/autorelease.rs +++ b/src/rc/autorelease.rs @@ -1,19 +1,20 @@ use crate::runtime::{objc_autoreleasePoolPop, objc_autoreleasePoolPush}; -use std::os::raw::c_void; +use core::cell::RefCell; +use core::ffi::c_void; /// An Objective-C autorelease pool. /// /// The pool is drained when dropped. /// -/// This is not `Send`, since `objc_autoreleasePoolPop` must be called on the -/// same thread. +/// This is not [`Send`], since `objc_autoreleasePoolPop` must be called on +/// the same thread. /// -/// And this is not `Sync`, since you can only autorelease a reference to a +/// And this is not [`Sync`], since you can only autorelease a reference to a /// pool on the current thread. /// -/// See [the clang documentation][clang-arc] and -/// [this apple article][memory-mgmt] for more information on automatic -/// reference counting. +/// See [the clang documentation][clang-arc] and [the apple article on memory +/// management][memory-mgmt] for more information on automatic reference +/// counting. /// /// [clang-arc]: https://clang.llvm.org/docs/AutomaticReferenceCounting.html /// [memory-mgmt]: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/MemoryMgmt.html @@ -34,6 +35,13 @@ pub struct AutoreleasePool { #[cfg(doctest)] pub struct AutoreleasePoolNotSendNorSync; +#[cfg(all(debug_assertions, not(feature = "unstable_autoreleasesafe")))] +thread_local! { + /// We track the thread's pools to verify that object lifetimes are only + /// taken from the innermost pool. + static POOLS: RefCell> = RefCell::new(Vec::new()); +} + impl AutoreleasePool { /// Construct a new autorelease pool. /// @@ -49,13 +57,69 @@ impl AutoreleasePool { #[doc(alias = "objc_autoreleasePoolPush")] unsafe fn new() -> Self { // TODO: Make this function pub when we're more certain of the API - Self { - context: objc_autoreleasePoolPush(), - } + let context = objc_autoreleasePoolPush(); + #[cfg(all(debug_assertions, not(feature = "unstable_autoreleasesafe")))] + POOLS.with(|c| c.borrow_mut().push(context)); + Self { context } } - // TODO: Add helper functions to ensure (with debug_assertions) that the - // pool is innermost when its lifetime is tied to a reference. + /// Returns a shared reference to the given autoreleased pointer object. + /// + /// This is the preferred way to make references from autoreleased + /// objects, since it binds the lifetime of the reference to the pool, and + /// does some extra checks when debug assertions are enabled. + /// + /// For the mutable counterpart see [`ptr_as_mut`](#method.ptr_as_mut). + /// + /// # Safety + /// + /// This is equivalent to `&*ptr`, and shares the unsafety of that, except + /// the lifetime is bound to the pool instead of being unbounded. + #[cfg_attr( + all(debug_assertions, not(feature = "unstable_autoreleasesafe")), + inline + )] + pub unsafe fn ptr_as_ref<'p, T>(&'p self, ptr: *const T) -> &'p T { + #[cfg(all(debug_assertions, not(feature = "unstable_autoreleasesafe")))] + POOLS.with(|c| { + assert_eq!( + c.borrow().last(), + Some(&self.context), + "Tried to create shared reference with a lifetime from a pool that was not the innermost pool" + ) + }); + // SAFETY: Checked by the caller + &*ptr + } + + /// Returns a unique reference to the given autoreleased pointer object. + /// + /// This is the preferred way to make mutable references from autoreleased + /// objects, since it binds the lifetime of the reference to the pool, and + /// does some extra checks when debug assertions are enabled. + /// + /// For the shared counterpart see [`ptr_as_ref`](#method.ptr_as_ref). + /// + /// # Safety + /// + /// This is equivalent to `&mut *ptr`, and shares the unsafety of that, + /// except the lifetime is bound to the pool instead of being unbounded. + #[cfg_attr( + all(debug_assertions, not(feature = "unstable_autoreleasesafe")), + inline + )] + pub unsafe fn ptr_as_mut<'p, T>(&'p self, ptr: *mut T) -> &'p mut T { + #[cfg(all(debug_assertions, not(feature = "unstable_autoreleasesafe")))] + POOLS.with(|c| { + assert_eq!( + c.borrow().last(), + Some(&self.context), + "Tried to create unique reference with a lifetime from a pool that was not the innermost pool") + } + ); + // SAFETY: Checked by the caller + &mut *ptr + } } impl Drop for AutoreleasePool { @@ -78,6 +142,14 @@ impl Drop for AutoreleasePool { #[doc(alias = "objc_autoreleasePoolPop")] fn drop(&mut self) { unsafe { objc_autoreleasePoolPop(self.context) } + #[cfg(all(debug_assertions, not(feature = "unstable_autoreleasesafe")))] + POOLS.with(|c| { + assert_eq!( + c.borrow_mut().pop(), + Some(self.context), + "Popped pool that was not the innermost pool" + ) + }); } } @@ -139,12 +211,9 @@ fn_autoreleasepool!( /// a lifetime parameter that autoreleased objects can refer to. /// /// The given reference must not be used in an inner `autoreleasepool`, - /// doing so will be a compile error in a future release. You can test - /// this guarantee with the `unstable_autoreleasesafe` crate feature on - /// nightly Rust. - /// - /// So using `autoreleasepool` is unsound right now because of this - /// specific problem. + /// doing so will panic with debug assertions enabled, and be a compile + /// error in a future release. You can test the compile error with the + /// `unstable_autoreleasesafe` crate feature on nightly Rust. /// /// # Examples /// @@ -155,11 +224,10 @@ fn_autoreleasepool!( /// use objc::rc::{autoreleasepool, AutoreleasePool}; /// use objc::runtime::Object; /// - /// fn needs_lifetime_from_pool<'p>(_pool: &'p AutoreleasePool) -> &'p mut Object { + /// fn needs_lifetime_from_pool<'p>(pool: &'p AutoreleasePool) -> &'p mut Object { /// let obj: *mut Object = unsafe { msg_send![class!(NSObject), new] }; /// let obj: *mut Object = unsafe { msg_send![obj, autorelease] }; - /// // SAFETY: Lifetime bounded by the pool - /// unsafe { &mut *obj } + /// unsafe { pool.ptr_as_mut(obj) } /// } /// /// autoreleasepool(|pool| { @@ -178,10 +246,10 @@ fn_autoreleasepool!( /// # use objc::rc::{autoreleasepool, AutoreleasePool}; /// # use objc::runtime::Object; /// # - /// # fn needs_lifetime_from_pool<'p>(_pool: &'p AutoreleasePool) -> &'p mut Object { + /// # fn needs_lifetime_from_pool<'p>(pool: &'p AutoreleasePool) -> &'p mut Object { /// # let obj: *mut Object = unsafe { msg_send![class!(NSObject), new] }; /// # let obj: *mut Object = unsafe { msg_send![obj, autorelease] }; - /// # unsafe { &mut *obj } + /// # unsafe { pool.ptr_as_mut(obj) } /// # } /// # /// let obj = autoreleasepool(|pool| { @@ -191,18 +259,18 @@ fn_autoreleasepool!( /// }); /// ``` /// - /// Incorrect usage which causes undefined behaviour: + /// Incorrect usage which panics: /// #[cfg_attr(feature = "unstable_autoreleasesafe", doc = "```rust,compile_fail")] - #[cfg_attr(not(feature = "unstable_autoreleasesafe"), doc = "```rust")] + #[cfg_attr(not(feature = "unstable_autoreleasesafe"), doc = "```rust,should_panic")] /// # use objc::{class, msg_send}; /// # use objc::rc::{autoreleasepool, AutoreleasePool}; /// # use objc::runtime::Object; /// # - /// # fn needs_lifetime_from_pool<'p>(_pool: &'p AutoreleasePool) -> &'p mut Object { + /// # fn needs_lifetime_from_pool<'p>(pool: &'p AutoreleasePool) -> &'p mut Object { /// # let obj: *mut Object = unsafe { msg_send![class!(NSObject), new] }; /// # let obj: *mut Object = unsafe { msg_send![obj, autorelease] }; - /// # unsafe { &mut *obj } + /// # unsafe { pool.ptr_as_mut(obj) } /// # } /// # /// autoreleasepool(|outer_pool| { @@ -210,7 +278,7 @@ fn_autoreleasepool!( /// let obj = needs_lifetime_from_pool(outer_pool); /// obj /// }); - /// // `obj` can wrongly be used here because it's lifetime was + /// // `obj` could wrongly be used here because it's lifetime was /// // assigned to the outer pool, even though it was released by the /// // inner pool already. /// });