Source Code on GitHub

Back for the release of iOS 15, Apple released Metal-cpp, a C++ binding primarly for the Metal framework, but also for some select symbols in Foundation and QuartzCore frequently used in conjunction with Metal. Thus, instead of writing the following non-ARC Obj-C:

- (void)gameLoop {
  @autoreleasepool {
    MTLTextureDescriptor *textureDesc = [[[MTLTextureDescriptor alloc] init] autorelease];
    textureDesc.pixelFormat = MTLPixelFormatRGBA8Unorm_sRGB;
    NSLog(@"width: %d", textureDesc.width);
  }
}

you can write the following C++:

void gameLoop() {
  NS::AutoreleasePool *pool = NS::AutoReleasepool::alloc()->init();
  
  MTL::TextureDescriptor *textureDesc = MTL::TextureDescriptor::alloc()->init()->autorelease();
  textureDesc->setPixelFormat(MTL::PixelFormatRGBA8Unorm_sRGB);
  std::cout << "width: " << textureDesc->getWidth() << std::endl;

  pool->release();
}

Although C++ and Obj-C could always be mixed via Obj-C++ (in .mm files usually), objects are either strictly Obj-C objects or C++ objects. You could never use Apple SDK objects and functions with C++ semantics. In a cross-platform codebase where Apple platforms are just a few among many targets, the rest of the application is usually written in pure C++. In pure C++, you can’t include Apple SDK framework headers like #include <AppKit/AppKit.h> just for defining types since these use Obj-C features. Thus, to prevent Obj-C++ from “infecting” the rest of the codebase and forcing you to compile everything as Obj-C++ on Apple (and because platform specifics are abstracted away at some point), Obj-C object pointer types are type erased to void * or similar in pure C/C++ and casted back when needed in Obj-C++ via shim layers.

Personally, I really enjoy writing Obj-C, but for those who haven’t spent as much time developing for Apple platforms, the overhead of learning a new language can be onerous, so there’s also that obvious benefit.

However, Metal-cpp isn’t a panacea – there are two limitations to keep in mind:

  • Memory management is manual (equivalent to non-ARC).
  • Only Metal and some select Foundation and QuartzCore are exposed

Memory Management

Memory management is not always manual in C++ anymore given widespread techniques including stack-based memory management, RAII, std::unique_ptr, std::shared_ptr, etc. However, when using Metal-cpp, you must treat these objects with non-ARC Obj-C semantics (hello retain, release, and autorelease my old friends :) It’s both a blessing and a curse in that virtually no overhead is imposed by the bindings – the Metal-cpp C++ calls simply inline the same effective non-ARC Obj-C. However, it means that you either need to manually track release and retain, or build your own wrappers around the provided types.

I think this was the right choice, especially given how finicky C++ devs can be about performance overhead, especially when it comes to graphics. Having non-ARC Obj-C experience is super helpful, as most of this shouldn’t be too difficult as long as you know how to arrange your object graph sanely: single parent strong pointers down the tree to indicate ownership, and weak pointers across and up the tree. It also helps to remember which function name types have ownership implications, like new, alloc, and copy. In any case, for the specific case of using Metal, you might be able to get away with stuffing the entire renderer in just one file so that it’s easy enough to reason about anyway – plus you have the autoreleasepool clearing out each frame.

Other Apple Frameworks

I enjoyed using Metal-cpp in AppleGfxBase, but I wondered if I could move the rest of remaining Obj-C logic into C++. This was mostly:

  • Views and View Controllers
    • UIView, UIViewController on iOS/tvOS
    • NSView, NSViewController on macOS
  • Display loop setup
    • CADisplayLink on iOS/tvOS
    • CVDisplayLink on macOS
  • GL backend setup
    • CAEGLContext on iOS/tvOS
    • NSOpenGLContext on macOS

I decided pretty quickly that the view and view controller code was not worth porting as so many UIKit/AppKit paradigms are fine-tuned to work idiomatically in Obj-C, wouldn’t benefit from any code sharing, and most importantly were all the way at the top of the build graph, right before the entry point. The last point also rang true for the display loop setup. In other words, no pure C++ code was calling into them – just the inverse.

However for the rest, lots of pure C/C++ OpenGL logic was sprinkled throughout the Apple-specific GL backend setup. It would be nice to treat this logic similarly to how it’s done on Linux, plus the API surface is not as large (at least the parts I need!).

