Skip to content

Commit

Permalink
Merge pull request SSheldon#17 from madsmtm/autoreleasepool-lifetime
Browse files Browse the repository at this point in the history
Make `autoreleasepool` take the pool as a parameter
  • Loading branch information
madsmtm committed Sep 2, 2021
2 parents de78ed4 + 478f7dc commit 1f82d5b
Show file tree
Hide file tree
Showing 6 changed files with 309 additions and 20 deletions.
7 changes: 7 additions & 0 deletions objc/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## Unreleased

### Changed

* The closure in `autoreleasepool` now takes an argument, a reference to the
pool.

## 0.2.7

### Fixed
Expand Down
1 change: 1 addition & 0 deletions objc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ exclude = [
[features]
exception = ["objc_exception"]
verify_message = []
unstable_autoreleasesafe = []

[dependencies]
malloc_buf = "1.0"
Expand Down
2 changes: 1 addition & 1 deletion objc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,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();
Expand Down
2 changes: 2 additions & 0 deletions objc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ The bindings can be used on Linux or *BSD utilizing the
*/

#![no_std]
#![cfg_attr(feature = "unstable_autoreleasesafe", feature(negative_impls, auto_traits))]

#![warn(missing_docs)]
#![allow(clippy::missing_safety_doc)]
// Update in Cargo.toml as well.
Expand Down
311 changes: 295 additions & 16 deletions objc/src/rc/autorelease.rs
Original file line number Diff line number Diff line change
@@ -1,32 +1,311 @@
use crate::runtime::{objc_autoreleasePoolPop, objc_autoreleasePoolPush};
use core::ffi::c_void;
#[cfg(all(debug_assertions, not(feature = "unstable_autoreleasesafe")))]
use std::{cell::RefCell, vec::Vec, thread_local};

use crate::runtime::{objc_autoreleasePoolPop, objc_autoreleasePoolPush};

// 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 [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
pub struct AutoreleasePool {
context: *mut c_void,
}

impl AutoReleaseHelper {
/// ```rust,compile_fail
/// use objc::rc::AutoreleasePool;
/// fn needs_sync<T: Send>() {}
/// needs_sync::<AutoreleasePool>();
/// ```
/// ```rust,compile_fail
/// use objc::rc::AutoreleasePool;
/// fn needs_send<T: Send>() {}
/// needs_send::<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<Vec<*mut c_void>> = RefCell::new(Vec::new());
}

impl AutoreleasePool {
/// Construct a new autorelease pool.
///
/// 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
let context = objc_autoreleasePoolPush();
#[cfg(all(debug_assertions, not(feature = "unstable_autoreleasesafe")))]
POOLS.with(|c| c.borrow_mut().push(context));
Self { context }
}

/// 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 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) }
#[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"
)
});
}
}

/// We use a macro here so that the documentation is included whether the
/// feature is enabled or not.
#[cfg(not(feature = "unstable_autoreleasesafe"))]
macro_rules! auto_trait {
{$(#[$fn_meta:meta])* $v:vis unsafe trait AutoreleaseSafe {}} => {
$(#[$fn_meta])*
$v unsafe trait AutoreleaseSafe {}
}
}

#[cfg(feature = "unstable_autoreleasesafe")]
macro_rules! auto_trait {
{$(#[$fn_meta:meta])* $v:vis unsafe trait AutoreleaseSafe {}} => {
$(#[$fn_meta])*
$v unsafe auto trait AutoreleaseSafe {}
}
}

/**
Execute `f` in the context of a new autorelease pool. The pool is drained
after the execution of `f` completes.
auto_trait! {
/// Marks types that are safe to pass across the closure in an
/// [`autoreleasepool`].
///
/// With the `unstable_autoreleasesafe` feature enabled, this is an auto
/// trait that is implemented for all types except [`AutoreleasePool`].
///
/// Otherwise it is just a dummy trait that is implemented for all types;
/// the safety invariants are checked with debug assertions instead.
///
/// You should not normally 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 or
/// likewise, this should be negatively implemented for that.
///
/// This can easily be accomplished with an `PhantomData<AutoreleasePool>`
/// if the `unstable_autoreleasesafe` feature is enabled.
pub unsafe trait AutoreleaseSafe {}
}

#[cfg(not(feature = "unstable_autoreleasesafe"))]
unsafe impl<T> AutoreleaseSafe for T {}

This corresponds to `@autoreleasepool` blocks in Objective-C and Swift.
*/
pub fn autoreleasepool<T, F: FnOnce() -> T>(f: F) -> T {
let _context = unsafe { AutoReleaseHelper::new() };
f()
#[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.
///
/// The given reference must not be used in an inner `autoreleasepool`,
/// 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
///
/// 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] };
/// // Lifetime of the returned reference is bounded by the pool
/// unsafe { pool.ptr_as_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 { pool.ptr_as_mut(obj) }
/// # }
/// #
/// let obj = autoreleasepool(|pool| {
/// let obj = needs_lifetime_from_pool(pool);
/// // Use `obj`
/// obj
/// });
/// ```
///
/// Incorrect usage which panics because we tried to pass an outer pool to an
/// inner pool:
///
#[cfg_attr(feature = "unstable_autoreleasesafe", doc = "```rust,compile_fail")]
#[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 {
/// # let obj: *mut Object = unsafe { msg_send![class!(NSObject), new] };
/// # let obj: *mut Object = unsafe { msg_send![obj, autorelease] };
/// # unsafe { pool.ptr_as_mut(obj) }
/// # }
/// #
/// autoreleasepool(|outer_pool| {
/// let obj = autoreleasepool(|inner_pool| {
/// let obj = needs_lifetime_from_pool(outer_pool);
/// obj
/// });
/// // `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.
/// });
/// ```
#[doc(alias = "@autoreleasepool")]
pub fn autoreleasepool<T, F>(f: F) -> T
where
for<'p> F: FnOnce(&'p AutoreleasePool) -> T + AutoreleaseSafe,
{
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<T: AutoreleaseSafe>() {}

#[test]
fn test_autoreleasesafe() {
requires_autoreleasesafe::<usize>();
requires_autoreleasesafe::<*mut Object>();
requires_autoreleasesafe::<&mut Object>();
}
}
6 changes: 3 additions & 3 deletions objc/src/rc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,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();
Expand All @@ -42,7 +42,7 @@ mod autorelease;
mod strong;
mod weak;

pub use self::autorelease::autoreleasepool;
pub use self::autorelease::{autoreleasepool, AutoreleasePool, AutoreleaseSafe};
pub use self::strong::StrongPtr;
pub use self::weak::WeakPtr;

Expand Down Expand Up @@ -105,7 +105,7 @@ mod tests {
}
let cloned = obj.clone();

autoreleasepool(|| {
autoreleasepool(|_| {
obj.autorelease();
assert!(retain_count(*cloned) == 2);
});
Expand Down

0 comments on commit 1f82d5b

Please sign in to comment.