Welding (THE latest) C++ to Metal, and macOS

For what?

I've previously deployed my game engine to macOS successfully with GLFW and macOS's "vanilla" version OpenGL 4.1 library, everything was nice and sweet (and of course naive) at that time, and every few weeks I would port new changes from Windows side to it in an accumulating way. But as I started to use some C++17 new features, yep I mean <filesystem>, which Apple still didn't give us a certain answer (I've heard about Xcode 10.2 update that there is something new with Apple Clang that somehow <experimental/filesystem> is just there, but later I'll talk about it separately), and targeting OpenGL version to almost latest 4.5 for quite few new features, I have to deprecate macOS for some moments because, why not?

So the frustrated feeling always tangles around when I started to think about macOS until one day I realized maybe I should just forget about Apple Clang, it's not the only fruit on the tree. Then I tested the LLVM/Clang package provided by Homebrew (since I use GCC on Linux, it's better to have another compiler/linker choice on macOS for a wider test scenario), and of course, it works.

But the problem related to OpenGL is still quite unsolvable, and when I heard about macOS 10.14 would deprecate OpenGL officially, there is really a moment I was thinking maybe it's time to say bye-bye to macOS. I wanted to use Metal, but I really didn't figure out how to integrate a Swift written module into C++. Then, I threw the idea about the macOS porting to a dusty corner as same as how Apple did with OpenGL decades ago.

And recently I removed the engine's dependency on GLFW, because I need to use Qt as the editor framework, then choose a design with the native window handling is better and more flexible. After 2 or 3 days refactoring, I successfully decoupled the entire window event system with GLFW, and plugged into Qt smoothly (ignore the annoying QtNativeEvent). Then I have to do the same thing for Linux (currently means Ubuntu), I need to write a native window event system for user input and display there too.

Then how?

And then macOS became a far more impossible direction, previously GLFW wrapped the platform window for me, now without it what should I do there? But GLFW provides a C++ API, while standard macOS application is often written in Objective-C, then it should have some bridging method to do this. So I googled a little bit and see a weird thing called, Objective-C++.

If you googled that name as same as me, then you should have a brief understanding about how to connect Objective C to C++, we just need to configure something on the build pipeline then we could build an Objective C source file mixed with C++ code together. In practice just simply rename your Objective-C source file's extension from .m to .mm, and then the build toolchain would consider it as an Objective-C++ file, and it would try to compile it with a mixed interpretation (Or you could change to recognize the file explicitly as an Objective-C++ file in your Xcode IDE).

Then I started to work on a native Cocoa window and, why not get some Metal? But I want to integrate them into my current architecture, not to alter something to fit them. So basically I need a bidirectional communication solution between the C++ engine code and the Objective-C code. Use Objective-C++ could only solve the problem on Objective-C side, it's always impossible to put any Objective-C language-set-only code in C++ file directly.

Luckily this kind of bridging hooking attaching mapping snapping problem should be common because I've found some nice example and article easily about how to deal with it. The basic idea is using inheritance to encapsulate the Objective-C object into a C++ object, and provide some virtual functions as the interface. Then it just becomes a solvable problem, I just need to wrap the window handle into an Objective-C++ class which inherits from a C++ class, and pass the handle or pointer of it to C++ side in runtime.

This is how an Objective-C++ header file looks like, it contains an Objective-C++ class declaration, while it looks just as same as a 100% pure C++ class who inherits from an engine's abstract bridge class (the private member pointers are Objective-C object pointers, I would explain why it is here later):

#define NO_ANY_OBJECTIVE_C_SXXT_HERE
class MacWindowSystemBridgeImpl : public MacWindowSystemBridge
{
public:
    explicit MacWindowSystemBridgeImpl(MacWindowDelegate* macWindowDelegate, MetalDelegate* metalDelegate);
    ~MacWindowSystemBridgeImpl();

    bool setup(unsigned int sizeX, unsigned int sizeY) override;
    bool initialize() override;
    bool update() override;
    bool terminate() override;

    ObjectStatus getStatus() override;
private:
    ObjectStatus m_objectStatus = ObjectStatus::SHUTDOWN;
    MacWindowDelegate* m_macWindowDelegate = nullptr;
    MetalDelegate* m_metalDelegate = nullptr;
};

And the .mm source file is mixed with the C++ function declaration and Objective-C implementation, "bridging" happens here:

