Skip to content

Commit

Permalink
Redesign of BlockUtil
Browse files Browse the repository at this point in the history
  • Loading branch information
gershnik committed Jan 14, 2024
1 parent f94cdc3 commit a1e78af
Show file tree
Hide file tree
Showing 10 changed files with 977 additions and 185 deletions.
1 change: 1 addition & 0 deletions .vscode/spellright.dict
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
awaitable
awaitables
Promisify
callables
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
## Unreleased

### Changed
- `BlockUtil.h`: `makeBlock` is deprecated in ObjectiveC++. Modern versions of Clang allow conversions from lambdas to block directly doing essentially what `makeBlock` was doing. Note that it is still available and necessary in C++.
- `BlockUtil.h`: `makeBlock` functionality is completely reworked. New functionality:
* Wrap any callables including mutable lambdas or any other callable that provides non-const `operator()`.
* If the callable is movable it will be moved into the block, not copied. It will also be moved if the block is "copied to heap"
by ObjectiveC runtime or `Block_copy` in plain C++.
* It is possible to use move-only callables.
* All of this is accomplished with NO dynamic memory allocation
- `BoxUtil.h`: boxing now detects comparability and enables `compare:` not just via presence of operator `<=>` but also when only operators `<`, `==`, `<=` etc. are present.
- `BoxUtil.h`: generated ObjectiveC box classes now have names unique to each shared library/main executable, preventing collisions if multiple modules use boxing.

Expand Down
135 changes: 102 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ An ever-growing collection of utilities to make coding on Apple platforms in C++
<!-- TOC depthfrom:2 -->

