15 unstable releases (4 breaking)
0.5.1 | May 21, 2024 |
---|---|
0.4.0 | Dec 3, 2023 |
0.3.0 | Jul 31, 2023 |
0.2.0-alpha.8 | Feb 7, 2023 |
0.2.0-alpha.1 | Nov 22, 2021 |
#5 in macOS and iOS APIs
506,560 downloads per month
Used in 935 crates
(73 directly)
780KB
13K
SLoC
block2
Apple's C language extension of blocks in Rust.
This crate provides functionality for interracting with C blocks, which is the C-equivalent of Rust's closures.
They are technically not limited to only being used in Objective-C, though in practice it's likely the only place you'll ever encounter them.
See the docs for a more thorough overview.
This crate is part of the objc2
project,
see that for related crates.
lib.rs
:
Apple's C language extension of blocks
C Blocks are functions which capture their environments, i.e. the
C-equivalent of Rust's [Fn
] closures. As they were originally developed
by Apple, they're often used in Objective-C code. This crate provides
capabilities to create, manage and invoke these blocks, in an ergonomic,
"Rust-centric" fashion.
At a high level, this crate contains four types, each representing different kinds of blocks, and different kinds of ownership.
block2 type |
Equivalent Rust type |
---|---|
&Block<dyn Fn() + 'a> |
&dyn Fn() + 'a |
RcBlock<dyn Fn() + 'a> |
Arc<dyn Fn() + 'a> |
StackBlock<'a, (), (), impl Fn() + 'a> |
impl Fn() + 'a |
GlobalBlock<dyn Fn()> |
fn item |
For more information on the specifics of the block implementation, see the C language specification and the ABI specification.
External functions using blocks
To declare external functions or methods that takes blocks, use
&Block<dyn Fn(Params) -> R>
or Option<&Block<dyn Fn(Args) -> R>>
,
where Params
is the parameter types, and R
is the return type.
In the next few examples, we're going to work with a function
check_addition
, that takes as parameter a block that adds two integers,
and checks that the addition is correct.
Such a function could be written in C like in the following.
#include <cassert>
#include <stdint.h>
#include <Block.h>
void check_addition(int32_t (^block)(int32_t, int32_t)) {
assert(block(5, 8) == 13);
}
An extern "C" { ... }
declaration for that function would then be:
use block2::Block;
extern "C" {
fn check_addition(block: &Block<dyn Fn(i32, i32) -> i32>);
}
This can similarly be done inside external methods declared with
objc2::extern_methods!
.
use block2::Block;
use objc2::extern_methods;
#
#
extern_methods!(
unsafe impl MyClass {
#[method(checkAddition:)]
pub fn checkAddition(&self, block: &Block<dyn Fn(i32, i32) -> i32>);
}
);
If the function/method allowed passing NULL
blocks, the type would be
Option<&Block<dyn Fn(i32, i32) -> i32>>
instead.
Invoking blocks
We can also define the external function in Rust, and expose it to
Objective-C. To do this, we can use Block::call
to invoke the block
inside the function.
use block2::Block;
#[no_mangle]
extern "C" fn check_addition(block: &Block<dyn Fn(i32, i32) -> i32>) {
assert_eq!(block.call((5, 8)), 13);
}
Note the extra parentheses in the call
method, since the arguments must
be passed as a tuple.
Creating blocks
Creating a block to pass to Objective-C can be done with RcBlock
or
StackBlock
, depending on if you want to move the block to the heap,
or let the callee decide if it needs to do that.
To call such a function / method, we could create a new block from a
closure using RcBlock::new
.
use block2::RcBlock;
#
let block = RcBlock::new(|a, b| a + b);
check_addition(&block);
This creates the block on the heap. If the external function you're
calling is not going to copy the block, it may be more performant if you
construct a StackBlock
directly, using StackBlock::new
.
Note that this requires that the closure is Clone
, as the external
code is allowed to copy the block to the heap in the future.
use block2::StackBlock;
#
let block = StackBlock::new(|a, b| a + b);
check_addition(&block);
As an optimization, if your closure doesn't capture any variables (as in
the above examples), you can use the global_block!
macro to create a
static block.
use block2::global_block;
#
global_block! {
static BLOCK = |a: i32, b: i32| -> i32 {
a + b
};
}
check_addition(&BLOCK);
Lifetimes
When dealing with blocks, there can be quite a few lifetimes to keep in mind.
The most important one is the lifetime of the block's data, i.e. the
lifetime of the data in the closure contained in the block. This lifetime
can be specified as 'f
in &Block<dyn Fn() + 'f>
.
Note that &Block<dyn Fn()>
, without any lifetime specifier, can be a bit
confusing, as the default depends on where it is typed. In function/method
signatures, it defaults to 'static
, but as the type of e.g. a let
binding, the lifetime may be inferred to be something smaller, see the
reference for details. If in doubt, either add a
+ 'static
or + '_
to force an escaping or non-escaping block.
Another lifetime is the lifetime of the currently held pointer, i.e. 'b
in &'b Block<dyn Fn()>
. This lifetime can be safely extended using
Block::copy
, so should prove to be little trouble (of course the
lifetime still can't be extended past the lifetime of the captured data
above).
Finally, the block's parameter and return types can also contain
lifetimes, as 'a
and 'r
in &Block<dyn Fn(&'a i32) -> &'r u32>
.
Unfortunately, these lifetimes are quite problematic and unsupported at
the moment, due to Rust trait limitations regarding higher-ranked trait
bounds. If you run into problems with this in a block that takes or
returns a reference, consider using the ABI-compatible NonNull<T>
, or
transmute to a 'static
lifetime.
Thread safety
Thread-safe blocks are not yet representable in block2
, and as such any
function that requires a thread-safe block must be marked unsafe
.
Mutability
Blocks are generally assumed to be shareable, and as such can only very rarely be made mutable. In particular, there is no good way to prevent re-entrancy.
You will likely have to use interior mutability instead.
Specifying a runtime
Different runtime implementations exist and act in slightly different ways (and have several different helper functions), the most important aspect being that the libraries are named differently, so we must take that into account when linking.
You can choose the desired runtime by using the relevant cargo feature
flags, see the following sections (you might have to disable the default
"apple"
feature first).
Apple's libclosure
- Feature flag:
apple
.
This is the most common and most sophisticated runtime, and it has quite a lot more features than the specification mandates.
The minimum required operating system versions are as follows (though in practice Rust itself requires higher versions than this):
- macOS:
10.6
- iOS/iPadOS:
3.2
- tvOS:
1.0
- watchOS:
1.0
This is used by default, so you do not need to specify a runtime if you're using this crate on of these platforms.
LLVM compiler-rt
's libBlocksRuntime
- Feature flag:
compiler-rt
.
This is a copy of Apple's older (around macOS 10.6) runtime, and is now
used in Swift's libdispatch
and Swift's Foundation as well.
The runtime and associated headers can be installed on many Linux systems
with the libblocksruntime-dev
package.
Using this runtime probably won't work together with objc2
crate.
GNUStep's libobjc2
- Feature flag:
gnustep-1-7
,gnustep-1-8
,gnustep-1-9
,gnustep-2-0
andgnustep-2-1
depending on the version you're using.
GNUStep is a bit odd, because it bundles blocks support into its
Objective-C runtime. This means we have to link to libobjc
, and this is
done by depending on the objc2
crate. A bit unorthodox, yes, but it
works.
Sources:
Microsoft's WinObjC
- Feature flag:
unstable-winobjc
.
Unstable: Hasn't been tested on Windows yet!
A fork based on GNUStep's libobjc2
version 1.8.
ObjFW
- Feature flag:
unstable-objfw
.
Unstable: Doesn't work yet!
C Compiler configuration
To our knowledge, only Clang supports blocks. To compile C or Objective-C
sources using these features, you should set the -fblocks
flag.