I’ve previously deployed my game engine to macOS successfully with GLFW and macOS’s “vanilla” version OpenGL 4.1 library, everything was pretty smooth and sweet (and of course naive) at that time, and every few weeks I would port the latest updates from Windows side to macOS in an accumulated way. But as I started to utilize some C++17 new features, yep I mean <filesystem>
, which Apple still didn’t give us a certain answer (I’ve heard that there is something new with Apple Clang in the Xcode 10.2 update, that somehow <experimental/filesystem>
is magically appearing, but later I’ll talk about it separately), and targeting OpenGL version to the 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 anything related to 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 was really a moment that I started to hesitate 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 an architectural design with the native OS window event handling is better and a little bit more flexible. After 2 or 3 days refactoring, I successfully decoupled the entire window event system on Windows with GLFW and plugged Qt into it smoothly (ignore the annoying QtNativeEvent). And then I have to repeat the similar procedure for Linux (currently means Ubuntu), I need to write a native window event system for user input and display there too.
And for the sake of these reasons I mentioned above macOS became a far more impossible direction. Previously GLFW wrapped the platform window for me, now what should I do there without it? But GLFW provides a C++ API while standard macOS application is often written in Objective-C (or Swift recently), thus I assume it should have some bridging method to achieve this. So I googled a little bit and saw a weird thing called, Objective-C++.
If you googled that name as same as what I did, then you should have had a brief understanding of 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 our task is just simply to rename your Objective-C source file’s extension from .m to .mm, and then the XCode build toolchain would interpret it as an Objective-C++ file, and it would try to compile it with a mixed favor (Or you could change to recognize any file explicitly as an Objective-C++ file in your Xcode IDE).
Then I started to build a native Cocoa window accidentally and, why not get some Metal? But I want to integrate the Metal rendering module into my current rendering backend architecture, rather than alter anything in case to fit them. So basically speaking what I need is 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-exclusive code in C++ source file directly.
Luckily this kind of bridging hooking attaching mapping snapping problem should be a common pain because I’ve found some nice example and article easily about how to deal with it. The basic idea is using inheritance (OOP is not always so bad taste!) 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 abstract bridge class of the engine (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 the macOS porting seems would disappear sooner. 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 bridger (or you could call it as “Adapter Pattern” or “Adopter Pattern” or “Avenger Pattern” or something else, I’m really bad at enumerating those design pattern name) for the communications we imagined before. Native window and Metal Objective-C objects are created by the Objective-C entrance application, and then use them to construct Objective-C++ class instances and pass the pointer as pure C++ classes to the engine’s runtime, everything works so perfectly!
And of course, there are some disadvantages beneath. The first thing which needs extra care about is the memory and the life cycle of the Objective-C object. Since the Objective-C has a so-called ARC some sort of reference counter style memory management stuff, in order to safely maintain the life cycle of them, I would never pass any Objective-C object pointer directly to C++ code in case someone would delete it by accident. Instead, it is better to use a std::shared_ptr
(or std::make_shared<T>
directly) to wrap the raw Objective-C object we created to pass it to C++ object. Or a better and safer solution is, that we’d never inform C++ side anything about Objective-C object, like my example above. But if we have to dance next to the cliff necessarily, then we could use the language keyword
like __bridge_retained
and __bridge_transfer
to transfer the ownership of the raw Objective-C object back and forth:
id<MTLBuffer> oldOCObjectHandle;
// ...Assign a value to oldOCObjectHandle
void* newCppObject = (__bridge_retained void*)(oldOCObjectHandle);
id<MTLBuffer> newOCObjectHandle = (__bridge_transfer id<MTLBuffer>)(cppObject);
And furthermore, as you may have had discovered in my example, actually I 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 was reset to 0 at some moments, or maybe some garbage collector or defragiler works around and then it was moved to another location, 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 what we’ve talked about in the original design, but don’t implement any Objective object-related features in it 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 entrance module is compiled by Apple Clang, and the engine module is compiled by LLVM/Clang, just link the entrance module with the engine module I would have a fully functional application, kill two birds with one stone!
So now the final solution looks like this:
The 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:).