Search code examples
c++objective-ccmake

Mix C++ and Objective-C code using CMake and get C++ executable


The Idea

I'm trying to implement a game engine in C++.

I'm using macOS for development (arm-64 architecture). I want to create a window using Foundation framework.

CMakeLists.txt utilizes C++ and Objective-C languages, and I want to build a library containing only Objective-C code and then use this library in my C++ code (probably including the header). The end result should be a C++ executable.

The Project Structure

[src]
    ├── [Draw]
        ├── Window.cpp
        ├── Window.h
        └── [WindowBuilder]
            ├── WindowBuilder.cpp
            ├── WindowBuilder.h
            └── [macOS]
                ├── WindowBuilderObjC.h
                └── WindowBuilderObjC.mm
    ├── [Utils]
        └── Size.h
    ├── main.cpp
└── CMakeLists.txt

The Source Code

src/Draw/Window.h

#include <iostream>

#include <Utils/Size.h>
#include "WindowBuilder/WindowBuilder.h"

#pragma once

namespace Draw
{

    class Window
    {
    private:
        bool _isPresented;
        Utils::Size _size = Utils::Size(0, 0);

        WindowBuilder::WindowBuilder *_builder;

        Window();
        ~Window() = default;

    public:
        // Public static member function to access the singleton instance
        static Window &Instance()
        {
            // Guaranteed to be initialized once
            static Window instance;
            return instance;
        }

        // Delete the copy constructor and assignment operator
        Window(const Window &) = delete;
        Window &operator=(const Window &) = delete;

        void SetSize(Utils::Size size);
        void DrawWindow();
    };
}

src/Draw/Window.cpp

#include "Window.h"

namespace Draw
{
    Window::Window()
    {
        _builder = &WindowBuilder::WindowBuilder::Instance();
    }

    void Window::SetSize(Utils::Size size)
    {
        _size = size;
    }

    void Window::DrawWindow()
    {
        std::cout << "Draw window with size: [" << _size.GetWidth() << ":" << _size.GetHeight() << "]" << std::endl;

        _builder->BuildWindow(_size);
    }
}

src/WindowBuilder/WindowBuilder.h

// General
#include <iostream>

// Internal
#include <Utils/Size.h>
#include <Draw/WindowBuilder/macOS/WindowBuilderObjC.h>

#pragma once

namespace WindowBuilder
{
    class WindowBuilder
    {
    private:
        bool _isPresented;

        WindowBuilder() = default;
        ~WindowBuilder() = default;

    public:
        static WindowBuilder &Instance()
        {
            // Guaranteed to be initialized once
            static WindowBuilder instance;
            return instance;
        }

        // Delete the copy constructor and assignment operator
        WindowBuilder(const WindowBuilder &) = delete;
        WindowBuilder &operator=(const WindowBuilder &) = delete;

        void BuildWindow(Utils::Size size);
    };
}

src/WindowBuilder/WindowBuilder.cpp

#include "WindowBuilder.h"

namespace WindowBuilder
{
    void WindowBuilder::BuildWindow(Utils::Size size)
    {
        buildWindowWithSize(size);
    }
}

src/Draw/WindowBuilder/macOS/WindowBuilderObjC.h

#import <Cocoa/Cocoa.h>

#import "Utils/Size.h"

@interface WindowBuilderObjC : NSObject

- (void)buildWindowWithSize:(Utils::Size)size;

@end

@interface WindowDelegate : NSObject <NSWindowDelegate>
@end

@implementation WindowDelegate

- (BOOL)windowShouldClose:(id)sender
{
    [NSApp terminate:nil];
    return YES;
}

@end

src/Draw/WindowBuilder/macOS/WindowBuilderObjC.mm

#import "WindowBuilderObjC.h"

@implementation WindowBuilderObjC

- (void)buildWindowWithSize:(Utils::Size)size
{
    @autoreleasepool 
    {
        [NSApplication sharedApplication];
        
        NSRect frame = NSMakeRect(0, 0, 800, 600);
        NSUInteger style = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskResizable;
        NSWindow *window = [[NSWindow alloc] initWithContentRect:frame styleMask:style backing:NSBackingStoreBuffered defer:NO];
        [window setTitle:@"My Window"];
        [window center];
        
        WindowDelegate *delegate = [[WindowDelegate alloc] init];
        [window setDelegate:delegate];
        
        [window makeKeyAndOrderFront:nil];
        
        [NSApp run];
    }
}

@end