- [What's included?](#whats-included)
- [BlockUtil.h](#blockutilh)
- [CoDispatch.h](#codispatchh)
- [BoxUtil.h](#boxutilh)
- [NSObjectUtil.h](#nsobjectutilh)
- [NSStringUtil.h](#nsstringutilh)
- [NSNumberUtil.h](#nsnumberutilh)
- [XCTestUtil.h](#xctestutilh)
- [BlockUtil.h](#blockutilh)
- [General notes](#general-notes)

<!-- /TOC -->
Expand All @@ -24,6 +24,106 @@ The library is a collection of mostly independent header files. There is nothing

The headers are as follows:

### `BlockUtil.h` ###

With modern Clang compiler you can seamlessly convert C++ lambdas to blocks like this:
```c++
dispatch_async(someQueue, []() {
//do something
})
```
This works and works great but there are a few things that don't:
* You can only pass a *lambda* as a block, not any other kind of callable. For example this does not compile:
```cpp
struct foo { void operator()() const {} };
dispatch_async(someQueue, foo{});
```
* You cannot pass a *mutable* lambda this way. This doesn't compile either
```cpp
dispatch_async(someQueue, []() mutable {
//do something
});
```
Neither cannot you pass a block that captures anything mutable (like your lambda) - captured variables are all const
* Your lambda captured variables are always *copied* into the block, not *moved*. If you have captures that are
expensive to copy - oh well...
* Because of the above you cannot have move-only thinks in your block. Forget about using `std::unique_ptr` for example.
The `BlockUtils.h` header gives you an ability to solve all of these problems.
It provides two functions: `makeBlock` and `makeMutableBlock` that take any C++ callable as an input and return an object
that is implicitly convertible to a block and can be passed to any block-taking API. They (or rather the object they return)
have the following features:
* You can wrap any C++ callable, not just a lambda.
* `makeBlock` returns a block that invokes `operator()` on a `const` callable and
`makeMutableBlock` returns a block that invokes it on a non-const one. Thus `makeMutableBlock` can be used with
mutable lambdas or any other callable that provides non-const `operator()`.
* If callable is movable it will be moved into the block, not copied. It will also be moved if the block is "copied to heap"
by ObjectiveC runtime or `Block_copy` in plain C++.
* It is possible to use move-only callables.
* All of this is accomplished with NO dynamic memory allocation
Some examples of their usage are as follows:
```c++
//Convert any callable
struct foo { void operator()() const {} };
dispatch_async(someQueue, makeBlock(foo{})); //this moves foo in since it's a temporary
//Copy or move a callable in
foo callable;
dispatch_async(someQueue, makeBlock(callable));
dispatch_async(someQueue, makeBlock(std::move(callable)));
//Convert mutable lambdas
int captureMeByValue;
dispatch_async(someQueue, makeMutableBlock([=]() mutable {
captureMeByValue = 5; //the local copy of captureMeByValue is mutable
}));
//Use move-only callables
auto ptr = std::make_unique<SomeType>();
dispatch_async(someQueue, makeBlock([ptr=str::move(ptr)]() {
ptr->someMethod();
}));
```

One important thing to keep in mind is that the object returned from `makeBlock`/`makeMutableBlock` **is the block**. It is NOT a block pointer (e.g. Ret (^) (args)) and it doesn't "store" the block pointer inside. The block's lifetime is this object's lifetime and it ends when this object is destroyed. You can copy/move this object around and invoke it as any other C++ callable.
You can also convert it to the block _pointer_ as needed either using implicit conversion or a `.get()` member function.
In ObjectiveC++ the block pointer lifetime is not-related to the block object's one. The objective C++ ARC machinery will do the
necessary magic behind the scenes. For example:

```c++
//In ObjectiveC++
void (^block)(int) = makeBlock([](int){});
block(7); // this works even though the original block object is already destroyed
```
In plain C++ the code above would crash since there is no ARC magic. You need to manually manage block pointers lifecycle using
`copy` and `Block_release`. For example:
```c++
//In plain C++
void (^block)() = copy(makeBlock([](int){}));
block(7); //this works because we made a copy
Block_release(block);
```

`BlockUtil.h` also provides two helpers: `makeWeak` and `makeStrong` that simplify the "strongSelf"
casting dance around avoiding circular references when using blocks/lambdas.

Here is the intended usage:

```objc++
dispatch_async(someQueue, [weakSelf = makeWeak(self)] () {
auto self = makeStrong(weakSelf);
if (!self)
return;
[self doSomething];
});
```

### `CoDispatch.h` ###

Allows you to use **asynchronous** C++ coroutines that execute on GCD dispatch queues. Yes there is [this library](https://github.com/alibaba/coobjc) but it is big, targeting Swift and ObjectiveC rather than C++/\[Objective\]C++ and has a library to integrate with. It also has more features, of course. Here you get basic powerful C++ coroutine support in a single not very large (~800 loc) header.
Expand Down Expand Up @@ -102,6 +202,7 @@ int main() {

This facility can also be used both from plain C++ (.cpp) and ObjectiveC++ (.mm) files.


### `BoxUtil.h` ###

Sometimes you want to store a C++ object where an ObjectiveC object is expected. Perhaps there is
Expand Down Expand Up @@ -208,38 +309,6 @@ That, in the case of failure, try to obtain description using the following meth

Thus if an object is printable using the typical means those will be automatically used. You can also make your own objects printable using either of the means above. The `testDescription` approach specifically exists to allow you to print something different for tests than in normal code.

### `BlockUtil.h` ###

> ℹ️️ Modern versions of Clang allow conversions of lambdas to blocks directly in **ObjectiveC++** (but not in plain C++). Thus `makeBlock` call described below is no longer necessary in ObjectiveC++ and is deprecated. It is still available in C++.
Allows clean and safe usage of C++ lambdas instead of ObjectiveC blocks which are also available
in pure C++ on Apple platforms as an extension.

Why not use blocks? Blocks capture any variable mentioned in them automatically which
causes no end of trouble with inadvertent capture and circular references. The most
common one is to accidentally capture `self` in a block. There is no protection in
ObjectiveC - you have to manually inspect code to ensure this doesn't happen.
Add in code maintenance, copy/paste and you are almost guaranteed to have bugs.
Even if you have no bugs your block code is likely littered with strongSelf
nonsense making it harder to understand.

C++ lambdas force you to explicitly specify what you capture, removing this whole problem.
Unfortunately lambdas cannot be used as blocks. This header provides utility functions
to fix this.
The intended usage is something like this

```objc++
dispatch_async(makeBlock([weakSelf = makeWeak(self)] () {
auto self = makeStrong(weakSelf);
if (!self)
return;
[self doSomething];
}));
```

> ℹ️️ Note that in C++ the block returned from `makeBlock` needs to be released at some point via `Block_release` - there is no ARC to handle it for you.
### General notes ###

For all comparators `nil`s are handled properly. A `nil` is equal to `nil` and is less than any non-`nil` object.
Expand Down
Loading

0 comments on commit a1e78af

Please sign in to comment.