// Personally I feel O-C is interesting to write painful to read
bool MacWindowSystemBridgeImpl::setup(unsigned int sizeX, unsigned int sizeY) {
    NSRect frame = NSMakeRect(0, 0, sizeX, sizeY);

    [m_macWindowDelegate initWithContentRect:frame
                                   styleMask:NSWindowStyleMaskTitled|NSWindowStyleMaskClosable
                                     backing:NSBackingStoreBuffered
                                       defer:NO];

    return true;
}
// And so on...

And it's quite a shame to talk about, I finally knew Metal support Objective-C at that time as late as I started to search about the window solution. But it's quite an additional inspire for me because all the barrier on macOS porting seems would disappear. Now the Objective-C side objects are created in such a way:

// The main entry
int main(int argc, const char * argv[]) {
    [NSApplication sharedApplication];
    AppDelegate* appDelegate = [[AppDelegate alloc] init];
    [NSApp setDelegate:appDelegate];
    [NSApp run];
    return 0;
}

//......
//......
//......
// the AppDelegate initialization, allocate the Objective-C objects

- (id)init
{
    m_macWindowDelegate = [MacWindowDelegate alloc];
    m_metalDelegate = [MetalDelegate alloc];


    // Allocate Objective-C++ bridge implementation class instances, the simple new operator allocation is quite enough, just remember to delete them in dtor

    m_macWindowSystemBridge = new MacWindowSystemBridgeImpl(m_macWindowDelegate, m_metalDelegate);
    m_metalRenderingSystemBridge = new MTRenderingSystemBridgeImpl(m_macWindowDelegate, m_metalDelegate);

    // Start the engine c++ code
    // Pass the bridge instance pointers to the engine
    const char* l_args = "-renderer 4 -mode 0";
    if (!InnoApplication::setup(m_macWindowSystemBridge, m_metalRenderingSystemBridge, (char*)l_args))
    {
        return 0;
    }
    if (!InnoApplication::initialize())
    {
        return 0;
    }


    // the classic while-loop for update
    [NSTimer scheduledTimerWithTimeInterval:0.000001 target:self selector:@selector(drawLoop:) userInfo:nil repeats:YES];

    return self;
}

And now I've implemented a bridge (or you could call it as "Adapter Pattern" or "Adopter Pattern" or something else, I'm really bad at remembering those design patterns name) for these communications. Native window and Metal Objective-C objects are created by the Objective-C application entry, and then use them to construct Objective-C++ classes, and pass the pointer as pure C++ classes to the engine's runtime, everything was so perfect!

Devils in the details???

And of course, there are some disadvantages. The first thing which needs extra care about is the memory, since Objective-C object has a so-called ARC some sort of memory management stuff, for to safely maintain the lifecycle of them, I would never pass any Objective-C object pointer directly to C++ code in case someone would delete it accidentally. Instead, it is better to use a std::shared_ptr (or std::make_shared<T> directly) to wrap the Objective-C object we created to pass it to C++ object, or better, never inform C++ side anything about Objective-C like my example.

And furthermore, as you may find, I actually add another indirection in between a C++ declaration and an Objective-C implementation. Because if you just pass your Objective-C++ object by a safer std::shared_ptr to C++, you would still occur some weird problems because the object is not always "there" in memory! Maybe its reference count is reset to 0 at a moment and then was deleted, I didn't dig into the reason so deeply, as the solution what I've figured out, that I would choose to use an Objective-C++ object as the bridge like the original design, but it doesn't implement any Objective object-related features by itself directly, instead it just owns a pure Objective object's reference (I use pointer actually), and dispatch all the invocation to that object.

Another not so bad "side-effect" when I removed the dependency on GLFW is, I could enjoy all the latest C++ features without worry about anything. Because now the platform entry module is compiled by Apple Clang, and the engine module is compiled by LLVM/Clang, just link the entry module with the engine module I would have a fully functional application, a kill two birds with one stone!

So now the final solution looks like this:

Created with Raphaël 2.2.0C++ ModuleObjective-C++ ModuleObjective-C Module

The further possible burden would relate to the indirection cost and the code redundancy, in this kind of design if you want any interactions in between, you may have to write lots of wrapper functions, and it quite reminds me of a similar thing which everybody used at least for a while when they were learning 3D programming, da, GLAD/GLEW/GLBLABLA these OpenGL function loader:).

About The Author

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.