src/main.cpp

#include <iostream>

#include <Draw/Window.h>

#include <Utils/Size.h>

int main()
{
    std::cout << "Hello World" << std::endl;

    Utils::Size size(600, 800);

    auto &window = Draw::Window::Instance();
    window.SetSize(size);

    window.DrawWindow();
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.16)
project(GameEngine LANGUAGES CXX OBJC OBJCXX)

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# enable_language(OBJC)
# enable_language(OBJCXX)

add_executable(Engine src/main.cpp)

add_library(UtilsLib SHARED
    src/Utils/Size.h
)

set_target_properties(UtilsLib PROPERTIES LINKER_LANGUAGE CXX)

add_library(WindowBuilderObjCLib SHARED
    src/Draw/WindowBuilder/macOS/WindowBuilderObjC.h
        src/Draw/WindowBuilder/macOS/WindowBuilderObjC.mm
)

target_include_directories(WindowBuilderObjCLib
    PRIVATE
        ${CMAKE_CURRENT_SOURCE_DIR}/src/Draw/WindowBuilder/macOS
)

target_link_libraries(WindowBuilderObjCLib
    PRIVATE "-framework Cocoa"
    PRIVATE "-framework Foundation"
    PRIVATE "-framework AppKit"
)

# Set the language for the library to Objective-C++
set_target_properties(WindowBuilderObjCLib PROPERTIES
    LINKER_LANGUAGE "OBJCXX"
    XCODE_ATTRIBUTE_CLANG_ENABLE_OBJC_ARC YES
    XCODE_ATTRIBUTE_CLANG_ENABLE_MODULES YES
    XCODE_ATTRIBUTE_CLANG_ENABLE_OBJC_WEAK YES
)

add_library(DrawLib STATIC
        src/Draw/Window.h
        src/Draw/Window.cpp
)

target_sources(DrawLib
  PRIVATE
    src/Draw/WindowBuilder/WindowBuilder.h
    src/Draw/WindowBuilder/WindowBuilder.cpp
)

target_sources(DrawLib
  PUBLIC
    src/Draw/Window.h
    src/Draw/Window.cpp
)

set_target_properties(DrawLib PROPERTIES LINKER_LANGUAGE CXX)

find_library(FOUNDATION_FRAMEWORK Foundation)

# Link against required frameworks and libraries
target_link_libraries(DrawLib
    PRIVATE ${FOUNDATION_FRAMEWORK}
    "-framework AppKit"
    "-framework CoreGraphics"
    "-lobjc"
)

find_package(OpenGL REQUIRED COMPONENTS OpenGL)

include_directories(${CMAKE_SOURCE_DIR}/src)

add_subdirectory(src/Draw/WindowBuilder/macOS)

# Link the DrawLib and UtilsLib targets to the Engine target
target_link_libraries(Engine PRIVATE
    WindowBuilderObjCLib
    DrawLib
    UtilsLib
    OpenGL::GL
    ${FOUNDATION_FRAMEWORK}
)

The Error

Seems like when I include Window.h into main.cpp, the compiler looks for symbols in Cocoa/Cocoa.h file and finds Objective-C symbols instead of C++.

The error is following:

[build] In file included from /Users/user/MySource/GameEngine_Vulkan/src/Draw/Window.cpp:1:
[build] In file included from /Users/user/MySource/GameEngine_Vulkan/src/Draw/Window.h:4:
[build] In file included from /Users/user/MySource/GameEngine_Vulkan/src/Draw/WindowBuilder/WindowBuilder.h:6:
[build] In file included from /Users/user/MySource/GameEngine_Vulkan/src/Draw/WindowBuilder/macOS/WindowBuilderObjC.h:1:
[build] In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.3.sdk/System/Library/Frameworks/Cocoa.framework/Headers/Cocoa.h:12:
[build] In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.3.sdk/System/Library/Frameworks/Foundation.framework/Headers/Foundation.h:8:
[build] /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.3.sdk/System/Library/Frameworks/Foundation.framework/Headers/NSObjCRuntime.h:601:1: error: expected unqualified-id
[build] @class NSString, Protocol;
[build] In file included from /Users/user/MySource/GameEngine_Vulkan/src/Draw/WindowBuilder/WindowBuilder.cpp:1:
[build] In file included from /Users/user/MySource/GameEngine_Vulkan/src/Draw/WindowBuilder/WindowBuilder.h:6:
[build] ^
[build] In file included from /Users/user/MySource/GameEngine_Vulkan/src/Draw/WindowBuilder/macOS/WindowBuilderObjC.h:1:
[build] In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.3.sdk/System/Library/Frameworks/Cocoa.framework/Headers/Cocoa.h:12:
[build] In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.3.sdk/System/Library/Frameworks/Foundation.framework/Headers/Foundation.h:8:
[build] /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.3.sdk/System/Library/Frameworks/Foundation.framework/Headers/NSObjCRuntime.h:601:1: error: expected unqualified-id
[build] @class NSString, Protocol;
[build] ^
[build] /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.3.sdk/System/Library/Frameworks/Foundation.framework/Headers/NSObjCRuntime.h:603:9: error: unknown type name 'NSString'
[build] typedef NSString * NSExceptionName NS_TYPED_EXTENSIBLE_ENUM;
[build]         ^
[build] /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.3.sdk/System/Library/Frameworks/Foundation.framework/Headers/NSObjCRuntime.h:603:9: error: unknown type name 'NSString'
[build] typedef NSString * NSExceptionName NS_TYPED_EXTENSIBLE_ENUM;
[build]         ^
[build] /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.3.sdk/System/Library/Frameworks/Foundation.framework/Headers/NSObjCRuntime.h:604:9: error: unknown type name 'NSString'
[build] typedef NSString * NSRunLoopMode NS_TYPED_EXTENSIBLE_ENUM;
[build]         ^
[build] /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.3.sdk/System/Library/Frameworks/Foundation.framework/Headers/NSObjCRuntime.h:604:9: error: unknown type name 'NSString'
[build] typedef NSString * NSRunLoopMode NS_TYPED_EXTENSIBLE_ENUM;
[build]         ^
[build] /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.3.sdk/System/Library/Frameworks/Foundation.framework/Headers/NSObjCRuntime.h:606:19: error: unknown type name 'NSString'
[build] FOUNDATION_EXPORT NSString *NSStringFromSelector(SEL aSelector);
[build]                   ^
[build] /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.3.sdk/System/Library/Frameworks/Foundation.framework/Headers/NSObjCRuntime.h:606:19: error: unknown type name 'NSString'
[build] FOUNDATION_EXPORT NSString *NSStringFromSelector(SEL aSelector);
[build]                   ^

...

The Question

Can I build a standalone library using Objective-C with Cocoa lib included (probably) and that it will have a public interface, so that I can use it in C++ project?


