diff --git a/crates/dispatch2/src/ffi.rs b/crates/dispatch2/src/ffi.rs index cd9eab70c..e65ed8ee8 100644 --- a/crates/dispatch2/src/ffi.rs +++ b/crates/dispatch2/src/ffi.rs @@ -2,11 +2,16 @@ #![allow(missing_docs, non_camel_case_types)] -use core::ffi::{c_long, c_uint, c_ulong, c_void}; -use core::ptr::addr_of; +use core::{ + ffi::{c_long, c_uint, c_ulong, c_void}, + ptr::addr_of, +}; #[cfg(feature = "objc2")] -use objc2::encode::{Encode, Encoding, RefEncode}; +use objc2::{ + encode::{Encode, Encoding, RefEncode}, + Message, +}; // Try to generate as much as possible. pub use crate::generated::*; @@ -29,6 +34,10 @@ macro_rules! create_opaque_type { unsafe impl RefEncode for $type_name { const ENCODING_REF: Encoding = Encoding::Object; } + + #[cfg(feature = "objc2")] + // SAFETY: Dispatch types respond to objc messages. + unsafe impl Message for $type_name {} }; } @@ -108,10 +117,13 @@ create_opaque_type!(dispatch_io_s, dispatch_io_t); /// A dispatch queue that executes blocks serially in FIFO order. pub const DISPATCH_QUEUE_SERIAL: dispatch_queue_attr_t = core::ptr::null_mut(); + /// A dispatch queue that executes blocks concurrently. -pub static DISPATCH_QUEUE_CONCURRENT: &dispatch_queue_attr_s = { +pub static DISPATCH_QUEUE_CONCURRENT: ImmutableStatic = { // Safety: immutable external definition - unsafe { &_dispatch_queue_attr_concurrent } + ImmutableStatic(unsafe { + &_dispatch_queue_attr_concurrent as *const _ as dispatch_queue_attr_t + }) }; pub const DISPATCH_APPLY_AUTO: dispatch_queue_t = core::ptr::null_mut(); @@ -241,3 +253,13 @@ pub extern "C" fn dispatch_get_main_queue() -> dispatch_queue_main_t { // SAFETY: Always safe to get pointer from static, only needed for MSRV. unsafe { addr_of!(_dispatch_main_q) as dispatch_queue_main_t } } + +/// Wrapper type for immutable static variables exported from C, +/// that are documented to be safe for sharing and passing between threads. +#[repr(transparent)] +#[derive(Debug)] +pub struct ImmutableStatic(pub T); +// Safety: safety is guaranteed by the external type. +unsafe impl Sync for ImmutableStatic {} +// Safety: safety is guaranteed by the external type. +unsafe impl Send for ImmutableStatic {} diff --git a/crates/dispatch2/src/group.rs b/crates/dispatch2/src/group.rs index 454118c90..a6f39ca62 100644 --- a/crates/dispatch2/src/group.rs +++ b/crates/dispatch2/src/group.rs @@ -1,38 +1,25 @@ //! Dispatch group definition. use alloc::boxed::Box; -use core::ffi::c_void; -use core::time::Duration; +use core::{ffi::c_void, time::Duration}; -use super::object::DispatchObject; -use super::queue::Queue; -use super::utils::function_wrapper; -use super::{ffi::*, WaitError}; +use super::{ffi::*, function_wrapper, queue::Queue, rc::Retained, AsRawDispatchObject, WaitError}; /// Dispatch group. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub struct Group { - dispatch_object: DispatchObject, + _inner: [u8; 0], } -/// Dispatch group guard. -#[derive(Debug)] -pub struct GroupGuard(Group, bool); - impl Group { /// Creates a new [Group]. - pub fn new() -> Option { + pub fn new() -> Option> { // Safety: valid to call. let object = unsafe { dispatch_group_create() }; + assert!(!object.is_null()); - if object.is_null() { - return None; - } - - // Safety: object cannot be null. - let dispatch_object = unsafe { DispatchObject::new_owned(object.cast()) }; - - Some(Group { dispatch_object }) + // Safety: object must be valid. + unsafe { Retained::from_raw(object.cast()) } } /// Submit a function to a [Queue] and associates it with the [Group]. @@ -101,15 +88,11 @@ impl Group { dispatch_group_enter(self.as_raw()); } - GroupGuard(self.clone(), false) - } + let group = + // Safety: group cannot be null. + unsafe { Retained::retain(self.as_raw().cast()) }.expect("failed to retain semaphore"); - /// Set the finalizer function for the object. - pub fn set_finalizer(&mut self, destructor: F) - where - F: Send + FnOnce(), - { - self.dispatch_object.set_finalizer(destructor); + GroupGuard(group, false) } /// Get the raw [dispatch_group_t] value. @@ -117,12 +100,27 @@ impl Group { /// # Safety /// /// - Object shouldn't be released manually. - pub const unsafe fn as_raw(&self) -> dispatch_group_t { - // SAFETY: Upheld by caller - unsafe { self.dispatch_object.as_raw() } + pub fn as_raw(&self) -> dispatch_group_t { + self as *const Self as _ } } +impl AsRawDispatchObject for Group { + fn as_raw_object(&self) -> dispatch_object_t { + self.as_raw().cast() + } +} + +// Safety: group is inherently safe to move between threads. +unsafe impl Send for Group {} + +// Safety: group is inherently safe to share between threads. +unsafe impl Sync for Group {} + +/// Dispatch group guard. +#[derive(Debug)] +pub struct GroupGuard(Retained, bool); + impl GroupGuard { /// Explicitly indicates that the function in the [Group] finished executing. pub fn leave(mut self) { diff --git a/crates/dispatch2/src/lib.rs b/crates/dispatch2/src/lib.rs index 7eb2066ef..e58874300 100644 --- a/crates/dispatch2/src/lib.rs +++ b/crates/dispatch2/src/lib.rs @@ -33,8 +33,6 @@ extern crate alloc; #[cfg(feature = "std")] extern crate std; -use self::ffi::dispatch_qos_class_t; - pub mod ffi; #[allow(clippy::undocumented_unsafe_blocks)] mod generated; @@ -44,8 +42,23 @@ mod main_thread_bound; pub mod object; mod once; pub mod queue; +pub mod rc; pub mod semaphore; -mod utils; +pub mod workloop; + +#[cfg(feature = "objc2")] +pub use self::main_thread_bound::{run_on_main, MainThreadBound}; +pub use self::once::*; +pub use group::*; +pub use object::*; +pub use queue::*; +pub use semaphore::*; +pub use workloop::*; + +use alloc::boxed::Box; +use core::{ffi::c_void, time::Duration}; + +use ffi::{dispatch_qos_class_t, dispatch_time, dispatch_time_t, DISPATCH_TIME_NOW}; /// Wait error. #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] @@ -58,7 +71,7 @@ pub enum WaitError { } /// Quality of service that specify the priorities for executing tasks. -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] #[non_exhaustive] pub enum QualityOfServiceClass { /// Quality of service for user-interactive tasks. @@ -66,6 +79,7 @@ pub enum QualityOfServiceClass { /// Quality of service for tasks that prevent the user from actively using your app. UserInitiated, /// Default Quality of service. + #[default] Default, /// Quality of service for tasks that the user does not track actively. Utility, @@ -91,10 +105,44 @@ impl From for dispatch_qos_class_t { } } -pub use self::group::*; -#[cfg(feature = "objc2")] -pub use self::main_thread_bound::{run_on_main, MainThreadBound}; -pub use self::object::*; -pub use self::once::*; -pub use self::queue::*; -pub use self::semaphore::*; +impl TryFrom for dispatch_time_t { + type Error = TryFromDurationError; + + fn try_from(value: Duration) -> Result { + let secs = value.as_secs() as i64; + + secs.checked_mul(1_000_000_000) + .and_then(|x| x.checked_add(i64::from(value.subsec_nanos()))) + .map(|delta| { + // Safety: delta cannot overflow + unsafe { dispatch_time(DISPATCH_TIME_NOW, delta) } + }) + .ok_or(Self::Error::TimeOverflow) + } +} + +/// Error returned by [Queue::after]. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[non_exhaustive] +pub enum TryFromDurationError { + /// The given timeout value will result in an overflow when converting to dispatch time. + TimeOverflow, +} + +/// Error returned by [Queue::set_qos_class_floor] or [WorkloopQueue::set_qos_class_floor]. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[non_exhaustive] +pub enum QualityOfServiceClassFloorError { + /// The relative priority is invalid. + InvalidRelativePriority, +} + +pub(crate) extern "C" fn function_wrapper(work_boxed: *mut c_void) +where + F: FnOnce(), +{ + // Safety: we reconstruct from a Box. + let work = unsafe { Box::from_raw(work_boxed.cast::()) }; + + (*work)(); +} diff --git a/crates/dispatch2/src/object.rs b/crates/dispatch2/src/object.rs index 4c18959de..1c710bf7e 100644 --- a/crates/dispatch2/src/object.rs +++ b/crates/dispatch2/src/object.rs @@ -2,172 +2,84 @@ use alloc::boxed::Box; -use super::{ffi::*, queue::Queue, utils::function_wrapper, QualityOfServiceClass}; +use super::{ffi::*, function_wrapper, queue::Queue}; -/// Error returned by [DispatchObject::set_target_queue]. -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] -#[non_exhaustive] -pub enum TargetQueueError { - /// The [DispatchObject] is already active. - ObjectAlreadyActive, +/// Types convertible to raw pointer to dispatch object. +pub trait AsRawDispatchObject { + /// Returns a raw pointer to dispatch object. + fn as_raw_object(&self) -> dispatch_object_t; } -/// Error returned by [DispatchObject::set_qos_class_floor]. -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] -#[non_exhaustive] -pub enum QualityOfServiceClassFloorError { - /// The relative priority is invalid. - InvalidRelativePriority, -} - -/// Represent a dispatch object. -#[repr(C)] -#[derive(Debug)] -pub struct DispatchObject { - object: *mut T, - is_activated: bool, -} +/// Objects that inherit from `DispatchObject` +pub trait DispatchObjectLike { + /// Activates the dispatch object. + fn activate(&self); -impl DispatchObject { - /// Create a new owned instance - /// - /// # Safety + /// Resumes the invocation of block objects on a dispatch object. /// - /// - ``object`` is expected to be a dispatch object that is owned. - pub unsafe fn new_owned(object: *mut T) -> Self { - Self { - object, - is_activated: false, - } - } + /// Calling this function decrements the suspension count. + /// The object remains suspended while the count is greater than zero. + fn resume(&self); - /// Create a new shared instance + /// Suspends the invocation of block objects on a dispatch object. /// - /// # Safety - /// - /// - ``object`` is expected to be a dispatch object that is shared. - pub unsafe fn new_shared(object: *mut T) -> Self { - let result = Self { - object, - is_activated: false, - }; - - // Safety: We own a reference to the object. - unsafe { - dispatch_retain(result.object.cast()); - } - - result - } - - /// Set the finalizer function for the object. - pub fn set_finalizer(&mut self, destructor: F) - where - F: Send + FnOnce(), - { - let destructor_boxed = Box::into_raw(Box::new(destructor)).cast(); - - // Safety: As this use the dispatch object's context, and because we need some way to wrap the Rust function, we set the context. - // Once the finalizer is executed, the context will be dangling. - // This isn't an issue as the context shall not be accessed after the dispatch object is destroyed. - unsafe { - dispatch_set_context(self.object.cast(), destructor_boxed); - dispatch_set_finalizer_f(self.object.cast(), function_wrapper::) - } - } - - /// Set the target [Queue] of this object. + /// Calling this function increments the suspension count. + /// The object remains suspended while the count count is greater than zero. /// /// # Safety - /// - /// - DispatchObject should be a queue or queue source. - pub unsafe fn set_target_queue(&self, queue: &Queue) -> Result<(), TargetQueueError> { - if self.is_activated { - return Err(TargetQueueError::ObjectAlreadyActive); - } - - // SAFETY: object and queue cannot be null. - unsafe { - dispatch_set_target_queue(self.as_raw().cast(), queue.as_raw()); - } - - Ok(()) - } + /// It is a programmer error to release an object that is currently suspended + unsafe fn suspend(&self); - /// Set the QOS class floor on a dispatch queue, source or workloop. + /// Specifies the dispatch queue on which to perform work associated with the current object. /// /// # Safety - /// - /// - DispatchObject should be a queue or queue source. - pub unsafe fn set_qos_class_floor( - &self, - qos_class: QualityOfServiceClass, - relative_priority: i32, - ) -> Result<(), QualityOfServiceClassFloorError> { - if !(QOS_MIN_RELATIVE_PRIORITY..=0).contains(&relative_priority) { - return Err(QualityOfServiceClassFloorError::InvalidRelativePriority); - } + /// - When setting up target queues, it is a programmer error to create cycles in the dispatch queue hierarchy. + /// In other words, don't set the target of queue A to queue B and the target of queue B to queue A. + /// - Once a dispatch object has been activated, it cannot change its target queue. + unsafe fn set_target_queue(&self, target_queue: Queue); - // SAFETY: Safe as relative_priority can only be valid. - unsafe { - dispatch_set_qos_class_floor( - self.as_raw().cast(), - dispatch_qos_class_t::from(qos_class), - relative_priority, - ); - } + /// Sets the finalizer function for a dispatch object. + fn set_finalizer(&mut self, destructor: F) + where + F: Send + FnOnce(); +} - Ok(()) +impl DispatchObjectLike for T +where + T: AsRawDispatchObject, +{ + fn activate(&self) { + // Safety: pointer must be valid. + unsafe { dispatch_activate(self.as_raw_object()) } } - /// Activate the object. - pub fn activate(&mut self) { - // Safety: object cannot be null. - unsafe { - dispatch_activate(self.as_raw().cast()); - } - - self.is_activated = true; + fn resume(&self) { + // Safety: pointer must be valid. + unsafe { dispatch_resume(self.as_raw_object()) }; } - /// Suspend the invocation of functions on the object. - pub fn suspend(&self) { - // Safety: object cannot be null. - unsafe { - dispatch_suspend(self.as_raw().cast()); - } + unsafe fn suspend(&self) { + // Safety: pointer must be valid. + unsafe { dispatch_suspend(self.as_raw_object()) }; } - /// Resume the invocation of functions on the object. - pub fn resume(&self) { - // Safety: object cannot be null. - unsafe { - dispatch_resume(self.as_raw().cast()); - } + unsafe fn set_target_queue(&self, target_queue: Queue) { + // Safety: pointers must be valid. + unsafe { dispatch_set_target_queue(self.as_raw_object(), target_queue.as_raw()) } } - /// Get the raw object value. - /// - /// # Safety - /// - /// - Object shouldn't be released manually. - pub const unsafe fn as_raw(&self) -> *mut T { - self.object - } -} - -impl Clone for DispatchObject { - fn clone(&self) -> Self { - // Safety: We own a reference to the object. - unsafe { Self::new_shared(self.object) } - } -} + fn set_finalizer(&mut self, destructor: F) + where + F: Send + FnOnce(), + { + let destructor_boxed = Box::into_raw(Box::new(destructor)).cast(); -impl Drop for DispatchObject { - fn drop(&mut self) { - // Safety: We own a reference to the object. + // Safety: As this use the dispatch object's context, and because we need some way to wrap the Rust function, we set the context. + // Once the finalizer is executed, the context will be dangling. + // This isn't an issue as the context shall not be accessed after the dispatch object is destroyed. unsafe { - dispatch_release(self.object.cast()); + dispatch_set_context(self.as_raw_object(), destructor_boxed); + dispatch_set_finalizer_f(self.as_raw_object(), function_wrapper::) } } } diff --git a/crates/dispatch2/src/queue.rs b/crates/dispatch2/src/queue.rs index 6c62a9151..f6b8a57b9 100644 --- a/crates/dispatch2/src/queue.rs +++ b/crates/dispatch2/src/queue.rs @@ -1,23 +1,12 @@ //! Dispatch queue definition. -use alloc::boxed::Box; -use alloc::ffi::CString; -use core::borrow::{Borrow, BorrowMut}; -use core::ops::{Deref, DerefMut}; -use core::ptr::NonNull; -use core::time::Duration; - -use super::object::{DispatchObject, QualityOfServiceClassFloorError, TargetQueueError}; -use super::utils::function_wrapper; -use super::{ffi::*, QualityOfServiceClass}; - -/// Error returned by [Queue::after]. -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] -#[non_exhaustive] -pub enum QueueAfterError { - /// The given timeout value will result in an overflow when converting to dispatch time. - TimeOverflow, -} +use alloc::{boxed::Box, ffi::CString}; +use core::{ptr::NonNull, time::Duration}; + +use super::{ + ffi::*, function_wrapper, rc::Retained, AsRawDispatchObject, QualityOfServiceClass, + QualityOfServiceClassFloorError, TryFromDurationError, +}; /// Queue type attribute. #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] @@ -33,7 +22,7 @@ impl From for dispatch_queue_attr_t { fn from(value: QueueAttribute) -> Self { match value { QueueAttribute::Serial => DISPATCH_QUEUE_SERIAL, - QueueAttribute::Concurrent => DISPATCH_QUEUE_CONCURRENT as *const _ as *mut _, + QueueAttribute::Concurrent => DISPATCH_QUEUE_CONCURRENT.0 as *const _ as *mut _, _ => panic!("Unknown QueueAttribute value: {:?}", value), } } @@ -90,45 +79,22 @@ impl GlobalQueueIdentifier { } } -/// Auto release frequency for [WorkloopQueue::set_autorelease_frequency]. -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] -#[non_exhaustive] -pub enum DispatchAutoReleaseFrequency { - /// Inherit autorelease frequency from the target [Queue]. - Inherit, - /// Configure an autorelease pool before the execution of a function and releases the objects in that pool after the function finishes executing. - WorkItem, - /// Never setup an autorelease pool. - Never, -} - -impl From for dispatch_autorelease_frequency_t { - fn from(value: DispatchAutoReleaseFrequency) -> Self { - match value { - DispatchAutoReleaseFrequency::Inherit => { - dispatch_autorelease_frequency_t::DISPATCH_AUTORELEASE_FREQUENCY_INHERIT - } - DispatchAutoReleaseFrequency::WorkItem => { - dispatch_autorelease_frequency_t::DISPATCH_AUTORELEASE_FREQUENCY_WORK_ITEM - } - DispatchAutoReleaseFrequency::Never => { - dispatch_autorelease_frequency_t::DISPATCH_AUTORELEASE_FREQUENCY_NEVER - } - _ => panic!("Unknown DispatchAutoReleaseFrequency value: {:?}", value), - } +impl Default for GlobalQueueIdentifier { + fn default() -> Self { + Self::QualityOfService(QualityOfServiceClass::default()) } } /// Dispatch queue. -#[derive(Debug, Clone)] +#[repr(transparent)] +#[derive(Debug, Copy, Clone)] pub struct Queue { - dispatch_object: DispatchObject, - is_workloop: bool, + _inner: [u8; 0], } impl Queue { /// Create a new [Queue]. - pub fn new(label: &str, queue_attribute: QueueAttribute) -> Self { + pub fn new(label: &str, queue_attribute: QueueAttribute) -> Retained { let label = CString::new(label).expect("Invalid label!"); // Safety: label and queue_attribute can only be valid. @@ -136,19 +102,16 @@ impl Queue { dispatch_queue_create(label.as_ptr(), dispatch_queue_attr_t::from(queue_attribute)) }; - assert!(!object.is_null(), "dispatch_queue_create shouldn't fail!"); - - // Safety: object cannot be null. - let dispatch_object = unsafe { DispatchObject::new_owned(object.cast()) }; - - Queue { - dispatch_object, - is_workloop: false, - } + // Safety: object must be valid. + unsafe { Retained::from_raw(object.cast()) }.expect("dispatch_queue_create failed") } /// Create a new [Queue] with a given target [Queue]. - pub fn new_with_target(label: &str, queue_attribute: QueueAttribute, target: &Queue) -> Self { + pub fn new_with_target( + label: &str, + queue_attribute: QueueAttribute, + target: &Queue, + ) -> Retained { let label = CString::new(label).expect("Invalid label!"); // Safety: label, queue_attribute and target can only be valid. @@ -156,58 +119,35 @@ impl Queue { dispatch_queue_create_with_target( label.as_ptr(), dispatch_queue_attr_t::from(queue_attribute), - target.dispatch_object.as_raw(), + target.as_raw(), ) }; - assert!(!object.is_null(), "dispatch_queue_create shouldn't fail!"); - - // Safety: object cannot be null. - let dispatch_object = unsafe { DispatchObject::new_owned(object.cast()) }; + assert!(!object.is_null()); // NOTE: dispatch_queue_create_with_target is in charge of retaining the target Queue. - Queue { - dispatch_object, - is_workloop: false, - } + // Safety: object must be valid. + unsafe { Retained::from_raw(object.cast()) }.expect("dispatch_queue_create failed") } /// Return a system-defined global concurrent [Queue] with the priority derived from [GlobalQueueIdentifier]. - pub fn global_queue(identifier: GlobalQueueIdentifier) -> Self { - let raw_identifier = identifier.to_identifier(); - + pub fn global_queue(identifier: GlobalQueueIdentifier) -> Retained { // Safety: raw_identifier cannot be invalid, flags is reserved. - let object = unsafe { dispatch_get_global_queue(raw_identifier, 0) }; + let object = unsafe { dispatch_get_global_queue(identifier.to_identifier(), 0) }; + assert!(!object.is_null()); - assert!( - !object.is_null(), - "dispatch_get_global_queue shouldn't fail!" - ); - - // Safety: object cannot be null. - let dispatch_object = unsafe { DispatchObject::new_shared(object.cast()) }; - - Queue { - dispatch_object, - is_workloop: false, - } + // Safety: object must be valid. + unsafe { Retained::from_raw(object.cast()) }.expect("dispatch_get_global_queue failed") } /// Return the main queue. - pub fn main() -> Self { - // Safety: raw_identifier cannot be invalid, flags is reserved. + pub fn main() -> Retained { let object = dispatch_get_main_queue(); + assert!(!object.is_null()); - assert!(!object.is_null(), "dispatch_get_main_queue shouldn't fail!"); - - // Safety: object cannot be null. - let dispatch_object = unsafe { DispatchObject::new_shared(object.cast()) }; - - Queue { - dispatch_object, - is_workloop: false, - } + // Safety: object must be valid. + unsafe { Retained::from_raw(object.cast()) }.expect("dispatch_get_main_queue failed") } /// Submit a function for synchronous execution on the [Queue]. @@ -215,8 +155,6 @@ impl Queue { where F: Send + FnOnce(), { - assert!(!self.is_workloop, "exec_sync is invalid for WorkloopQueue"); - let work_boxed = Box::into_raw(Box::new(work)).cast(); // Safety: object cannot be null and work is wrapped to avoid ABI incompatibility. @@ -226,7 +164,7 @@ impl Queue { /// Submit a function for asynchronous execution on the [Queue]. pub fn exec_async(&self, work: F) where - F: Send + FnOnce(), + F: Send + FnOnce() + 'static, { let work_boxed = Box::into_raw(Box::new(work)).cast(); @@ -235,12 +173,11 @@ impl Queue { } /// Enqueue a function for execution at the specified time on the [Queue]. - pub fn after(&self, wait_time: Duration, work: F) -> Result<(), QueueAfterError> + pub fn after(&self, wait_time: Duration, work: F) -> Result<(), TryFromDurationError> where - F: Send + FnOnce(), + F: Send + FnOnce() + 'static, { - let when = - dispatch_time_t::try_from(wait_time).map_err(|_| QueueAfterError::TimeOverflow)?; + let when = dispatch_time_t::try_from(wait_time)?; let work_boxed = Box::into_raw(Box::new(work)).cast(); // Safety: object cannot be null and work is wrapped to avoid ABI incompatibility. @@ -254,7 +191,7 @@ impl Queue { /// Enqueue a barrier function for asynchronous execution on the [Queue] and return immediately. pub fn barrier_async(&self, work: F) where - F: Send + FnOnce(), + F: Send + FnOnce() + 'static, { let work_boxed = Box::into_raw(Box::new(work)).cast(); @@ -308,46 +245,26 @@ impl Queue { } } - /// Set the finalizer function for the [Queue]. - pub fn set_finalizer(&mut self, destructor: F) - where - F: Send + FnOnce(), - { - self.dispatch_object.set_finalizer(destructor); - } - - /// Set the target [Queue] of this [Queue]. - pub fn set_target_queue(&self, queue: &Queue) -> Result<(), TargetQueueError> { - // Safety: We are in Queue instance. - unsafe { self.dispatch_object.set_target_queue(queue) } - } - /// Set the QOS class floor of the [Queue]. pub fn set_qos_class_floor( &self, qos_class: QualityOfServiceClass, relative_priority: i32, ) -> Result<(), QualityOfServiceClassFloorError> { - // Safety: We are in Queue instance. - unsafe { - self.dispatch_object - .set_qos_class_floor(qos_class, relative_priority) + if !(QOS_MIN_RELATIVE_PRIORITY..=0).contains(&relative_priority) { + return Err(QualityOfServiceClassFloorError::InvalidRelativePriority); } - } - - /// Activate the [Queue]. - pub fn activate(&mut self) { - self.dispatch_object.activate(); - } - /// Suspend the invocation of functions on the [Queue]. - pub fn suspend(&self) { - self.dispatch_object.suspend(); - } + // SAFETY: Safe as relative_priority can only be valid. + unsafe { + dispatch_set_qos_class_floor( + self.as_raw_object(), + dispatch_qos_class_t::from(qos_class), + relative_priority, + ); + } - /// Resume the invocation of functions on the [Queue]. - pub fn resume(&self) { - self.dispatch_object.resume(); + Ok(()) } /// Get the raw [dispatch_queue_t] value. @@ -355,107 +272,98 @@ impl Queue { /// # Safety /// /// - Object shouldn't be released manually. - pub const unsafe fn as_raw(&self) -> dispatch_queue_t { - // SAFETY: Upheld by caller. - unsafe { self.dispatch_object.as_raw() } + pub fn as_raw(&self) -> dispatch_queue_t { + self as *const Self as _ } } -/// Dispatch workloop queue. -#[derive(Debug, Clone)] -pub struct WorkloopQueue { - queue: Queue, +impl AsRawDispatchObject for Queue { + fn as_raw_object(&self) -> dispatch_object_t { + self.as_raw().cast() + } } -impl WorkloopQueue { - /// Create a new [WorkloopQueue]. - pub fn new(label: &str, inactive: bool) -> Self { - let label = CString::new(label).expect("Invalid label!"); +// Safety: it's safe to move queue between threads. +unsafe impl Send for Queue {} - // Safety: label can only be valid. - let object = unsafe { - if inactive { - dispatch_workloop_create_inactive(label.as_ptr()) - } else { - dispatch_workloop_create(label.as_ptr()) - } - }; +// Safety: it's safe to share queue between threads. +unsafe impl Sync for Queue {} - assert!(!object.is_null(), "dispatch_queue_create shouldn't fail!"); +#[cfg(test)] +mod tests { + use std::sync::mpsc; - // Safety: object cannot be null. - let dispatch_object = unsafe { DispatchObject::new_owned(object.cast()) }; + use super::*; - WorkloopQueue { - queue: Queue { - dispatch_object, - is_workloop: true, - }, - } + #[test] + fn test_create_main_queue() { + let _ = Queue::main(); } - /// Configure how the [WorkloopQueue] manage the autorelease pools for the functions it executes. - pub fn set_autorelease_frequency(&self, frequency: DispatchAutoReleaseFrequency) { - // Safety: object and frequency can only be valid. - unsafe { - dispatch_workloop_set_autorelease_frequency( - self.as_raw(), - dispatch_autorelease_frequency_t::from(frequency), - ); - } + #[test] + fn test_serial_queue() { + let queue = Queue::new("com.github.madsmtm.objc2", QueueAttribute::Serial); + let (tx, rx) = mpsc::channel(); + queue.exec_async(move || { + tx.send(()).unwrap(); + }); + rx.recv().unwrap(); } - /// Get the raw [dispatch_workloop_t] value. - /// - /// # Safety - /// - /// - Object shouldn't be released manually. - pub const unsafe fn as_raw(&self) -> dispatch_workloop_t { - // SAFETY: Upheld by caller. - unsafe { self.queue.as_raw() as dispatch_workloop_t } - } -} - -impl Deref for WorkloopQueue { - type Target = Queue; - - #[inline] - fn deref(&self) -> &Self::Target { - &self.queue - } -} - -impl DerefMut for WorkloopQueue { - #[inline] - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.queue + #[test] + fn test_concurrent_queue() { + let queue = Queue::new("com.github.madsmtm.objc2", QueueAttribute::Concurrent); + let (tx, rx) = mpsc::channel(); + let cloned_tx = tx.clone(); + queue.exec_async(move || { + tx.send(()).unwrap(); + }); + queue.exec_async(move || { + cloned_tx.send(()).unwrap(); + }); + for _ in 0..2 { + rx.recv().unwrap(); + } } -} -impl AsRef for WorkloopQueue { - #[inline] - fn as_ref(&self) -> &Queue { - self + #[test] + fn test_global_default_queue() { + let queue = Queue::global_queue(GlobalQueueIdentifier::default()); + let (tx, rx) = mpsc::channel(); + queue.exec_async(move || { + tx.send(()).unwrap(); + }); + rx.recv().unwrap(); } -} -impl AsMut for WorkloopQueue { - #[inline] - fn as_mut(&mut self) -> &mut Queue { - &mut *self - } -} - -impl Borrow for WorkloopQueue { - #[inline] - fn borrow(&self) -> &Queue { - self + #[test] + fn test_share_queue_across_threads() { + let queue = Queue::new("com.github.madsmtm.objc2", QueueAttribute::Serial); + let (tx, rx) = mpsc::channel(); + let cloned_tx = tx.clone(); + let cloned_queue = queue.clone(); + queue.exec_async(move || { + cloned_queue.exec_async(move || { + cloned_tx.send(()).unwrap(); + }); + }); + queue.exec_async(move || { + tx.send(()).unwrap(); + }); + for _ in 0..2 { + rx.recv().unwrap(); + } } -} -impl BorrowMut for WorkloopQueue { - #[inline] - fn borrow_mut(&mut self) -> &mut Queue { - &mut *self + #[test] + fn test_move_queue_between_threads() { + let queue = Queue::new("com.github.madsmtm.objc2", QueueAttribute::Serial); + let (tx, rx) = mpsc::channel(); + std::thread::spawn(move || { + queue.exec_async(move || { + tx.send(()).unwrap(); + }); + }); + rx.recv().unwrap(); } } diff --git a/crates/dispatch2/src/rc.rs b/crates/dispatch2/src/rc.rs new file mode 100644 index 000000000..8b73f1abe --- /dev/null +++ b/crates/dispatch2/src/rc.rs @@ -0,0 +1,100 @@ +//! Smart pointer definitions used by libdispatch. + +use core::{fmt, ops::Deref, ptr::NonNull}; + +use super::ffi::*; + +/// Smart pointer based on libdispatch reference counting system. +#[repr(transparent)] +pub struct Retained { + ptr: NonNull, +} + +impl Retained { + /// Create new smart pointer assuming the ownership over the object. + /// The retain count will stay the same. + /// + /// # Safety + /// Must be a valid pointer to the dispatch object having a retain count of +1. + pub unsafe fn from_raw(ptr: *mut T) -> Option { + NonNull::new(ptr).map(|ptr| Self { ptr }) + } + + /// Create new smart pointer with shared ownership. + /// Increments reference counter by 1. + /// + /// # Safety + /// Must be a valid pointer to the dispatch object. + #[allow(unused)] + pub unsafe fn retain(ptr: *mut T) -> Option { + NonNull::new(ptr).map(|ptr| { + // Safety: upheld by the caller + unsafe { dispatch_retain(ptr.as_ptr().cast()) }; + Self { ptr } + }) + } + + /// Returns the pointer to retained object. + #[inline] + pub fn as_ptr(this: &Self) -> *const T { + this.ptr.as_ptr() + } +} + +impl Drop for Retained { + fn drop(&mut self) { + // Safety: the pointer must be valid. + unsafe { dispatch_release(self.ptr.as_ptr().cast()) }; + } +} + +impl Clone for Retained { + /// Retain the object, increasing its reference count. + #[inline] + fn clone(&self) -> Self { + // Safety: upheld by the caller. + unsafe { dispatch_retain(self.ptr.as_ptr().cast()) }; + Self { ptr: self.ptr } + } +} + +impl fmt::Pointer for Retained { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Pointer::fmt(&self.ptr.as_ptr(), f) + } +} + +impl Deref for Retained { + type Target = T; + + /// Obtain an immutable reference to the object. + // Box doesn't inline, but that's because it's a compiler built-in + #[inline] + fn deref(&self) -> &T { + // SAFETY: The pointer's validity is verified when the type is + // created. + unsafe { self.ptr.as_ref() } + } +} + +impl fmt::Debug for Retained { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.ptr.as_ptr().fmt(f) + } +} + +// Safety: inherently safe to move between threads. +unsafe impl Send for Retained {} + +#[cfg(feature = "objc2")] +impl From> for objc2::rc::Retained +where + T: objc2::Message, +{ + fn from(value: Retained) -> Self { + // Safety: upheld by the caller + unsafe { + objc2::rc::Retained::retain(Retained::as_ptr(&value).cast_mut()).expect("cannot be nil") + } + } +} diff --git a/crates/dispatch2/src/semaphore.rs b/crates/dispatch2/src/semaphore.rs index 88efa40c1..ab6affe39 100644 --- a/crates/dispatch2/src/semaphore.rs +++ b/crates/dispatch2/src/semaphore.rs @@ -2,21 +2,20 @@ use core::time::Duration; -use super::ffi::*; -use super::object::DispatchObject; -use super::WaitError; +use super::{ffi::*, rc::Retained, AsRawDispatchObject, WaitError}; /// Dispatch semaphore. -#[derive(Debug, Clone)] +#[repr(transparent)] +#[derive(Debug, Clone, Copy)] pub struct Semaphore { - dispatch_object: DispatchObject, + _inner: [u8; 0], } impl Semaphore { /// Creates a new [Semaphore] with an initial value. /// /// Returns None if value is negative or if creation failed. - pub fn new(value: isize) -> Option { + pub fn new(value: isize) -> Option> { // Per documentation creating a semaphore with a negative size isn't allowed. if value < 0 { return None; @@ -25,14 +24,8 @@ impl Semaphore { // Safety: value is valid let object = unsafe { dispatch_semaphore_create(value) }; - if object.is_null() { - return None; - } - - // Safety: object cannot be null. - let dispatch_object = unsafe { DispatchObject::new_owned(object.cast()) }; - - Some(Semaphore { dispatch_object }) + // Safety: retained accepts null pointer. + unsafe { Retained::from_raw(object.cast()) } } /// Attempt to acquire the [Semaphore] and return a [SemaphoreGuard]. @@ -52,34 +45,41 @@ impl Semaphore { // Safety: Semaphore cannot be null. let result = unsafe { dispatch_semaphore_wait(self.as_raw(), timeout) }; + let sema = + // Safety: semaphore cannot be null. + unsafe { Retained::retain(self.as_raw().cast()) }.expect("failed to retain semaphore"); + match result { - 0 => Ok(SemaphoreGuard(self.clone(), false)), + 0 => Ok(SemaphoreGuard(sema, false)), _ => Err(WaitError::Timeout), } } - /// Set the finalizer function for the object. - pub fn set_finalizer(&mut self, destructor: F) - where - F: Send + FnOnce(), - { - self.dispatch_object.set_finalizer(destructor); - } - /// Get the raw [dispatch_semaphore_t] value. /// /// # Safety /// /// - Object shouldn't be released manually. - pub const unsafe fn as_raw(&self) -> dispatch_semaphore_t { - // SAFETY: Upheld by caller. - unsafe { self.dispatch_object.as_raw() } + pub fn as_raw(&self) -> dispatch_semaphore_t { + self as *const Self as _ + } +} + +impl AsRawDispatchObject for Semaphore { + fn as_raw_object(&self) -> dispatch_object_t { + self.as_raw().cast() } } +// Safety: semaphore is inherently safe to move between threads. +unsafe impl Send for Semaphore {} + +// Safety: semaphore is inherently safe to share between threads. +unsafe impl Sync for Semaphore {} + /// Dispatch semaphore guard. #[derive(Debug)] -pub struct SemaphoreGuard(Semaphore, bool); +pub struct SemaphoreGuard(Retained, bool); impl SemaphoreGuard { /// Release the [Semaphore]. diff --git a/crates/dispatch2/src/utils.rs b/crates/dispatch2/src/utils.rs deleted file mode 100644 index 38997d674..000000000 --- a/crates/dispatch2/src/utils.rs +++ /dev/null @@ -1,31 +0,0 @@ -use alloc::boxed::Box; -use core::ffi::c_void; -use core::time::Duration; - -use super::ffi::{dispatch_time, dispatch_time_t, DISPATCH_TIME_NOW}; - -impl TryFrom for dispatch_time_t { - type Error = (); - - fn try_from(value: Duration) -> Result { - let secs = value.as_secs() as i64; - - secs.checked_mul(1_000_000_000) - .and_then(|x| x.checked_add(i64::from(value.subsec_nanos()))) - .map(|delta| { - // Safety: delta cannot overflow - unsafe { dispatch_time(DISPATCH_TIME_NOW, delta) } - }) - .ok_or(()) - } -} - -pub(crate) extern "C" fn function_wrapper(work_boxed: *mut c_void) -where - F: FnOnce(), -{ - // Safety: we reconstruct from a Box. - let work = unsafe { Box::from_raw(work_boxed.cast::()) }; - - (*work)(); -} diff --git a/crates/dispatch2/src/workloop.rs b/crates/dispatch2/src/workloop.rs new file mode 100644 index 000000000..f1ce09a62 --- /dev/null +++ b/crates/dispatch2/src/workloop.rs @@ -0,0 +1,222 @@ +//! Dispatch workloop definition. + +use alloc::{boxed::Box, ffi::CString}; +use core::{ptr::NonNull, time::Duration}; + +use super::{ + ffi::*, function_wrapper, rc::Retained, AsRawDispatchObject, QualityOfServiceClass, + QualityOfServiceClassFloorError, TryFromDurationError, +}; + +/// Dispatch workloop queue. +#[repr(transparent)] +#[derive(Debug, Clone, Copy)] +pub struct WorkloopQueue { + _inner: [u8; 0], +} + +impl WorkloopQueue { + /// Create a new [WorkloopQueue]. + pub fn new(label: &str, inactive: bool) -> Retained { + let label = CString::new(label).expect("Invalid label!"); + + // Safety: label can only be valid. + let object = unsafe { + if inactive { + dispatch_workloop_create_inactive(label.as_ptr()) + } else { + dispatch_workloop_create(label.as_ptr()) + } + }; + + // Safety: object must be valid. + unsafe { Retained::from_raw(object.cast()) }.expect("dispatch_queue_create failed") + } + + /// Configure how the [WorkloopQueue] manage the autorelease pools for the functions it executes. + pub fn set_autorelease_frequency(&self, frequency: DispatchAutoReleaseFrequency) { + // Safety: object and frequency can only be valid. + unsafe { + dispatch_workloop_set_autorelease_frequency( + self.as_raw(), + dispatch_autorelease_frequency_t::from(frequency), + ); + } + } + + /// Submit a function for synchronous execution on the [WorkloopQueue]. + pub fn exec_sync(&self, work: F) + where + F: Send + FnOnce(), + { + let work_boxed = Box::into_raw(Box::new(work)).cast(); + + // Safety: object cannot be null and work is wrapped to avoid ABI incompatibility. + unsafe { dispatch_sync_f(self.as_raw().cast(), work_boxed, function_wrapper::) } + } + + /// Submit a function for asynchronous execution on the [WorkloopQueue]. + pub fn exec_async(&self, work: F) + where + F: Send + FnOnce() + 'static, + { + let work_boxed = Box::into_raw(Box::new(work)).cast(); + + // Safety: object cannot be null and work is wrapped to avoid ABI incompatibility. + unsafe { dispatch_async_f(self.as_raw().cast(), work_boxed, function_wrapper::) } + } + + /// Enqueue a function for execution at the specified time on the [WorkloopQueue]. + pub fn after(&self, wait_time: Duration, work: F) -> Result<(), TryFromDurationError> + where + F: Send + FnOnce() + 'static, + { + let when = dispatch_time_t::try_from(wait_time)?; + let work_boxed = Box::into_raw(Box::new(work)).cast(); + + // Safety: object cannot be null and work is wrapped to avoid ABI incompatibility. + unsafe { + dispatch_after_f( + when, + self.as_raw().cast(), + work_boxed, + function_wrapper::, + ); + } + + Ok(()) + } + + /// Enqueue a barrier function for asynchronous execution on the [WorkloopQueue] and return immediately. + pub fn barrier_async(&self, work: F) + where + F: Send + FnOnce() + 'static, + { + let work_boxed = Box::into_raw(Box::new(work)).cast(); + + // Safety: object cannot be null and work is wrapped to avoid ABI incompatibility. + unsafe { dispatch_barrier_async_f(self.as_raw().cast(), work_boxed, function_wrapper::) } + } + + /// Enqueue a barrier function for synchronous execution on the [WorkloopQueue] and wait until that function completes. + pub fn barrier_sync(&self, work: F) + where + F: Send + FnOnce(), + { + let work_boxed = Box::into_raw(Box::new(work)).cast(); + + // Safety: object cannot be null and work is wrapped to avoid ABI incompatibility. + unsafe { dispatch_barrier_sync_f(self.as_raw().cast(), work_boxed, function_wrapper::) } + } + + /// Submit a function for synchronous execution and mark the function as a barrier for subsequent concurrent tasks. + pub fn barrier_async_and_wait(&self, work: F) + where + F: Send + FnOnce(), + { + let work_boxed = Box::into_raw(Box::new(work)).cast(); + + // Safety: object cannot be null and work is wrapped to avoid ABI incompatibility. + unsafe { + dispatch_barrier_async_and_wait_f( + self.as_raw().cast(), + work_boxed, + function_wrapper::, + ) + } + } + + /// Sets a function at the given key that will be executed at [WorkloopQueue] destruction. + pub fn set_specific(&mut self, key: NonNull<()>, destructor: F) + where + F: Send + FnOnce(), + { + let destructor_boxed = Box::into_raw(Box::new(destructor)).cast(); + + // SAFETY: object cannot be null and destructor is wrapped to avoid + // ABI incompatibility. + // + // The key is never dereferenced, so passing _any_ pointer here is + // safe and allowed. + unsafe { + dispatch_queue_set_specific( + self.as_raw().cast(), + key.cast(), + destructor_boxed, + function_wrapper::, + ) + } + } + + /// Set the QOS class floor of the [WorkloopQueue]. + pub fn set_qos_class_floor( + &self, + qos_class: QualityOfServiceClass, + relative_priority: i32, + ) -> Result<(), QualityOfServiceClassFloorError> { + if !(QOS_MIN_RELATIVE_PRIORITY..=0).contains(&relative_priority) { + return Err(QualityOfServiceClassFloorError::InvalidRelativePriority); + } + + // SAFETY: Safe as relative_priority can only be valid. + unsafe { + dispatch_set_qos_class_floor( + self.as_raw_object(), + dispatch_qos_class_t::from(qos_class), + relative_priority, + ); + } + + Ok(()) + } + + /// Get the raw [dispatch_workloop_t] value. + /// + /// # Safety + /// + /// - Object shouldn't be released manually. + pub fn as_raw(&self) -> dispatch_workloop_t { + self as *const Self as _ + } +} + +impl AsRawDispatchObject for WorkloopQueue { + fn as_raw_object(&self) -> dispatch_object_t { + self.as_raw().cast() + } +} + +// Safety: it's safe to move workloop queue between threads. +unsafe impl Send for WorkloopQueue {} + +// Safety: it's safe to share workloop queue between threads. +unsafe impl Sync for WorkloopQueue {} + +/// Auto release frequency for [WorkloopQueue::set_autorelease_frequency]. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[non_exhaustive] +pub enum DispatchAutoReleaseFrequency { + /// Inherit autorelease frequency from the target [crate::Queue]. + Inherit, + /// Configure an autorelease pool before the execution of a function and releases the objects in that pool after the function finishes executing. + WorkItem, + /// Never setup an autorelease pool. + Never, +} + +impl From for dispatch_autorelease_frequency_t { + fn from(value: DispatchAutoReleaseFrequency) -> Self { + match value { + DispatchAutoReleaseFrequency::Inherit => { + dispatch_autorelease_frequency_t::DISPATCH_AUTORELEASE_FREQUENCY_INHERIT + } + DispatchAutoReleaseFrequency::WorkItem => { + dispatch_autorelease_frequency_t::DISPATCH_AUTORELEASE_FREQUENCY_WORK_ITEM + } + DispatchAutoReleaseFrequency::Never => { + dispatch_autorelease_frequency_t::DISPATCH_AUTORELEASE_FREQUENCY_NEVER + } + _ => panic!("Unknown DispatchAutoReleaseFrequency value: {:?}", value), + } + } +}