From f79601313e181562a1dd7af5c844b9128ec1af1a Mon Sep 17 00:00:00 2001 From: Paul Mabileau Date: Tue, 16 Jul 2024 20:29:38 +0200 Subject: [PATCH] Feat(block2): Add new `with_encoding` unsafe block constructors Signed-off-by: Paul Mabileau --- Cargo.lock | 1 + crates/block2/Cargo.toml | 6 ++ crates/block2/src/lib.rs | 2 +- crates/block2/src/rc_block.rs | 53 +++++++++- crates/block2/src/stack.rs | 187 ++++++++++++++++++++++++++++++---- crates/block2/src/traits.rs | 106 +++++++++++++++++++ 6 files changed, 332 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e15ddfcd6..512ceb146 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -75,6 +75,7 @@ name = "block2" version = "0.5.1" dependencies = [ "objc2", + "objc2-foundation", ] [[package]] diff --git a/crates/block2/Cargo.toml b/crates/block2/Cargo.toml index 1e44cd674..ba2f300b1 100644 --- a/crates/block2/Cargo.toml +++ b/crates/block2/Cargo.toml @@ -59,6 +59,12 @@ unstable-private = [] [dependencies] objc2 = { path = "../objc2", version = "0.5.2", default-features = false } +[dev-dependencies.objc2-foundation] +path = "../../framework-crates/objc2-foundation" +version = "0.2.2" +default-features = false +features = ["NSError"] + [package.metadata.docs.rs] default-target = "aarch64-apple-darwin" features = ["unstable-private"] diff --git a/crates/block2/src/lib.rs b/crates/block2/src/lib.rs index cd4d44f39..c9b7fda89 100644 --- a/crates/block2/src/lib.rs +++ b/crates/block2/src/lib.rs @@ -379,7 +379,7 @@ pub use self::block::Block; pub use self::global::GlobalBlock; pub use self::rc_block::RcBlock; pub use self::stack::StackBlock; -pub use self::traits::{BlockFn, IntoBlock}; +pub use self::traits::{BlockFn, IntoBlock, ManualBlockEncoding}; /// Deprecated alias for a `'static` `StackBlock`. #[deprecated = "renamed to `StackBlock`"] diff --git a/crates/block2/src/rc_block.rs b/crates/block2/src/rc_block.rs index b24323ad7..0b871178b 100644 --- a/crates/block2/src/rc_block.rs +++ b/crates/block2/src/rc_block.rs @@ -7,6 +7,7 @@ use objc2::encode::{EncodeArguments, EncodeReturn}; use crate::abi::BlockHeader; use crate::debug::debug_block_header; +use crate::traits::{ManualBlockEncoding, NoBlockEncoding}; use crate::{ffi, Block, IntoBlock, StackBlock}; /// A reference-counted Objective-C block that is stored on the heap. @@ -91,14 +92,58 @@ impl RcBlock { /// /// When the block is called, it will return the value that results from /// calling the closure. - // + #[inline] + pub fn new<'f, A, R, Closure>(closure: Closure) -> Self + where + A: EncodeArguments, + R: EncodeReturn, + Closure: IntoBlock<'f, A, R, Dyn = F>, + { + // SAFETY: no encoding is given. + unsafe { Self::with_encoding::<_, _, _, NoBlockEncoding>(closure) } + } + + /// Constructs a new [`RcBlock`] with the given function and encoding + /// information. + /// + /// See [`StackBlock::with_encoding`] as to why and how this could be + /// useful. The same requirements as [`Self::new`] apply here as well. + /// + /// # Safety + /// + /// The raw encoding string given through `E` must be correct with respect + /// to the given function's input and output types: see + /// [`ManualBlockEncoding`]. + /// + /// # Example + /// + /// ``` + /// # use core::ffi::CStr; + /// # use block2::{Block, ManualBlockEncoding, RcBlock}; + /// # use objc2_foundation::NSError; + /// # + /// struct MyBlockEncoding; + /// unsafe impl ManualBlockEncoding for MyBlockEncoding { + /// type Arguments = (*mut NSError,); + /// type Return = i32; + /// const ENCODING_CSTR: &'static CStr = cr#"i16@?0@"NSError"8"#; + /// } + /// + /// let my_block = unsafe { + /// RcBlock::with_encoding::<_, _, _, MyBlockEncoding>(|_err: *mut NSError| { + /// 42i32 + /// }) + /// }; + /// assert_eq!(my_block.call((std::ptr::null_mut(),)), 42); + /// ``` // Note: Unsure if this should be #[inline], but I think it may be able to // benefit from not being so. - pub fn new<'f, A, R, Closure>(closure: Closure) -> Self + pub unsafe fn with_encoding<'f, A, R, Closure, E>(closure: Closure) -> Self where A: EncodeArguments, R: EncodeReturn, Closure: IntoBlock<'f, A, R, Dyn = F>, + E: ManualBlockEncoding, { // SAFETY: The stack block is copied once below. // @@ -108,7 +153,9 @@ impl RcBlock { // // Clang doesn't do this optimization either. // - let block = unsafe { StackBlock::new_no_clone(closure) }; + // + // Encoding safety is supposed to be upheld by the caller. + let block = unsafe { StackBlock::new_no_clone::(closure) }; // Transfer ownership from the stack to the heap. let mut block = ManuallyDrop::new(block); diff --git a/crates/block2/src/stack.rs b/crates/block2/src/stack.rs index ba2e000a7..f0fb93260 100644 --- a/crates/block2/src/stack.rs +++ b/crates/block2/src/stack.rs @@ -1,4 +1,4 @@ -use core::ffi::c_void; +use core::ffi::{c_void, CStr}; use core::fmt; use core::marker::PhantomData; use core::mem::{self, MaybeUninit}; @@ -10,9 +10,11 @@ use std::os::raw::c_ulong; use objc2::encode::{EncodeArguments, EncodeReturn, Encoding, RefEncode}; use crate::abi::{ - BlockDescriptor, BlockDescriptorCopyDispose, BlockDescriptorPtr, BlockFlags, BlockHeader, + BlockDescriptor, BlockDescriptorCopyDispose, BlockDescriptorCopyDisposeSignature, + BlockDescriptorPtr, BlockDescriptorSignature, BlockFlags, BlockHeader, }; use crate::debug::debug_block_header; +use crate::traits::{ManualBlockEncoding, ManualBlockEncodingExt, NoBlockEncoding}; use crate::{ffi, Block, IntoBlock}; /// An Objective-C block constructed on the stack. @@ -103,6 +105,17 @@ impl<'f, A, R, Closure> StackBlock<'f, A, R, Closure> { reserved: 0, size: Self::SIZE, }; + + /// The [`BlockDescriptorSignature`] corresponding to + /// [`Self::DESCRIPTOR_BASIC`] with the given `encoding` additionally set. + #[inline(always)] + const fn descriptor_basic_with_encoding(encoding: &'static CStr) -> BlockDescriptorSignature { + BlockDescriptorSignature { + reserved: Self::DESCRIPTOR_BASIC.reserved, + size: Self::DESCRIPTOR_BASIC.size, + encoding: encoding.as_ptr(), + } + } } // `StackBlock::new` @@ -140,9 +153,30 @@ impl<'f, A, R, Closure: Clone> StackBlock<'f, A, R, Closure> { copy: Some(Self::clone_closure), dispose: Some(Self::drop_closure), }; + + /// The [`BlockDescriptorCopyDisposeSignature`] corresponding to + /// [`Self::DESCRIPTOR_WITH_CLONE`] with the given `encoding` additionally + /// set. + #[inline(always)] + const fn descriptor_with_clone_and_encoding( + encoding: &'static CStr, + ) -> BlockDescriptorCopyDisposeSignature { + BlockDescriptorCopyDisposeSignature { + reserved: Self::DESCRIPTOR_WITH_CLONE.reserved, + size: Self::DESCRIPTOR_WITH_CLONE.size, + copy: Self::DESCRIPTOR_WITH_CLONE.copy, + dispose: Self::DESCRIPTOR_WITH_CLONE.dispose, + encoding: encoding.as_ptr(), + } + } } -impl<'f, A, R, Closure> StackBlock<'f, A, R, Closure> { +impl<'f, A, R, Closure> StackBlock<'f, A, R, Closure> +where + A: EncodeArguments, + R: EncodeReturn, + Closure: IntoBlock<'f, A, R> + Clone, +{ /// Construct a `StackBlock` with the given closure. /// /// Note that this requires [`Clone`], as a C block is generally assumed @@ -154,21 +188,90 @@ impl<'f, A, R, Closure> StackBlock<'f, A, R, Closure> { /// /// [`RcBlock::new`]: crate::RcBlock::new #[inline] - pub fn new(closure: Closure) -> Self + pub fn new(closure: Closure) -> Self { + // SAFETY: no encoding is given. + unsafe { Self::with_encoding::>(closure) } + } + + /// Constructs a new [`StackBlock`] with the given function and encoding + /// information. + /// + /// Some particular newer-ish Apple Objective-C APIs expect the block they + /// are given to be created with encoding information set in the block + /// object itself and crash the calling process if they fail to find it, + /// which renders them pretty much unusable with only [`Self::new`] that + /// currently does not set such encoding information. This is for example + /// the case in [`FileProvider`] for [`NSFileProviderManager`]'s + /// [`reimportItemsBelowItemWithIdentifier:completionHandler:`] and + /// [`waitForStabilizationWithCompletionHandler:`], but also in + /// [`NetworkExtension`] for [`NEFilterDataProvider`]'s + /// [`applySettings:completionHandler`]. A complete list of such APIs may + /// not be easily obtained, though. + /// + /// This encoding information string could technically be generated at + /// compile time using the generic parameters already available to + /// [`Self::new`]. However, doing so would require some constant evaluation + /// features that are yet to be implemented and stabilized in the Rust + /// compiler. This function is therefore exposed in the meantime so users + /// may still be able to call the concerned APIs by providing the raw + /// encoding information string themselves, thus obtaining a block + /// containing it and working with these APIs. + /// + /// The same requirements as [`Self::new`] apply here as well. + /// + /// [`FileProvider`]: https://developer.apple.com/documentation/fileprovider?language=objc + /// [`NSFileProviderManager`]: https://developer.apple.com/documentation/fileprovider/nsfileprovidermanager?language=objc + /// [`reimportItemsBelowItemWithIdentifier:completionHandler:`]: https://developer.apple.com/documentation/fileprovider/nsfileprovidermanager/reimportitems(below:completionhandler:)?language=objc + /// [`waitForStabilizationWithCompletionHandler:`]: https://developer.apple.com/documentation/fileprovider/nsfileprovidermanager/waitforstabilization(completionhandler:)?language=objc + /// [`NetworkExtension`]: https://developer.apple.com/documentation/networkextension?language=objc + /// [`NEFilterDataProvider`]: https://developer.apple.com/documentation/networkextension/nefilterdataprovider?language=objc + /// [`applySettings:completionHandler`]: https://developer.apple.com/documentation/networkextension/nefilterdataprovider/3181998-applysettings?language=objc + /// + /// # Safety + /// + /// The raw encoding string given through `E` must be correct with respect + /// to the given function's input and output types: see + /// [`ManualBlockEncoding`]. + /// + /// # Example + /// + /// ``` + /// # use core::ffi::CStr; + /// # use block2::{Block, ManualBlockEncoding, StackBlock}; + /// # use objc2_foundation::NSError; + /// # + /// struct MyBlockEncoding; + /// unsafe impl ManualBlockEncoding for MyBlockEncoding { + /// type Arguments = (*mut NSError,); + /// type Return = i32; + /// const ENCODING_CSTR: &'static CStr = cr#"i16@?0@"NSError"8"#; + /// } + /// + /// let my_block = unsafe { + /// StackBlock::with_encoding::(|_err: *mut NSError| { + /// 42i32 + /// }) + /// }; + /// assert_eq!(my_block.call((std::ptr::null_mut(),)), 42); + /// ``` + #[inline] + pub unsafe fn with_encoding(closure: Closure) -> Self where - A: EncodeArguments, - R: EncodeReturn, - Closure: IntoBlock<'f, A, R> + Clone, + E: ManualBlockEncoding, { let header = BlockHeader { isa: unsafe { ptr::addr_of!(ffi::_NSConcreteStackBlock) }, - // TODO: Add signature. - flags: BlockFlags::BLOCK_HAS_COPY_DISPOSE, + flags: BlockFlags::BLOCK_HAS_COPY_DISPOSE + | if !E::is_none() { + BlockFlags::BLOCK_HAS_SIGNATURE + } else { + BlockFlags::EMPTY + }, reserved: MaybeUninit::new(0), invoke: Some(Closure::__get_invoke_stack_block()), // TODO: Use `Self::DESCRIPTOR_BASIC` when `F: Copy` // (probably only possible with specialization). - descriptor: BlockDescriptorPtr { + descriptor: if E::is_none() { // SAFETY: The descriptor must (probably) point to `static` // memory, as Objective-C code may assume the block's // descriptor to be alive past the lifetime of the block @@ -184,7 +287,16 @@ impl<'f, A, R, Closure> StackBlock<'f, A, R, Closure> { // does not contain `UnsafeCell` (it does not). // // [promotion]: https://doc.rust-lang.org/reference/destructors.html#constant-promotion - with_copy_dispose: &Self::DESCRIPTOR_WITH_CLONE, + BlockDescriptorPtr { + with_copy_dispose: &Self::DESCRIPTOR_WITH_CLONE, + } + } else { + // SAFETY: promotion is guaranteed with the used inline const. + BlockDescriptorPtr { + with_copy_dispose_signature: &const { + Self::descriptor_with_clone_and_encoding(E::ENCODING_CSTR) + }, + } }, }; Self { @@ -195,11 +307,11 @@ impl<'f, A, R, Closure> StackBlock<'f, A, R, Closure> { } } -// `RcBlock::new` +// `RcBlock::with_encoding` impl<'f, A, R, Closure> StackBlock<'f, A, R, Closure> { crate::__c_unwind! {unsafe extern "C" fn empty_clone_closure(_dst: *mut c_void, _src: *const c_void) { // We do nothing, the closure has been `memmove`'d already, and - // ownership will be passed in `RcBlock::new`. + // ownership will be passed in `RcBlock::with_encoding`. }} const DESCRIPTOR_WITH_DROP: BlockDescriptorCopyDispose = BlockDescriptorCopyDispose { @@ -209,34 +321,71 @@ impl<'f, A, R, Closure> StackBlock<'f, A, R, Closure> { dispose: Some(Self::drop_closure), }; + /// The [`BlockDescriptorCopyDisposeSignature`] corresponding to + /// [`Self::DESCRIPTOR_WITH_DROP`] with the given `encoding` additionally + /// set. + #[inline(always)] + const fn descriptor_with_drop_and_encoding( + encoding: &'static CStr, + ) -> BlockDescriptorCopyDisposeSignature { + BlockDescriptorCopyDisposeSignature { + reserved: Self::DESCRIPTOR_WITH_DROP.reserved, + size: Self::DESCRIPTOR_WITH_DROP.size, + copy: Self::DESCRIPTOR_WITH_DROP.copy, + dispose: Self::DESCRIPTOR_WITH_DROP.dispose, + encoding: encoding.as_ptr(), + } + } + /// # Safety /// - /// `_Block_copy` must be called on the resulting stack block only once. + /// * `_Block_copy` must be called on the resulting stack block only once. + /// * Encoding must be correct with respect to the given function's input + /// and output types: see [`ManualBlockEncoding`]. #[inline] - pub(crate) unsafe fn new_no_clone(closure: Closure) -> Self + pub(crate) unsafe fn new_no_clone(closure: Closure) -> Self where A: EncodeArguments, R: EncodeReturn, Closure: IntoBlock<'f, A, R>, + E: ManualBlockEncoding, { // Don't need to emit copy and dispose helpers if the closure // doesn't need it. - // - // TODO: Add signature. let flags = if mem::needs_drop::() { BlockFlags::BLOCK_HAS_COPY_DISPOSE } else { BlockFlags::EMPTY + } | if !E::is_none() { + BlockFlags::BLOCK_HAS_SIGNATURE + } else { + BlockFlags::EMPTY }; // See discussion in `new` above with regards to the safety of the // pointer to the descriptor. let descriptor = if mem::needs_drop::() { + if E::is_none() { + // SAFETY: see above. + BlockDescriptorPtr { + with_copy_dispose: &Self::DESCRIPTOR_WITH_DROP, + } + } else { + // SAFETY: promotion is guaranteed with the used inline const. + BlockDescriptorPtr { + with_copy_dispose_signature: &const { + Self::descriptor_with_drop_and_encoding(E::ENCODING_CSTR) + }, + } + } + } else if E::is_none() { + // SAFETY: see above. BlockDescriptorPtr { - with_copy_dispose: &Self::DESCRIPTOR_WITH_DROP, + basic: &Self::DESCRIPTOR_BASIC, } } else { + // SAFETY: promotion is guaranteed with the used inline const. BlockDescriptorPtr { - basic: &Self::DESCRIPTOR_BASIC, + with_signature: &const { Self::descriptor_basic_with_encoding(E::ENCODING_CSTR) }, } }; diff --git a/crates/block2/src/traits.rs b/crates/block2/src/traits.rs index 565fe5fea..7f87c96fe 100644 --- a/crates/block2/src/traits.rs +++ b/crates/block2/src/traits.rs @@ -1,3 +1,5 @@ +use core::ffi::CStr; +use core::marker::PhantomData; use core::mem; use core::ptr; @@ -135,3 +137,107 @@ impl_traits!(t0: T0, t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: impl_traits!(t0: T0, t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9); impl_traits!(t0: T0, t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10); impl_traits!(t0: T0, t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10, t11: T11); + +/// Interim abstraction to manually provide block encodings for use at compile +/// time with [`StackBlock::with_encoding`] and [`RcBlock::with_encoding`]. +/// +/// See these functions for examples of how to implement and use this trait, +/// since its sole purpose is passing values at compile time to them. +/// +/// [`RcBlock::with_encoding`]: crate::RcBlock::with_encoding +/// +/// # Safety +/// +/// [`Self::ENCODING_CSTR`] must correspond to the actual signature string a +/// recent-enough Objective-C compiler would generate for a block taking in +/// [`Self::Arguments`] as input and returning [`Self::Return`] as output. The +/// simplest way to achieve this is to somehow obtain a block with the correct +/// signature from the Objective-C runtime and [`Debug`]-display it to copy its +/// `encoding` C string. Another possibility is to compile a small Objective-C +/// program, with GCC and GNUstep for example, that simply displays the result of +/// `method_getTypeEncoding(class_getClassMethod([MyClass class] @selector(sel)))` +/// where `MyClass::sel` has a signature compatible with the block you want, +/// after which the string can be slightly modified to fit an actual block +/// instead of a method: see below. A more thorough but manual approach is to +/// only follow the rules described below. +/// +/// # Encoding string generation +/// +/// This is the result of the `@encode` Objective-C compiler directive. The +/// [Apple documentation] and [GCC documentation] explain how each base type is +/// encoded into a string representation. See there for a somewhat-formal +/// specification and a few basic examples. +/// +/// See also the [GCC method signatures] section. It is mostly valid for blocks +/// as well, since they are basically functions with captured environment -- +/// i.e. closures, except that no selector is implicitely sent, only the block +/// object is. In short, the "signature" is a null-terminated string, composed +/// of the following: +/// +/// * The return type, including type qualifiers. For example, a block +/// returning `int` would have `i` here. +/// * The total size (in bytes) required to pass all the parameters: the call +/// frame size. This includes the hidden block object parameter that is +/// passed as a pointer, so at least 4 bytes when under a 32-bit system or +/// most probably 8 bytes when under a 64-bit one. +/// * Each argument, with the type encoding, followed by the offset (in bytes) +/// of the argument in the list of parameters. +/// +/// Examples: +/// +/// | Objective-C signature | Runtime encoding | +/// | ------------------------ | ------------------------------------------ | +/// | `void (^)(void)` | `v8@?0` | +/// | `int (^)(void)` | `i8@?0` | +/// | `int (^)(float)` | `i12@?0f8` | +/// | `int (^)(float, _Bool)` | `i16@?0f8B12` | +/// | `void (^)(int*)` | `v16@?0^i8` | +/// | `void (^)(NSError*)` | `v16@?0@8` or `v16@?0@"NSError"8` | +/// | `NSError* (^)(NSError*)` | `@16@?0@8` or `@"NSError"16@?0@"NSError"8` | +/// +/// [Apple documentation]: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html +/// [GCC documentation]: https://gcc.gnu.org/onlinedocs/gcc/Type-encoding.html +/// [GCC method signatures]: https://gcc.gnu.org/onlinedocs/gcc/Method-signatures.html +pub unsafe trait ManualBlockEncoding { + /// The function's input argument types. + type Arguments: EncodeArguments; + /// The function's return type. + type Return: EncodeReturn; + /// The raw encoding information string. + const ENCODING_CSTR: &'static CStr; +} + +/// Particular [`ManualBlockEncoding`] that indicates no actual encoding should +/// be set in the block's descriptor. +/// +/// This is used is a bit of a hackish way in order to share more code between +/// the encoded and non-encoded paths. +pub(crate) struct NoBlockEncoding +where + A: EncodeArguments, + R: EncodeReturn, +{ + _a: PhantomData, + _r: PhantomData, +} + +unsafe impl ManualBlockEncoding for NoBlockEncoding +where + A: EncodeArguments, + R: EncodeReturn, +{ + type Arguments = A; + type Return = R; + const ENCODING_CSTR: &'static CStr = c""; +} + +/// Crate-private extension to [`ManualBlockEncoding`]. +pub(crate) trait ManualBlockEncodingExt: ManualBlockEncoding { + /// Returns `true` if the current [`ManualBlockEncoding`] is [`NoBlockEncoding`]. + #[inline(always)] + fn is_none() -> bool { + // HACK: use this in order to detect `NoBlockEncoding`. + Self::ENCODING_CSTR.is_empty() + } +} +impl ManualBlockEncodingExt for E {}