Solution

  • So for anyone who's looking for such kind of implementation to be used:

    1. Write code in Objective-C that provides a functionality you want (in my case, creating a window in macOS):

    WindowBuilderObjC.h

    #ifndef WindowBuilderObjC_h
    #define WindowBuilderObjC_h
    
    #import <Cocoa/Cocoa.h>
    
    @interface WindowBuilderObjC : NSObject
    
    - (void)buildWindowWithWidth:(int)width height:(int)height;
    
    @end
    
    @interface WindowDelegate : NSObject <NSWindowDelegate>
    @end
    
    #endif /* WindowBuilderObjC_h */
    

    WindowBuilderObjC.mm

    #import "WindowBuilderObjC.h"
    
    @implementation WindowBuilderObjC
    
    - (void)buildWindowWithWidth:(int)width height:(int)height
    {
        @autoreleasepool 
        {
            [NSApplication sharedApplication];
            [[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
    
            NSRect frame = NSMakeRect(0, 0, 800, 600);
            NSUInteger style = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskResizable;
            NSWindow *window = [[NSWindow alloc] initWithContentRect:frame styleMask:style backing:NSBackingStoreBuffered defer:NO];
            [window setTitle:@"My Window"];
            [window center];
            
            WindowDelegate *delegate = [[WindowDelegate alloc] init];
            [window setDelegate:delegate];
            
            [window makeKeyWindow];
            [window orderFrontRegardless];
    
            [NSApp run];
        }
    }
    
    @end
    
    @implementation WindowDelegate
    
    - (BOOL)windowShouldClose:(id)sender
    {
        [NSApp terminate:nil];
        return YES;
    }
    
    @end
    

    2. Create a file (still in Objective-C) with C++ headers using keyword extern "C" (so they can be used in C++ code):

    WindowBuilderObjCWrapper.mm

    extern "C"
    {
    #import "WindowBuilderObjC.h"
    }
    
    extern "C" WindowBuilderObjC *createWindowBuilder()
    {
        WindowBuilderObjC* builder = [[WindowBuilderObjC alloc] init];
    
        return builder;
    }
    
    extern "C" void deleteWindowBuilder(WindowBuilderObjC *builder)
    {
        [builder dealloc];
    }
    
    extern "C" void buildWindow(WindowBuilderObjC *builder, int* width, int* height)
    {    
        [builder buildWindowWithWidth:*width height:*height];
    }
    

    3. Modify CMakeLists.txt to create a module library from your Objective-C files:

    CMakeLists.txt

    add_library(WindowBuilderObjCLib MODULE
        src/Utils/WindowBuilder/macOS/Objective-C/WindowBuilderObjCWrapper.mm
    )
    
    target_sources(WindowBuilderObjCLib
      PRIVATE
        src/Utils/WindowBuilder/macOS/Objective-C/WindowBuilderObjC.h
        src/Utils/WindowBuilder/macOS/Objective-C/WindowBuilderObjC.mm
    )
    
    target_link_libraries(WindowBuilderObjCLib
        PRIVATE "-framework Cocoa"
        PRIVATE "-framework Foundation"
        PRIVATE "-framework AppKit"
    )
    
    # Set the language for the library to Objective-C++
    set_target_properties(WindowBuilderObjCLib PROPERTIES
        LINKER_LANGUAGE "CXX"
        XCODE_ATTRIBUTE_CLANG_ENABLE_OBJC_ARC YES
        XCODE_ATTRIBUTE_CLANG_ENABLE_MODULES YES
        XCODE_ATTRIBUTE_CLANG_ENABLE_OBJC_WEAK YES
    )
    

    4. In C++ follow these steps:

    4.1. Open the library using dlfcn.h lib:

    // Open the library.
    lib_handle = dlopen("./build/libWindowBuilderObjCLib.so", RTLD_LOCAL);
    if (!lib_handle)
    {
        exit(EXIT_FAILURE);
    }
    

    4.2. (Optional) Use boost lib to check if the opened library contains the required function signatures:

    // Boost for lib info
    #include <boost/dll/library_info.hpp>
    
    static void PrintSymbols(std::string libPath)
    {
        // Class `library_info` can extract information from a library
        boost::dll::library_info inf(libPath);
    
        // Getting exported symbols
        std::vector<std::string> exports = inf.symbols();
    
        // Printing symbols
        for (std::size_t j = 0; j < exports.size(); ++j)
        {
            std::cout << exports[j] << std::endl;
        }
    }
    

    4.3. (Optional) Create method signatures in the C++ class that is going to use the library's functions (you can also build up your own implementation based on the signatures):

    // Objective-C wrapper signatures
    typedef void (*buildWindowObjC)(void *builder, int *width, int *height);
    typedef void *(*createWindowBuilderObjC)();
    typedef void *(*deleteWindowBuilderObjC)(void *builder);
    
    // Objective-C handlers
    createWindowBuilderObjC createWindowBuilderObjCHandler;
    buildWindowObjC buildWindowObjCHandler;
    deleteWindowBuilderObjC deleteWindowBuilderObjCHandler;
    
    // Objective-C Window Builder Instance
    void *windowBuilderObjC;
    

    4.4. Get functions from the library and cast it to the suitable signature:

    // Load functions
    createWindowBuilderObjCHandler = reinterpret_cast<createWindowBuilderObjC>(dlsym(lib_handle, "createWindowBuilder"));
    if (!createWindowBuilderObjCHandler)
    {
        exit(EXIT_FAILURE);
    }
    
    buildWindowObjCHandler = reinterpret_cast<buildWindowObjC>(dlsym(lib_handle, "buildWindow"));
    if (!buildWindowObjCHandler)
    {
        exit(EXIT_FAILURE);
    }
    
    deleteWindowBuilderObjCHandler = reinterpret_cast<deleteWindowBuilderObjC>(dlsym(lib_handle, "deleteWindowBuilder"));
    if (!deleteWindowBuilderObjCHandler)
    {
        exit(EXIT_FAILURE);
    }
    

    4.5. Use the Objective-C functions (via C++ headers) from the imported library:

    windowBuilderObjC = createWindowBuilderObjCHandler();
    
    int width = size.GetWidth();
    int height = size.GetHeight();
    buildWindowObjCHandler(windowBuilderObjC, &width, &height);
    
    // ...
    
    deleteWindowBuilderObjCHandler(windowBuilderObjC);
    
    // ...
    

    Thats it. Hope it can be useful for someone.