Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up

Utilities to make coding on Apple platforms in C++ or ObjectiveC++ more pleasant

License

NotificationsYou must be signed in to change notification settings

gershnik/objc-helpers

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

74 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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?

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.

Convert ANY C++ callable to a block

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 either
    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 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 usingstd::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 orBlock_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/makeMutableBlockis 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];});

Coroutines that execute on GCD dispatch queues

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).

Boxing of any C++ objects in ObjectiveC ones

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);

Comparators for ObjectiveC objects

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::setas 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 comparatorsnils are handled properly. Anil is equal tonil and is less than any non-nil object.

Printing ObjectiveC objects to C++ streams and std::format

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::formatters 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.

Accessing NSString/CFString as a char16_t container

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.

Conversions betweenNSString/CFString andchar/char16_t/char32_t/char8_t/wchar_t ranges

HeaderNSStringUtil.h providesmakeNSString andmakeCFString functions that accept:

  • Any contiguous range of Chars (includingstd::basic_string_view,std::basic_string,std::span etc. etc.)
  • A pointer to a null-terminated C string of Chars
  • Anstd::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 ofNSStringCharAccess iterators
  • Any range ofNSStringCharAccess 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++

XCTest assertions for C++ objects

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 calltestDescription(obj) that producesNSString *, use that.
  • Otherwise, if there is an ADL callto_string(obj) inusing std::to_string scope, use that
  • Otherwise, if it is possible to doostream << 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.

Linux notes

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

[8]ページ先頭

©2009-2025 Movatter.jp