diff --git a/Cargo.lock b/Cargo.lock index b08d0d932..8607e6f3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -108,6 +108,12 @@ dependencies = [ "libloading", ] +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + [[package]] name = "glob" version = "0.3.1" @@ -158,6 +164,7 @@ name = "icrate" version = "0.0.1" dependencies = [ "block2", + "dispatch", "objc2", ] diff --git a/crates/icrate/Cargo.toml b/crates/icrate/Cargo.toml index e8dee717c..864dd6413 100644 --- a/crates/icrate/Cargo.toml +++ b/crates/icrate/Cargo.toml @@ -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 diff --git a/crates/icrate/src/Foundation/additions/mod.rs b/crates/icrate/src/Foundation/additions/mod.rs index 66d8422e7..d9ff70a47 100644 --- a/crates/icrate/src/Foundation/additions/mod.rs +++ b/crates/icrate/src/Foundation/additions/mod.rs @@ -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; diff --git a/crates/icrate/src/Foundation/additions/thread.rs b/crates/icrate/src/Foundation/additions/thread.rs index eced00c03..3705770b3 100644 --- a/crates/icrate/src/Foundation/additions/thread.rs +++ b/crates/icrate/src/Foundation/additions/thread.rs @@ -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::*; @@ -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)] @@ -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 { if NSThread::isMainThread_class() { // SAFETY: We just checked that we are running on the main thread. @@ -100,13 +102,63 @@ 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: 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 { @@ -114,3 +166,143 @@ impl fmt::Debug for MainThreadMarker { 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(ManuallyDrop); + +// 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 Send for MainThreadBound {} + +// 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 Sync for MainThreadBound {} + +#[cfg(feature = "dispatch")] +impl Drop for MainThreadBound { + fn drop(&mut self) { + if mem::needs_drop::() { + // 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 MainThreadBound { + /// 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)>` + 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 MainThreadBound { + /// Access the item on the main thread. + /// + /// See [`MainThreadMarker::run_on_main`] for caveats. + #[inline] + pub fn get_on_main(&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(&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 fmt::Debug for MainThreadBound { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("MainThreadBound").finish_non_exhaustive() + } +} diff --git a/crates/icrate/tests/thread.rs b/crates/icrate/tests/thread.rs index a98a990a9..c9a4c7e94 100644 --- a/crates/icrate/tests/thread.rs +++ b/crates/icrate/tests/thread.rs @@ -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() {} + + assert_send_sync::>(); + assert_send_sync::>(); + + fn foo() { + assert_send_sync::>(); + } + + 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, + } + + 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()); +}