- Notifications
You must be signed in to change notification settings - Fork0
Utilities to make coding on Apple platforms in C++ or ObjectiveC++ more pleasant
License
gershnik/objc-helpers
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
An ever-growing collection of utilities to make coding on Apple platforms in C++ or ObjectiveC++ more pleasant. Some functionality is also available on Linux.
- What's included?
- Convert ANY C++ callable to a block
- Coroutines that execute on GCD dispatch queues
- Boxing of any C++ objects in ObjectiveC ones
- Comparators for ObjectiveC objects
- Printing ObjectiveC objects to C++ streams and std::format
- Accessing NSString/CFString as a char16_t container
- Conversions between NSString/CFString and char/char16_t/char32_t/char8_t/wchar_t ranges
- XCTest assertions for C++ objects
- Linux notes
The library is a collection of mostly independent header files. There is nothing to link with. Simply add these headers to your include path and include them as needed.
sample
directory contains a sample that demonstrates the usage of main features.
With modern Clang compiler you can seamlessly convert C++ lambdas to blocks like this:
dispatch_async(someQueue, []() {//do something})
This works and works great but there are a few things that don't:
- You can only pass alambda as a block, not any other kind of callable. For example this does not compile:
structfoo {voidoperator()()const {} };dispatch_async(someQueue, foo{});
- You cannot pass amutable lambda this way. This doesn't compile eitherNeither cannot you pass a block that captures anything mutable (like your lambda) - captured variables are all const
dispatch_async(someQueue, []() mutable {//do something});
- Your lambda captured variables are alwayscopied into the block, notmoved. If you have captures that areexpensive 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.
TheBlockUtils.h
header gives you an ability to solve all of these problems.
It provides two functions:makeBlock
andmakeMutableBlock
that take any C++ callable as an input and return an objectthat 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 invokesoperator()
on aconst
callable andmakeMutableBlock
returns a block that invokes it on a non-const one. ThusmakeMutableBlock
can be used withmutable lambdas or any other callable that provides non-constoperator()
.- 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
- This functionality is also available on Linux under CLang (seeLinux notes below).
Some examples of their usage are as follows:
//Convert any callablestructfoo {voidoperator()()const {} };dispatch_async(someQueue, makeBlock(foo{}));//this moves foo in since it's a temporary//Copy or move a callable infoo callable;dispatch_async(someQueue, makeBlock(callable));dispatch_async(someQueue, makeBlock(std::move(callable)));//Convert mutable lambdasint captureMeByValue;dispatch_async(someQueue, makeMutableBlock([=]() mutable { captureMeByValue =5;//the local copy of captureMeByValue is mutable}));//Use move-only callablesauto 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 frommakeBlock
/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 blockpointer 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 thenecessary magic behind the scenes. For example:
//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 usingcopy
andBlock_release
. For example:
//In plain C++void (^block)() = copy(makeBlock([](int){}));block(7);//this works because we made a copyBlock_release(block);
BlockUtil.h
also provides two helpers:makeWeak
andmakeStrong
that simplify the "strongSelf"casting dance around avoiding circular references when using blocks/lambdas.
Here is the intended usage:
dispatch_async(someQueue, [weakSelf =makeWeak(self)] () {auto self =makeStrong(weakSelf);if (!self)return; [selfdoSomething];});
HeaderCoDispatch.h
allows you to useasynchronous C++ coroutines that execute on GCD dispatch queues. Yes there isthis library 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.
Working with coroutines is discussed in greater detail ina separate doc.
Here is a small sample of what you can do:
DispatchTask<int>coro() {//this will execute asyncronously on the main queueint i =co_awaitco_dispatch([]() {return7; });//you can specify a different queue of courseauto queue =dispatch_get_global_queue(QOS_CLASS_BACKGROUND,0);int j =co_awaitco_dispatch(queue, []() {return42; }).resumeOnMainQueue();//add this to resume back on main queue//you can convert ObjC APIs with asynchronous callbacks to couroutinesauto status =co_await makeAwaitable<int>([](auto promise) {NSError * err; [NSTasklaunchedTaskWithExecutableURL:[NSURLfileURLWithPath:@"/bin/bash"]arguments:@[@"-c",@"ls"]error:&errterminationHandler:^(NSTask * res){ promise.success(res.terminationStatus); }];if (err)throwstd::runtime_error(err.description.UTF8String); }).resumeOnMainQueue();//this will switch execution to a different queueco_awaitresumeOn(queue);}//coroutines can await other corotinesDispatchTask<int>anotherCoro() {int res =co_awaitcoro();co_return res;}//you can also have asynchronous generatorsDispatchGenerator<std::string>generator() {co_yield"Hello";co_yield"World";//in real life you probably will use something like//co_yield co_await somethingAsync();}DispatchTask<int>useGenerator() { std::vector<std::string> dest;//this will run generator asynchrnously on the main queuefor (auto it =co_awaitgenerator().begin(); it;co_await it.next()) { res.push_back(*it); }//you can also say things like//auto it = generator().resumingOnMainQueue().beginOn(queue)//to control the running and resuming queues}intmain() {//fire and forgetanotherCoro();useGenerator();dispatch_main();}
This facility can also be used both from plain C++ (.cpp) and ObjectiveC++ (.mm) files. It is also available on Linux usinglibdispatch library (seeLinux notes below).
Sometimes you want to store a C++ object where an ObjectiveC object is expected. Perhaps there issomeNSObject * tag
which you really want to put anstd::vector
in or something similar. You can,of course, do that by creating a wrapper ObjectiveC class that storesstd::vector
but it is a huge annoyance. Yet another ObjectiveC class to write (so a new header and a .mm file) lots of boilerplate code forinit
and value access and, after all that, it is going to to bestd::vector
specific. If you later need to wrap another C++ class you need yet another wrapper.
For plain C structs ObjectiveC has a solution:NSValue
that can store any C struct and let you retrieve it back later. Unfortunately in C++ this only works for "trivially copyable" types (which more or less correspond to "plain C structs"). Trying to stick anything else inNSValue
will appear to work but likely do very bad things - it simply copies object bytes into it and out! Whether bytes copied out will work as the original object is undefined.
To solve this issueBoxUtil.h
provides generic facilities for wrapping and unwrapping of any C++ object in anNSObject
-derived classes without writing any code. Such wrapping and unwrapping of native objects in higher-level language ones are usually called "boxing" and "unboxing", hence thename of the header and it's APIs.
The only requirement for the C++ class to be wrappable is having a public destructor and at least one public constructor. The constructor doesn't need to be default - boxing works with objects that need to be "emplaced".
You use it like this:
std::vector<int> someVector{1,2,3};//this copies the vector into the wrapperNSObject * obj1 = box(someVector);//and this moves itNSObject * obj2 = box(std::move(someVector));//you can also do thisNSObject * obj3 = box(std::vector<int>{1,2,3});//and you can emplace the object directly rather than copy or move itNSObject * obj4 = box<std::vector<int>>(5,3);//emplaces {3,3,3,3,3}//You can get a reference to wrapped object//This will raise an ObjectiveC exception if the type doesn't macthauto & vec = boxedValue<std::vector<int>>(obj1);assert(vec.size() == 3);assert(vec[1] ==2);The reference you get back ismutable bydefault. If you want immutabilitydothisNSObject * immuatbleObj = box<const std::vector<int>>(...any of the stuff above...);//if your C++ object has a copy constructor the wrapper//will implement NSCopyingauto * obj5 = (NSObject *)[obj1copy];//this uses operator== if available, which it isassert([obj1isEqual:obj3]);//and this uses std::hash if available//it will raise an exception if you have operator== but not std::hash!//as incositent equality and hashing is one of the most common ObjectiveC errorsauto hash = obj1.hash//you can obtain a sensible description//it will try to use://std::to_string//iostream <<//fall back on "boxed object of type <name of the class>"auto desc = obj1.description;//if your object supports <=> operator that returns std::strong_ordering//you can use compare: methodassert([box(5)compare:box(6)] == NSOrderingAscending);
HeaderNSObjectUtil.h
providesNSObjectEqual
andNSObjectHash
- functors that evaluate equality and hash code for any NSObject and allow them to be used as keys instd::unordered_map
andstd::unordered_set
for example. These are implemented in terms ofisEqual
andhash
methods ofNSObject
.
HeaderNSStringUtil.h
providesNSStringLess
andNSStringLocaleLess
comparators. These allowNSString
objects to be used as keys instd::map
orstd::set
as well as used in STL sorting and searching algorithms.
Additionally it providesNSStringEqual
comparator. This is more efficient thanNSObjectEqual
and is implemented in terms ofisEqualToString
.
HeaderNSNumberUtil.h
providesNSNumberLess
comparator. This allowsNSNumber
objects to be used as keys instd::map
orstd::set
as well as used in STL sorting and searching algorithms.
Additionally it providesNSNumberEqual
comparator. This is more efficient thanNSObjectEqual
and is implemented in terms ofisEqualToNumber
.
For all comparatorsnil
s are handled properly. Anil
is equal tonil
and is less than any non-nil
object.
HeaderNSObjectUtil.h
providesoperator<<
for anyNSObject
to print it to anstd::ostream
. This behaves similarly to%@
formatting flag by delegating either todescriptionWithLocale:
or todescription
.
HeaderNSStringUtil.h
provides additionaloperator<<
to print anNSString
to anstd::ostream
. This outputsUTF8String
.
Both headers also providestd::formatter
s with the same functionality ifstd::format
is available in the standard library andfmt::formatter
if a macroNS_OBJECT_UTIL_USE_FMT
is defined. In the later case presence of<fmt/format.h>
or"fmt/format.h"
include file is required.
HeaderNSStringUtil.h
providesNSStringCharAccess
- a fast accessor forNSString
characters (aschar16_t
) via an STL container interface. This uses approach similar toCFStringInlineBuffer
one. This facility can be used both from ObjectiveC++ and plain C++.
Here are some examples of usage
for (char16_t c: NSStringCharAccess(@"abc")) { ...}std::ranges::for_each(NSStringCharAccess(@"abc") | std::views::take(2), [](char16_t c) { ...});
Note thatNSStringCharAccess
is areference class (akin in spirit tostd::string_view
). It does not hold a strong reference to theNSString
/CFString
it uses and is only valid as long as that string exists.
HeaderNSStringUtil.h
providesmakeNSString
andmakeCFString
functions that accept:
- Any contiguous range of Chars (including
std::basic_string_view
,std::basic_string
,std::span
etc. etc.) - A pointer to a null-terminated C string of Chars
- An
std::initializer_list<Char>
where Char can be any ofchar
,char16_t
,char32_t
,char8_t
,wchar_t
and converts it toNSString
/CFString
. They returnnil
on failure.
Conversions fromchar16_t
are exact and can only fail when out of memory. Conversions from other formats will fail also when encoding is invalid. Conversions fromchar
assume UTF-8 and fromwchar_t
, UTF-32.
To convert in the opposite direction the header providesmakeStdString<Char>
overloads. These accept:
NSString *
/CFStringRef
, optional start position (0 by default) and optional length (whole string by default)- A pair of
NSStringCharAccess
iterators - Any range of
NSStringCharAccess
iterators
They return anstd::basic_string<Char>
. Anil
input produces an empty string. Similar to above conversions fromchar16_t
are exact and conversions to other char types transcode from an appropriate UTF encoding. If the sourceNSString *
/CFStringRef
contains invalid UTF-16 the output is an empty string.
This functionality is available in both ObjectiveC++ and plain C++
When using XCTest framework you might be tempted to useXCTAssertEqual
and similar on C++ objects. While this works and works safely you will quickly discover that when the tests fail you get a less than useful failure message that showsraw bytes of the C++ object instead of any kind of logical description. This happens because in order to obtain the textual description of the valueXCTAssertEqual
and friends stuff it into anNSValue
and then query its description. And, as mentioned inBoxUtil.h section,NSValue
simply copies raw bytes of a C++ object.
While this is still safe, because nothing except the description is ever done with those bytes the end result is hardly usable. To fix thisXCTestUtil.h
header provides the following replacement macros:
XCTAssertCppEqual
XCTAssertCppNotEqual
XCTAssertCppGreaterThan
XCTAssertCppGreaterThanOrEqual
XCTAssertCppLessThan
XCTAssertCppLessThanOrEqual
That, in the case of failure, try to obtain description using the following methods:
- If there is an ADL call
testDescription(obj)
that producesNSString *
, use that. - Otherwise, if there is an ADL call
to_string(obj)
inusing std::to_string
scope, use that - Otherwise, if it is possible to do
ostream << obj
, use that - Finally produce
"<full name of the type> object"
string.
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. ThetestDescription
approach specifically exists to allow you to print something different for tests than in normal code.
BlockUtil.h
andCoDispatch.h
headers can also be used on Linux. Currently this requires
- CLang 16 or above (for blocks support). Seethis issue for status of blocks support in GCC
- swift-corelibs-libdispatch library. Note thatmost likely you need to build it from sources. The versions available via various package managers (as of summer 2024) are very old and cannot be used.
You must use:
--std=c++20 -fblocks
flags to use these headers.
ForCoDispatch.h
link with:
-ldispatch -lBlocksRuntime
ForBlockUtil.h
link with:
-lBlocksRuntime
About
Utilities to make coding on Apple platforms in C++ or ObjectiveC++ more pleasant