Skip to content

Commit

Permalink
Add MainThreadBound
Browse files Browse the repository at this point in the history
  • Loading branch information
madsmtm committed Jan 18, 2023
1 parent 01778b4 commit 5f3a8f0
Show file tree
Hide file tree
Showing 5 changed files with 257 additions and 2 deletions.
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion crates/icrate/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ license = "MIT"
[dependencies]
objc2 = { path = "../objc2", version = "=0.3.0-beta.4", default-features = false, optional = true }
block2 = { path = "../block2", version = "=0.2.0-alpha.7", default-features = false, optional = true }
dispatch = { version = "0.2.0", optional = true }

[package.metadata.docs.rs]
default-target = "x86_64-apple-darwin"
features = ["block", "objective-c", "unstable-frameworks-all", "unstable-private", "unstable-docsrs"]
features = ["block", "objective-c", "dispatch", "unstable-frameworks-all", "unstable-private", "unstable-docsrs"]

targets = [
# MacOS
Expand Down
3 changes: 3 additions & 0 deletions crates/icrate/src/Foundation/additions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ pub use self::geometry::{
};
pub use self::range::NSRange;
#[cfg(feature = "Foundation_NSThread")]
#[cfg(feature = "dispatch")]
pub use self::thread::MainThreadBound;
#[cfg(feature = "Foundation_NSThread")]
pub use self::thread::{is_main_thread, is_multi_threaded, MainThreadMarker};

mod array;
Expand Down
194 changes: 193 additions & 1 deletion crates/icrate/src/Foundation/additions/thread.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#![cfg(feature = "Foundation_NSThread")]
use core::fmt;
use core::marker::PhantomData;
use core::mem::{self, ManuallyDrop};
use core::panic::{RefUnwindSafe, UnwindSafe};

use crate::common::*;
Expand Down Expand Up @@ -72,7 +73,7 @@ fn make_multithreaded() {
/// }
///
/// // Usage
/// let mtm = MainThreadMarker::new().unwrap();
/// let mtm = MainThreadMarker::new().expect("must be on the main thread");
/// unsafe { do_thing(obj, mtm) }
/// ```
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
Expand All @@ -88,6 +89,7 @@ impl MainThreadMarker {
///
/// Returns [`None`] if the current thread was not the main thread.
#[cfg(feature = "Foundation_NSThread")]
#[inline]
pub fn new() -> Option<Self> {
if NSThread::isMainThread_class() {
// SAFETY: We just checked that we are running on the main thread.
Expand All @@ -100,17 +102,207 @@ impl MainThreadMarker {
/// Construct a new [`MainThreadMarker`] without first checking whether
/// the current thread is the main one.
///
///
/// # Safety
///
/// The current thread must be the main thread.
///
/// Alternatively, you may create this briefly if you know that a an API
/// is safe in a specific case, but is not marked so. If you do that, you
/// must ensure that any use of the marker is actually safe to do from
/// another thread than the main one.
#[inline]
pub unsafe fn new_unchecked() -> Self {
// SAFETY: Upheld by caller
//
// We can't debug_assert that this actually is the main thread, see
// the comment above.
Self { _priv: PhantomData }
}

/// Submit the given closure to the runloop on the main thread.
///
/// If the current thread is the main thread, this simply runs the
/// closure.
///
/// The closure is passed a [`MainThreadMarker`] that it can further use
/// to access APIs that are only accessible from the main thread.
///
/// This function should only be used in applications whose main thread is
/// running an event loop with `dispatch_main`, `UIApplicationMain`,
/// `NSApplicationMain`, `CFRunLoop` or similar; it will block
/// indefinitely if that is not the case.
///
///
/// # Example
///
/// ```no_run
/// use icrate::Foundation::MainThreadMarker;
/// MainThreadMarker::run_on_main(|mtm| {
/// // Do something on the main thread with the given marker
/// });
/// ```
#[cfg(feature = "dispatch")]
pub fn run_on_main<F, R>(f: F) -> R
where
F: Send + FnOnce(MainThreadMarker) -> R,
R: Send,
{
if let Some(mtm) = MainThreadMarker::new() {
f(mtm)
} else {
dispatch::Queue::main().exec_sync(|| {
// SAFETY: The outer closure is submitted to run on the main
// thread, so now, when the closure actually runs, it's
// guaranteed to be on the main thread.
f(unsafe { MainThreadMarker::new_unchecked() })
})
}
}
}

impl fmt::Debug for MainThreadMarker {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("MainThreadMarker").finish()
}
}

/// Make a type that can only be used on the main thread be `Send` + `Sync`.
///
/// On `Drop`, the inner type is sent to the main thread's runloop and dropped
/// there. This may lead to deadlocks if the main runloop is not running, or
/// if it is waiting on a lock that the dropping thread is holding. See
/// [`MainThreadMarker::run_on_main`] for some of the caveats around that.
///
///
/// # Related
///
/// This type takes inspiration from `threadbound::ThreadBound`.
///
/// The functionality also somewhat resembles Swift's `@MainActor`, which
/// ensures that a type is only usable from the main thread.
#[doc(alias = "@MainActor")]
#[cfg(feature = "dispatch")]
pub struct MainThreadBound<T>(ManuallyDrop<T>);

// SAFETY: The inner value is guaranteed to originate from the main thread
// because `new` takes [`MainThreadMarker`].
//
// `into_inner` is the only way to get the value out, and that is also
// guaranteed to happen on the main thread.
//
// Finally, the value is dropped on the main thread in `Drop`.
#[cfg(feature = "dispatch")]
unsafe impl<T> Send for MainThreadBound<T> {}

// SAFETY: We only provide access to the inner value via. `get` and `get_mut`.
//
// Both of these take [`MainThreadMarker`], which guarantees that the access
// is done from the main thread.
#[cfg(feature = "dispatch")]
unsafe impl<T> Sync for MainThreadBound<T> {}

#[cfg(feature = "dispatch")]
impl<T> Drop for MainThreadBound<T> {
fn drop(&mut self) {
if mem::needs_drop::<T>() {
// TODO: Figure out whether we should assume the main thread to be
// dead if we're panicking, and just leak instead?
MainThreadMarker::run_on_main(|_mtm| {
let this = self;
// SAFETY: The value is dropped on the main thread, which is
// the same thread that it originated from (guaranteed by
// `new` taking `MainThreadMarker`).
//
// Additionally, the value is never used again after this
// point.
unsafe { ManuallyDrop::drop(&mut this.0) };
})
}
}
}

/// Main functionality.
#[cfg(feature = "dispatch")]
impl<T> MainThreadBound<T> {
/// Create a new [`MainThreadBound`] value of type `T`.
///
///
/// # Example
///
/// ```no_run
/// use icrate::Foundation::{MainThreadMarker, MainThreadBound};
///
/// let foo;
/// # foo = ();
/// let mtm = MainThreadMarker::new().expect("must be on the main thread");
/// let foo = MainThreadBound::new(foo, mtm);
///
/// // `foo` is now `Send + Sync`.
/// ```
#[inline]
pub fn new(inner: T, _mtm: MainThreadMarker) -> Self {
Self(ManuallyDrop::new(inner))
}

/// Returns a reference to the value.
#[inline]
pub fn get(&self, _mtm: MainThreadMarker) -> &T {
&self.0
}

/// Returns a mutable reference to the value.
#[inline]
pub fn get_mut(&mut self, _mtm: MainThreadMarker) -> &mut T {
&mut self.0
}

/// Extracts the value from the [`MainThreadBound`] container.
#[inline]
pub fn into_inner(self, _mtm: MainThreadMarker) -> T {
// Prevent our `Drop` impl from running.
//
// This is a bit confusing, now `this` is:
// `ManuallyDrop<Self(ManuallyDrop<T>)>`
let mut this = ManuallyDrop::new(self);

// SAFETY: `self` is consumed by this function, and wrapped in
// `ManuallyDrop`, so the item's destructor is never run.
unsafe { ManuallyDrop::take(&mut this.0) }
}
}

/// Helper functions for running [`MainThreadMarker::run_on_main`].
#[cfg(feature = "dispatch")]
impl<T> MainThreadBound<T> {
/// Access the item on the main thread.
///
/// See [`MainThreadMarker::run_on_main`] for caveats.
#[inline]
pub fn get_on_main<F, R>(&self, f: F) -> R
where
F: Send + FnOnce(&T, MainThreadMarker) -> R,
R: Send,
{
MainThreadMarker::run_on_main(|mtm| f(self.get(mtm), mtm))
}

/// Access the item mutably on the main thread.
///
/// See [`MainThreadMarker::run_on_main`] for caveats.
#[inline]
pub fn get_on_main_mut<F, R>(&mut self, f: F) -> R
where
F: Send + FnOnce(&mut T, MainThreadMarker) -> R,
R: Send,
{
MainThreadMarker::run_on_main(|mtm| f(self.get_mut(mtm), mtm))
}
}

#[cfg(feature = "dispatch")]
impl<T> fmt::Debug for MainThreadBound<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MainThreadBound").finish_non_exhaustive()
}
}
52 changes: 52 additions & 0 deletions crates/icrate/tests/thread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,55 @@ fn test_debug() {
let marker = unsafe { MainThreadMarker::new_unchecked() };
assert_eq!(format!("{marker:?}"), "MainThreadMarker");
}

#[test]
#[cfg(feature = "dispatch")]
fn test_main_thread_bound_traits() {
use icrate::Foundation::MainThreadBound;

struct Foo(*const ());

fn assert_send_sync<T: Send + Sync>() {}

assert_send_sync::<MainThreadBound<MainThreadMarker>>();
assert_send_sync::<MainThreadBound<Foo>>();

fn foo<T>() {
assert_send_sync::<MainThreadBound<T>>();
}

foo::<()>();
}

#[test]
#[cfg(feature = "dispatch")]
fn test_main_thread_bound_into_inner() {
use core::cell::Cell;
use icrate::Foundation::MainThreadBound;

// SAFETY: For testing only
let mtm = unsafe { MainThreadMarker::new_unchecked() };

struct Foo<'a> {
is_dropped: &'a Cell<bool>,
}

impl Drop for Foo<'_> {
fn drop(&mut self) {
self.is_dropped.set(true);
}
}

let is_dropped = Cell::new(false);
let foo = Foo {
is_dropped: &is_dropped,
};
let foo = MainThreadBound::new(foo, mtm);
assert!(!is_dropped.get());

let foo = foo.into_inner(mtm);
assert!(!is_dropped.get());

drop(foo);
assert!(is_dropped.get());
}

0 comments on commit 5f3a8f0

Please sign in to comment.