As a result, after studying the Metal-cpp headers, I extended them to also provide support for the following types:

  • AppKit.hpp
    • NS::OpenGLContext
    • NS::OpenGLPixelFormat
    • NS::OpenGLView
    • NS::OpenGLPixelFormatAttribute
    • NS::OpenGLProfileVersion
  • CoreGraphics.hpp
    • CG::Float
    • CG::Size
  • OpenGLES.hpp
    • EAGL::Context
    • EAGL::Drawable
    • EAGL::RenderingAPI
  • QuartzCore.hpp
    • CA::EAGLLayer

The basic gist of the bindings (for object types) is to define a shell C++ class and for each method/property call NS::Object::sendMessage, since Obj-C properties are really just get/set methods under the hood. First, the (quite predictable, thankfully) symbols exported by the Obj-C Apple frameworks are declared (as extern to indicate that they are defined elsewhere, in the Apple frameworks):

namespace NS {

namespace Private {
namespace Class {
extern void* s_kNSOpenGLContext;
extern void* s_kNSOpenGLPixelFormat;
extern void* s_kNSOpenGLView;
} // namespace Class
namespace Selector {
extern SEL s_kinitWithFormat_shareContext_;
extern SEL s_kpixelFormat_;
extern SEL s_ksetPixelFormat_;
} // namespace Selector
} // namespace Private

Then, the shim is declared, to provide C++ calling semantics:

class OpenGLView : public NS::Referencing<OpenGLView> {

// Alloc example
_AK_INLINE NS::OpenGLContext* NS::OpenGLContext::alloc() {
  return NS::Object::sendMessage<NS::OpenGLContext*>(
      Private::Class::s_kNSOpenGLContext, Private::Selector::s_kalloc);
}

// Init example
_AK_INLINE NS::OpenGLContext* NS::OpenGLContext::init(
    NS::OpenGLPixelFormat* pixel_format, NS::OpenGLContext* share_context) {
  return NS::Object::sendMessage<NS::OpenGLContext*>(
      this, Private::Selector::s_kinitWithFormat_shareContext_, pixel_format,
      share_context);
}
};

class OpenGLContext : public NS::Referencing<OpenGLContext> {

// Getter example
_AK_INLINE NS::OpenGLPixelFormat* NS::OpenGLView::pixelFormat() {
  return NS::Object::sendMessage<NS::OpenGLPixelFormat*>(
      this, Private::Selector::s_kpixelFormat);
}

// Setter example
_AK_INLINE void NS::OpenGLView::setPixelFormat(
    NS::OpenGLPixelFormat* pixel_format) {
  NS::Object::sendMessage<void>(this, _Private::Selector::s_ksetPixelFormat_,
                                pixel_format);
}

};
} // namespace NS

Note that _AK_INLINE just expands to inline __attribute__((always_inline)) to force the inline. In reality, _AK_PRIVATE_DEF_CLS and _AK_PRIVATE_DEF_SEL are used in the first section to make the declarations more readable, and _AK_PRIVATE_CLS and _AK_PRIVATE_SEL are used in the second part for the same reason. However, I present them expanded to demonstrate that we’re really just using the exported symbols and passing them through without much magic.

Under the hood, NS::Object::sendMessage does most of the heavy lifting (which in the happy path and cleaned up) looks something like this:

template <typename ReturnType, typename... Args>
_NS_INLINE ReturnType NS::Object::sendMessage(const void* objectPointer, SEL selector, Args... args) {
  // Define the function signature
  using FunctionType = ReturnType (*)(const void*, SEL, Args...);
  // Obtain a pointer to the function we're about to call
  const FunctionType functionPointer = reinterpret_cast<FunctionType>(&objc_msgSend);
  // Call it!
  return (*functionPointer)(objectPointer, selector, args...);
}

Wow! To break it down, NS::Object::sendMessage is first of all templated by return type and argument types. This ensures some level of safety while writing the declarations, without runtime cost (just build time cost :). Then, objc_msgSend is cast to the expected function signature and called with a series of type massaging to make the compiler happy. This is where the real magic happens, as objc_msgSend can be called as if it had the function signature of the according selector, otherwise throwing the infamous “unrecognized selector sent to instance” NSInvalidArgumentException at runtime. The rest is just trivial C, to dereference and call the function and return.

With this, I was able to include the Linux and Apple GL implementations in the same file quite smoothly! You can see my additions here on GitHub, which I hence dub apple_cpp. Enjoy! 😃

Future Work

As mentioned, I don’t see the UI portions of UIKit/AppKit as ideal candidates for usage in C++. However, other lower-level frameworks like AVFoundation or Core Media might benefit from C++ bindings since you’re dealing with lots of Core Foundation libraries that use C semantics.

Even for the existing bindings, it would be great to have a way to automate the generation of the binding headers, at least for the majority that don’t require any special care.