From b74e32308acbf2289bfa2eb19b9f7f2676fd85b4 Mon Sep 17 00:00:00 2001 From: Andrej Mihajlov Date: Tue, 17 Dec 2024 19:42:55 +0100 Subject: [PATCH] Refactor dispatch2 --- crates/dispatch2/Cargo.toml | 8 +- crates/dispatch2/src/ffi.rs | 20 +- crates/dispatch2/src/group.rs | 68 ++++--- crates/dispatch2/src/lib.rs | 69 ++++++- crates/dispatch2/src/object.rs | 196 +++++------------- crates/dispatch2/src/queue.rs | 323 +++++++++++++----------------- crates/dispatch2/src/rc.rs | 80 ++++++++ crates/dispatch2/src/semaphore.rs | 56 +++--- crates/dispatch2/src/utils.rs | 31 --- crates/dispatch2/src/workloop.rs | 234 ++++++++++++++++++++++ 10 files changed, 648 insertions(+), 437 deletions(-) create mode 100644 crates/dispatch2/src/rc.rs delete mode 100644 crates/dispatch2/src/utils.rs create mode 100644 crates/dispatch2/src/workloop.rs diff --git a/crates/dispatch2/Cargo.toml b/crates/dispatch2/Cargo.toml index ceea6d376..b73a46748 100644 --- a/crates/dispatch2/Cargo.toml +++ b/crates/dispatch2/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "dispatch2" -version = "0.1.0" # Remember to update html_root_url in lib.rs +version = "0.1.0" # Remember to update html_root_url in lib.rs description = "Bindings and wrappers for Apple's Grand Central Dispatch (GCD)" edition.workspace = true rust-version.workspace = true @@ -47,11 +47,7 @@ block2 = ["dep:block2"] libc = ["dep:libc"] objc2 = ["dep:objc2"] -all = [ - "block2", - "libc", - "objc2", -] +all = ["block2", "libc", "objc2"] [package.metadata.release] shared-version = false diff --git a/crates/dispatch2/src/ffi.rs b/crates/dispatch2/src/ffi.rs index 17826b959..07ad3cc5d 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 std::ptr::addr_of; +use std::{ + 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,11 @@ 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 const DISPATCH_QUEUE_CONCURRENT: dispatch_queue_attr_t = { // Safety: immutable external definition - unsafe { &_dispatch_queue_attr_concurrent } + unsafe { &_dispatch_queue_attr_concurrent as *const _ as dispatch_queue_attr_t } }; pub const DISPATCH_APPLY_AUTO: dispatch_queue_t = core::ptr::null_mut(); diff --git a/crates/dispatch2/src/group.rs b/crates/dispatch2/src/group.rs index 7fdcd44d4..1aabff138 100644 --- a/crates/dispatch2/src/group.rs +++ b/crates/dispatch2/src/group.rs @@ -1,38 +1,25 @@ //! Dispatch group definition. -use std::time::Duration; +use std::{ffi::c_void, time::Duration}; -use core::ffi::c_void; - -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)] pub struct Group { - dispatch_object: DispatchObject, + inner: Retained, } -/// Dispatch group guard. -#[derive(Debug)] -pub struct GroupGuard(Group, bool); - impl Group { /// Creates a new [Group]. pub fn new() -> Option { // Safety: valid to call. let object = unsafe { dispatch_group_create() }; - if object.is_null() { - return None; - } - - // Safety: object cannot be null. - let dispatch_object = unsafe { DispatchObject::new_owned(object.cast()) }; + // Safety: retained accepts null pointer. + let inner = unsafe { Retained::from_raw(object)? }; - Some(Group { dispatch_object }) + Some(Group { inner }) } /// Submit a function to a [Queue] and associates it with the [Group]. @@ -104,25 +91,44 @@ impl Group { GroupGuard(self.clone(), false) } - /// 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_group_t] value. /// /// # 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 { + // Safety: Upheld by caller + Retained::as_ptr(&self.inner).cast_mut() } } +impl Clone for Group { + fn clone(&self) -> Self { + Self { + // Safety: pointer must be valid. + inner: unsafe { + Retained::retain(self.as_raw()).expect("failed to retain dispatch_group") + }, + } + } +} + +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(Group, 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 32ccfc3bd..5e105e41e 100644 --- a/crates/dispatch2/src/lib.rs +++ b/crates/dispatch2/src/lib.rs @@ -24,8 +24,6 @@ // Update in Cargo.toml as well. #![doc(html_root_url = "https://docs.rs/dispatch2/0.1.0")] -use self::ffi::dispatch_qos_class_t; - pub mod ffi; #[allow(clippy::undocumented_unsafe_blocks)] mod generated; @@ -35,8 +33,22 @@ mod main_thread_bound; pub mod object; mod once; pub mod queue; +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 std::{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)] @@ -49,7 +61,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. @@ -57,6 +69,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, @@ -82,10 +95,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 5547ee2e7..6024a65f0 100644 --- a/crates/dispatch2/src/object.rs +++ b/crates/dispatch2/src/object.rs @@ -1,171 +1,83 @@ //! Dispatch object definition. -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 6a0a0fa78..1e7081ef3 100644 --- a/crates/dispatch2/src/queue.rs +++ b/crates/dispatch2/src/queue.rs @@ -1,22 +1,11 @@ //! Dispatch queue definition. -use std::borrow::{Borrow, BorrowMut}; -use std::ffi::CString; -use std::ops::{Deref, DerefMut}; -use std::ptr::NonNull; -use std::time::Duration; +use std::{ffi::CString, mem::ManuallyDrop, ptr::NonNull, 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 super::{ + ffi::*, function_wrapper, rc::Retained, AsRawDispatchObject, QualityOfServiceClass, + QualityOfServiceClassFloorError, TryFromDurationError, +}; /// Queue type attribute. #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] @@ -89,40 +78,17 @@ 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)] +#[derive(Debug)] pub struct Queue { - dispatch_object: DispatchObject, - is_workloop: bool, + inner: ManuallyDrop>, + is_global_queue: bool, } impl Queue { @@ -135,14 +101,13 @@ 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()) }; + let inner = + unsafe { Retained::from_raw(object).expect("dispatch_queue_create shouldn't fail!") }; Queue { - dispatch_object, - is_workloop: false, + inner: ManuallyDrop::new(inner), + is_global_queue: false, } } @@ -155,20 +120,19 @@ 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()) }; + let inner = + unsafe { Retained::from_raw(object).expect("dispatch_queue_create shouldn't fail!") }; // NOTE: dispatch_queue_create_with_target is in charge of retaining the target Queue. Queue { - dispatch_object, - is_workloop: false, + inner: ManuallyDrop::new(inner), + is_global_queue: false, } } @@ -179,33 +143,27 @@ impl Queue { // Safety: raw_identifier cannot be invalid, flags is reserved. let object = unsafe { dispatch_get_global_queue(raw_identifier, 0) }; - 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()) }; + let inner = unsafe { + Retained::from_raw(object.cast()).expect("dispatch_get_global_queue shouldn't fail!") + }; Queue { - dispatch_object, - is_workloop: false, + inner: ManuallyDrop::new(inner), + is_global_queue: true, } } /// Return the main queue. pub fn main() -> Self { - // Safety: raw_identifier cannot be invalid, flags is reserved. - let object = dispatch_get_main_queue(); - - 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()) }; - + let inner = unsafe { + Retained::from_raw(dispatch_get_main_queue().cast()) + .expect("dispatch_get_main_queue shouldn't fail!") + }; Queue { - dispatch_object, - is_workloop: false, + inner: ManuallyDrop::new(inner), + is_global_queue: true, } } @@ -214,8 +172,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. @@ -234,12 +190,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(), { - 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. @@ -307,46 +262,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. @@ -354,107 +289,119 @@ 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 { + Retained::as_ptr(&self.inner).cast_mut() } } -/// Dispatch workloop queue. -#[derive(Debug, Clone)] -pub struct WorkloopQueue { - queue: Queue, -} - -impl WorkloopQueue { - /// Create a new [WorkloopQueue]. - pub fn new(label: &str, inactive: bool) -> Self { - 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()) - } - }; - - assert!(!object.is_null(), "dispatch_queue_create shouldn't fail!"); - - // Safety: object cannot be null. - let dispatch_object = unsafe { DispatchObject::new_owned(object.cast()) }; - - WorkloopQueue { - queue: Queue { - dispatch_object, - is_workloop: true, - }, +impl Drop for Queue { + fn drop(&mut self) { + if !self.is_global_queue { + // Safety: do not release global queues as they are singletons. + let _ = unsafe { ManuallyDrop::take(&mut self.inner) }; } } +} - /// 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), - ); +impl Clone for Queue { + fn clone(&self) -> Self { + Self { + // Safety: pointer must be valid. + inner: unsafe { + ManuallyDrop::new( + Retained::retain(self.as_raw()).expect("failed to retain dispatch_queue"), + ) + }, + is_global_queue: self.is_global_queue, } } +} - /// 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 AsRawDispatchObject for Queue { + fn as_raw_object(&self) -> dispatch_object_t { + self.as_raw().cast() } } -impl Deref for WorkloopQueue { - type Target = Queue; +// Safety: it's safe to move queue between threads. +unsafe impl Send for Queue {} + +// Safety: it's safe to share queue between threads. +unsafe impl Sync for Queue {} + +#[cfg(test)] +mod tests { + use std::sync::mpsc; - #[inline] - fn deref(&self) -> &Self::Target { - &self.queue + use super::*; + + #[test] + fn test_create_main_queue() { + let _ = Queue::main(); } -} -impl DerefMut for WorkloopQueue { - #[inline] - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.queue + #[test] + fn test_serial_queue() { + let queue = Queue::new("com.github.madsmtm.objc2", QueueAttribute::Serial); + let (tx, rx) = mpsc::channel(); + queue.exec_async(|| { + tx.send(()).unwrap(); + }); + rx.recv().unwrap(); } -} -impl AsRef for WorkloopQueue { - #[inline] - fn as_ref(&self) -> &Queue { - self + #[test] + fn test_concurrent_queue() { + let queue = Queue::new("com.github.madsmtm.objc2", QueueAttribute::Concurrent); + let (tx, rx) = mpsc::channel(); + queue.exec_async(|| { + tx.send(()).unwrap(); + }); + queue.exec_async(|| { + tx.send(()).unwrap(); + }); + for _ in 0..2 { + rx.recv().unwrap(); + } } -} -impl AsMut for WorkloopQueue { - #[inline] - fn as_mut(&mut self) -> &mut Queue { - &mut *self + #[test] + fn test_global_default_queue() { + let queue = Queue::global_queue(GlobalQueueIdentifier::default()); + let (tx, rx) = mpsc::channel(); + queue.exec_async(|| { + tx.send(()).unwrap(); + }); + rx.recv().unwrap(); } -} -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(); + queue.exec_async(|| { + queue.exec_async(|| { + tx.send(()).unwrap(); + }); + }); + queue.exec_async(|| { + 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 || { + // This has to use move semantics otherwise tx is dropped before the closure can execute causing panic?! + 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..2d39759e1 --- /dev/null +++ b/crates/dispatch2/src/rc.rs @@ -0,0 +1,80 @@ +//! Smart pointer definitions used by libdispatch. + +use std::{fmt, ops::Deref, ptr::NonNull}; + +use super::ffi::*; + +/// Smart pointer based on libdispatch reference counting system. +#[repr(transparent)] +pub(crate) struct Retained { + ptr: NonNull, +} + +impl Retained { + /// Create new smart pointer assuming the ownership over the object. + /// The retain count will stay the same. + pub(crate) 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. + #[allow(unused)] + pub(crate) 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 } + }) + } + + #[inline] + pub(crate) 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 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) + } +} + +#[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 4d302cdfd..ad0a9ed27 100644 --- a/crates/dispatch2/src/semaphore.rs +++ b/crates/dispatch2/src/semaphore.rs @@ -2,14 +2,12 @@ use std::time::Duration; -use super::ffi::*; -use super::object::DispatchObject; -use super::WaitError; +use super::{ffi::*, rc::Retained, AsRawDispatchObject, WaitError}; /// Dispatch semaphore. -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct Semaphore { - dispatch_object: DispatchObject, + inner: Retained, } impl Semaphore { @@ -25,14 +23,12 @@ 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()) }; + // Safety: retained accepts null pointer. + let dispatch_object = unsafe { Retained::from_raw(object)? }; - Some(Semaphore { dispatch_object }) + Some(Semaphore { + inner: dispatch_object, + }) } /// Attempt to acquire the [Semaphore] and return a [SemaphoreGuard]. @@ -58,25 +54,39 @@ impl 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); - } - /// 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 { + Retained::as_ptr(&self.inner).cast_mut() } } +impl Clone for Semaphore { + fn clone(&self) -> Self { + Self { + // Safety: pointer must be valid. + inner: unsafe { + Retained::retain(self.as_raw()).expect("failed to retain dispatch_semaphore_t") + }, + } + } +} + +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); diff --git a/crates/dispatch2/src/utils.rs b/crates/dispatch2/src/utils.rs deleted file mode 100644 index 5c6c08050..000000000 --- a/crates/dispatch2/src/utils.rs +++ /dev/null @@ -1,31 +0,0 @@ -use std::time::Duration; - -use core::ffi::c_void; - -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..884a062f8 --- /dev/null +++ b/crates/dispatch2/src/workloop.rs @@ -0,0 +1,234 @@ +//! Dispatch workloop definition. + +use std::{ffi::CString, ptr::NonNull, time::Duration}; + +use super::{ + ffi::*, function_wrapper, rc::Retained, AsRawDispatchObject, QualityOfServiceClass, + QualityOfServiceClassFloorError, TryFromDurationError, +}; + +/// Dispatch workloop queue. +#[derive(Debug)] +pub struct WorkloopQueue { + inner: Retained, +} + +impl WorkloopQueue { + /// Create a new [WorkloopQueue]. + pub fn new(label: &str, inactive: bool) -> Self { + 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 cannot be null. + let inner = + unsafe { Retained::from_raw(object).expect("failed to create dispatch_workloop") }; + + WorkloopQueue { inner } + } + + /// 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(), + { + 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(), + { + 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(), + { + 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 { + Retained::as_ptr(&self.inner).cast_mut() + } +} + +impl AsRawDispatchObject for WorkloopQueue { + fn as_raw_object(&self) -> dispatch_object_t { + self.as_raw().cast() + } +} + +impl Clone for WorkloopQueue { + fn clone(&self) -> Self { + Self { + // Safety: pointer must be valid. + inner: unsafe { + Retained::retain(self.as_raw()).expect("failed to retain dispatch_workloop") + }, + } + } +} + +// 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), + } + } +}