Коротко о себе

Я программист энтузиаст, люблю пробовать что-то новое и затрагивать неизвестные для меня области программирования. На работе и дома я в основном пишу на С++, но далеко им не ограничиваюсь.

Описание задачи

Однажды, для одного домашнего проекта, мне пришлось реализовывать приложение которое будет воспроизводить аудио на OSX. Я сразу же полез в какой-то cpp-awesome и нашел себе подходящую библиотечку, но потом что-то ударило мне в голову и я вспомнил про многими (в моих кругах) хвалений Apple SDK. Посмотрел я немного на него, и что-то не увидел там C++ или хотяб C в выпадающем окне с языками... Хорошо, подумал я, в Objective-C должна же быть прямая совместимость с C++ или C, и что если писать все на том же С++, использовать все то что я так давно и хорошо знаю, но при этом использовать Objective-C заголовки Apple SDK.

Для упрощения задачи будем считать что нам нужно реализовать некий класс Printer(std::string) с методом Print, которий должен печатать в Stdout, сообщение которое было передано в конструкторе используя инструменты Foundation/Foundation.h (NSLog).

Printer.h
class Printer {
public:
    Printer(const std::string&);
    void Print() const;
};

Система

Собирать все я буду на OSX 12.6 (M1). Компилировать буду Apple clang 14 arm. Но так же можно использовать GCC и LLVM Clang без проблем. Использовал C++20, но там достаточно и С++17.

Проба пера

Первым делом просто пробуем подключить #include <Foundation/Foundation.h> в заголовочный файл Printer.h который позднее будет использован в main.cpp. Компилятор явно дал мне понять что скомпилировать Objective-C и C++ вместе (в cpp) файле у меня не получится...

Глава 1 (Си)

Отойдем от нашей главной задачи (чтобы вернутся потом к ней) и попробуем воспользоваться линкером и прямой совместимостью Objecive-C и C++ к Си.

printer.h
#ifndef PRINTER_H
#define PRINTER_H

void print(const char*);

#endif

Так как Objective-C может спокойно использовать Си код без особого труда пишем printer.m файл

printer.m
#include "printer.h"
#import <Foundation/Foundation.h>

void print(const char* str) {
    NSString *objCString = [[NSString alloc] initWithCString: str encoding:NSUTF8StringEncoding];
    NSLog(@"C Version: %@", objCString);
}

Makefile

Тут явно передаем флаги -lobjc и Фреймворк из которого будем брать код (-framework Foundation) линкеру

CXX = clang++
CXX_FLAGS = -std=c++20 -g
LDOBJC_FLAGS = -lobjc -framework Foundation

bin: main.o printer_c.o
	$(CXX) $? $(LDOBJC_FLAGS) -o $@

main.o: main.cpp
	$(CXX) -c $? $(CXX_FLAGS) -o $@

printer_c.o: printer.m
	$(CXX) -c $? -o $@

clean:
	rm -rf bin
	rm -rf *.o

.PHONY: clean

Используем printer.h в нашем main.cpp и все бы ничего, но линкер все равно ругается. Дело тут в том, что objective-c компилируется как Си, значит и использовать его нужно соответствующе. Необходимо добавить extern "C", чтобы указать линковщику что реализация данного заголовочного файла была скомпилирована как Си код.

main.cpp
extern "C" {
#include "printer.h"
}
#include <string>

int main(int argc, char** argv) {
    std::string first = "first";
    print(first.c_str());
    return 0;
}

Дальше не буду добвавлять main.cpp, так как вызывающий код там будет очень похож и очевиден.


Хорошо, мы подружили C++ с C и Objective-C, уже что-то, но пока это далеко от поставленной задачи.

Глава 2 (С++)

Давайте все таки вернёмся к нашему классу Printer и сделаем так чтобы сообщение хранилось собственно в классе.

Код

printer2.h

#ifndef PRINTER_2_H
#define PRINTER_2_H
#include <string>

class Printer {
public:
    Printer(const std::string&);
    void Print() const;
private:
    std::string message_;
};
#endif

printer2.cpp

#include "printer2.h"
Printer::Printer(const std::string& message): message_(message) {}

printet2.m

#include "printer2.h"
#import <Foundation/Foundation.h>

void Printer::Print() const {
    NSString *objCString = [[NSString alloc] initWithCString: message_.c_str() encoding:NSUTF8StringEncoding];
    NSLog(@"C++ Version: %@", objCString);
}

Makefile

CXX = clang++
CXX_FLAGS = -std=c++20 -g
LDOBJC_FLAGS = -lobjc -framework Foundation

bin: main.o printer2_cpp.o printer2_objc.o
	$(CXX) $? $(LDOBJC_FLAGS) -o $@

main.o: main.cpp
	$(CXX) -c $? $(CXX_FLAGS) -o $@

printer2_cpp.o: printer2.cpp
	$(CXX) -c $? $(CXX_FLAGS) -o $@

printer2_objc.o: printer2.mm
	$(CXX) -c $? -o $@

clean:
	rm -rf bin
	rm -rf *.o

.PHONY: clean

Все бы ничего но теперь компилятор ругается что он встретил С++ код там где его не должно быть. Тут на помощь приходит Objective-C++ (.mm файл). По сути это даже не новый язык, а просто надстройка над Objective-C, которая позволяет использовать внутри C++ код. меняем printer2.m -> printer2.mm. При этом, так как printer2.mm будет скомпилирован как С++ исходник, нам уже не требуется писать extern "C" для помощи линкеру.

Хорошо, вроде как подружили, но есть 2 нюанса:

  1. Почему мы просто не можем миксовать файлы С++ и Objective-C в Objective-C++ и использовать его во всем проекте?

    Тут у меня явного ответа нету, скажу лишь что когда я хочу писать на С++ и использовать инструменты которые написаны на Objective-C последнее что я хочу видеть, это миграцию всего проекта на Objective-C++ и замена .cpp на .mm.

    Плюс там значительно падает, и так не очень хорошее, время компиляции и не работают многие оптимизации.

  2. То что я сделал это конечно хорошо, но в реальной задачи в полях у нас тоже объекты из Apple SDK на Objective-C, значит наше решении уже нам не подходит, потому что .cpp файл будет использовать заголовочный файл с объектами из Objective-C что я показывал в проблеме "Проба Пера". Тут мы плавно подбираемся к финальной версии.

Финальная версия

Для использования класса с объектами из Objective-C при этом, он не может быть в заголовочном файле, так как он используется C++ кодом, будет использована идиома PIMPL.

#ifndef PRINTER_3_H
#define PRINTER_3_H
#include <memory>
#include <string>

using unique_void_ptr = std::unique_ptr<void, void(*)(void const*)>;
class Printer3 {
public:
    Printer3(const std::string& message);
    void Print() const;
private:
    unique_void_ptr pImpl_;    
};

#endif

Тут класс ( с Objective-C объектами) будет находиться в указателе pImpl_. Да, указатель на void, но forward declaration не прокатит, так как класс будет не C++ классом.

#import <Foundation/Foundation.h>
#include "printer3.h"

@interface PrinterImpl: NSObject {
    NSString* _message;
}
@property (assign)NSString *message;

- (id)initWithMessage:(NSString *)message;
- (void)Print;
@end
@implementation PrinterImpl
@synthesize message = _message;

- (id)initWithMessage:(NSString *)message {
    self = [super init];
    if (self) {
        self.message = message;
    }
    return self;
}

- (void)Print {
    NSLog(@"Objective Str: %@", _message);
}
@end

template<typename T>
auto unique_void(T * ptr) -> unique_void_ptr{
    return unique_void_ptr(ptr, [](void const * data) {
            [(id)data dealloc];
    });
}

Printer3::Printer3(const std::string& message) : pImpl_(unique_void(
    [[PrinterImpl alloc] 
        initWithMessage: [[NSString alloc] initWithCString: message.c_str() encoding:NSUTF8StringEncoding]
    ]
)) {}

void Printer3::Print() const {
    [(id)pImpl_.get() Print];
}

Тут я создаю в конструкторе новый объект типа PrinterImpl и выделяю на него память, unique_void функция создаст новый указатель и позаботится о том чтобы память потом почистилась. Дальше во всех вызовах к Printer3 мы делегируем работу PrinterImpl.

Makefile
CXX = clang++
CXX_FLAGS = -std=c++20 -g
LDOBJC_FLAGS = -lobjc -framework Foundation

bin: main.o printer3_objc.o
	$(CXX) $? $(LDOBJC_FLAGS) -o $@

main.o: main.cpp
	$(CXX) -c $? $(CXX_FLAGS) -o $@

printer3_objc.o: printer3.mm
	$(CXX) -c $? $(CXX_FLAGS) -o $@

clean:
	rm -rf bin
	rm -rf *.o

.PHONY: clean

Bonus (CMake)

Бонусом добавляю CMakeLists.txt чтобы можно было генерировать xcodeproject.

CMakeLists.txt
cmake_minimum_required(VERSION 3.5)
project(TEST VERSION 0.0.1 LANGUAGES C CXX OBJC)

set(TARGET_NAME ${PROJECT_NAME}_bin)

if (NOT APPLE)
    message(FATAL_ERROR "Build available only for OSX")
endif()


find_library(FOUNDATION Foundation)
if (NOT FOUNDATION) 
    message(FATAL_ERROR "Could not found foundation framework")
endif()

file(GLOB SOURCE_FILES 
    ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp 
    ${CMAKE_CURRENT_SOURCE_DIR}/*.c
    ${CMAKE_CURRENT_SOURCE_DIR}/*.mm 
    ${CMAKE_CURRENT_SOURCE_DIR}/*.m
)

add_executable(${TARGET_NAME} ${SOURCE_FILES})
set_target_properties(${TARGET_NAME} PROPERTIES
    CXX_STANDARD 17
    CXX_STANDARD_REQUIRED ON
    CXX_EXTENSIONS OFF
)
target_link_libraries(${TARGET_NAME} PRIVATE ${FOUNDATION})

Заключение

К чему этот надуманный пример с принтером? Вместо NSString в последнем примере может быть любой объект из Apple SDK или с любой другой Objective-C библиотеки (В моем случае это AVAudioPlayer из фреймворка AVAudio). Это все можно инициализировать в конструкторе C++ класса и вызывать Objective-C код прямо из C++.

Ресурсы

https://en.cppreference.com/w/cpp/language/pimpl

https://andreicalazans.medium.com/how-to-interop-between-objective-c-and-c-cd0d7ff0e100

Комментарии (36)


  1. storoj
    09.12.2022 18:19

    В не-ARC среде (и в бородатые времена) класс бы выглядел так:

    @interface PrinterImpl: NSObject
    // объявлять ivar уже не нужно
    @property (retain) NSString *message;
    
    - (instancetype)initWithMessage:(NSString *)message;
    - (void)print;
    @end
    
    @implementation PrinterImpl
    // synthesize уже не нужен,
    // и вроде как он сгенерирует ivar "_message", а не "message"
    
    - (void)dealloc {
        [_message release];
        [super dealloc];
    }
    
    - (instancetype)initWithMessage:(NSString *)message {
        self = [super init];
        if (self) {
            // запись в ivar, чтобы избежать возможных сайд-эффектов
            // когда перегружен setMessage
            _message = [message retain];
        }
        return self;
    }
    


  1. awoland
    09.12.2022 11:08
    +1

    А разве существуют какие-либо проблемы с использованием Objective-C++ совместно с C++ кодом? Насколько мне известно, таких проблем никогда не было.


    1. Dimalovanyy Автор
      09.12.2022 13:02
      +1

      Смотря что вы имеете ввиду под проблемами. Использовать С++ код в Objective-C можно без проблем, это будет, как вы и написали, Objective-C++. А если стоит задача использовать Objective-C++ в существующем C++ коде, то тогда уже появляются проблемы описание в статье.


  1. storoj
    09.12.2022 16:45

    Новый день, новая статья на тему iOS/macos разработки, вызывающая вопросы.

    Называется "Используем Objective-C в C++ без проблем". Хотя, по моим ощущениям, у автора есть пробелы и в одном, и в другом.

    Всё началось с Принтера:

    class Printer {
    public:
        Printer(const std::string&);
        void Print() const;
    };
    

    На этом этапе я уже начал подозревать, что С++ сейчас будет такой себе. Я тоже далеко не эксперт, но некоторые вещи вроде понимаю.

    Лирическое отступление о C++.

    Как-то раз я посмотрел хороший доклад про этот язык, но к сожалению напрочь забыл кто (кажется, Андрей Александреску) и что рассказывал. Но там была одна хорошая мысль, которую я надолго запомнил.

    Мысль о том, что глядя на сигнатуру функции в С++ часто можно сразу представить себе реализацию, можно понять что функция делает. И говоря "сигнатура" имеется в виду именно список агрументов и возвращаемое значение, на название можно даже не смотреть.

    Примеры:

    // функция только читает из x
    void f(const T& x);
    
    // функция записывает в x по ссылке.
    // не _может_, а именно так или иначе _записывает_ в x.
    // иначе какого чёрта это не const ссылка?
    void f(T& x); 
    
    // функция использует копию x
    void f(T x);
    
    // функция хочет "поглотить" x, например сохранить куда-то.
    // можно сделать move
    void f(T&& x)
    

    Так вот. Глядя на интерфейс Printer, можно сразу догадаться, что наверное Print() будет печатать то, что было передано в конструкторе. Но конструктор говорит о том, что он собирается только читать из аргумента. Хотя на самом деле, он хочет взять аргумент и положить себе в поле. В итоге произойдёт копирование, которого можно было избежать. Но дело даже не в копировании, а в запутывающем интерфейсе. Он говорит "я буду только читать", а на деле происходит что-то другое.

    Одна лишь эта "мелочь" уже подрывает моё доверие к статье о C++.

    Потом пошёл ObjectiveC.

    template<typename T>
    auto unique_void(T * ptr) -> unique_void_ptr{
        return unique_void_ptr(ptr, [](void const * data) {
                [(id)data dealloc];
        });
    }
    

    Здесь я тоже склонен заявить, что автор совершенно не разбирается в управлении памятью в ObjectiveC.

    unique_void функция создаст новый указатель и позаботится о том чтобы память потом почистилась.

    Каким образом почистилась? Если подразумевается вызов free() на указателе на ObjectiveC объект, то я нахожу это плохой идеей. Жизненный цикл ObjectiveC объектов управляется счётчиком ссылок. Что будет, если указателем на PrinterImpl владеет кто-то ещё? unique_void() просто уничтожит этот принтер, а другие пользователи получат мусор, хотя имели сильную ссылку. Более того, unique_void счётчика ссылок не увеличивает, но умеет "убивать". Что если бы это была среда с автоматическим подсчётом ссылок? Тогда PrinterImpl сразу умер бы, так как никто не держит сильной ссылки на него, и позже этот умный указатель попытался бы освободить мусор. Потом этот прямой вызов dealloc. Почему было просто не сделать retain перед созданием unique_ptr, а вместо dealloc не вызывать release? Тогда и память точно освободится правильно (может там далеко не только free() происходит), и заодно она не освободится тогда, когда этого делать не стоит (когда счётчик ещё не дошёл до нуля).

    По моим ощущениям, тут скрестились две неграмотности, и велика вероятность получить неграмотность в квадрате как результат.

    ps.

    затрагивать неизвестные для меня области программирования


    1. storoj
      09.12.2022 17:10

      unique_ptr здесь даже не вызовет никакое free(), а только вызовет dealloc, что в свою очередь не приводит ни как каким освобождениям памяти. По всем признакам вместо dealloc должен был быть release, плюс заблаговременный retain.


      1. storoj
        09.12.2022 18:48

        Хотя про dealloc я тоже слегка наврал. Я всегда относился к нему как скорее к коллбеку перед тем как система сделает free().

        Однако, я провёл такой эксперимент:

            id x = [NSObject new];
            [x dealloc];
            [x dealloc]; // EXC_BAD_ACCESS (code=1, address=0xbeaddead4060)
        

        Из чего можно сделать вывод, что память всё же освобождается. Я поставил брейкпоинт на -[NSObject dealloc], и единственной инструкцией в реализации является прыжок на _objc_rootDealloc, в теле которой и находится free().

        Но как бы там ни было, напрямую я бы всё равно dealloc не вызывал, потому что на этот объект могут быть сильные ссылки, которым станет плохо.


    1. Dimalovanyy Автор
      09.12.2022 17:19

      Спасибо за развернутый комментарий! На счет С++, я с вами согласен, но частично. Углубляться в тему ссылок и перемещения можно вечно и дискуссия не потухнет еще долго, оставлю лишь ссылку на отличный материал по этой теме http://scrutator.me/post/2018/07/30/value_vs_reference.aspx (тут каждый делает выводы сам, очевидного ответа там нету).
      На счет интерфейса класса я с вами согласен, что было б правильней передавать аргумент сообщения напрямую в Print, но я хотел свести все к "финальной версии", где у нас должно существовать именно поле которое является обьектом Objective-C и функции которые должны взаимодействовать с этим полем, по этому пример и не совсем правильный по дизайну.

      На счет, управлением памяти, вы действительно правы и ваше предложение с retain действительно имеет место быть. Но указателем на PrinterImpl, теоретически владеть кто-то еще может, но фактически он под unique_ptr, что означает если ты достаешь от туда сырой указатель unique_ptr::get() то ты получишь "не владеющий" указатель, unique_ptr для того и создан чтобы не было двух владеющих указателей на один участок памяти.
      Среду с автоматическим подсчетом ссылок я действительно не брал в расчет, так как подразумевал что С++ классы будут вызываться и использоваться только в среде С++, когда я писал статью я не рассматривал что другой Objective-C++ код будет их использовать.


      1. storoj
        09.12.2022 17:34

        но фактически он под unique_ptr

        но что мешало кому-то другому им владеть до того, как из него сделали unique_ptr?

        Как бы там ни было, освобождение ресурсов всё равно было некорректным. И как правило, когда кто-то делает release, ему же стоило заранее сделать retain.

        Наконец, я бы порекомендовал побольше изучить нюансы управления памятью в Objective-C++, ну или же просто поэкспериментировать. Я так понимаю, что всё-таки основное назначение unique_ptr это скорее нежелание делать деструктор и самостоятельно освобождать ресурсы. Есть вероятность, что unique_ptr вообще не нужен в некоторых сетапах.

        Objective-C++ имеет больше фич, чем просто "собрать файл со смесью двух языков". Ещё вроде было можно смешивать данные в коллекциях (ObjC объекты в c++ коллекциях и наоборот), использовать лямбды вместо блоков, и что-то подобное. Я сам мало пользовался ObjC++, но я предполагаю, что управление памятью это одна из основных проблем, которая должна была бы на каком-то уровне да решаться.

        К сожалению, документацию по Objc++ найти практически невозможно. https://clang.llvm.org/docs/AutomaticReferenceCounting.html – тут конечно что-то упоминается, но без особых деталей.

        Вот какая-то археологическая раскопка, в которой хотя бы есть какие-то примеры: http://web.archive.org/web/20101203170217/http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/ObjectiveC/Articles/ocCPlusPlus.html.

        Я это всё нашёл в этом треде: https://developer.apple.com/forums/thread/76892.


        1. Dimalovanyy Автор
          09.12.2022 17:41

          Спасибо за источники (будут обязательные к прочтению :) ), ваше замечание с release абсолютно коректно


      1. storoj
        09.12.2022 17:36

        Углубляться в тему ссылок и перемещения можно вечно и дискуссия не потухнет еще долго, оставлю лишь ссылку на отличный материал по этой теме

        Но что будет, если я передам гигабайтную rvalue строку в конструктор? Вместо мгновенного "поглощения" мне может не хватить ещё одного гигабайта памяти. Зачем самому себе мешать жить?


        1. Dimalovanyy Автор
          09.12.2022 17:41

          А что если я на вход передам lvalue и эта строка будет потом использоваться в других объектах и классах (везде ее перемещать?), напоминаю, перемещение Не бесплатно.


          1. storoj
            09.12.2022 18:14

            Написал маленькую программку, и да, я согласен. Почему-то меня с утра замкнуло, и я вдруг решил, что в T&& всё-таки можно передавать и lvalue тоже, якобы "оно само скопируется если надо, и превратится в rvalue", что очевидно глупость.

            Но не лучше ли тогда всё же явно сделать два конструктора? Разве есть какая-то гарантия того, что компилятор будет хотя бы пытаться не копировать из const&? Неприятно конечно два раза писать одно и то же, но если это всё же имеет практический смысл, то я бы не ленился.


            1. Dimalovanyy Автор
              09.12.2022 18:30
              +1

              Практическое применение второго конструктора с rvalue, все же есть, но наверное оно больше для того чтобы показать что вызывающая сторона больше не владеет обьектом. В класических примерах я бы все таки рассчитывал на оптимизации (такие как Copy elision).


      1. storoj
        09.12.2022 17:38

        Но ссылка на статью вроде хорошая, правда там надо прям очень медленно читать, добавлю себе в закладки, спасибо.


        1. storoj
          09.12.2022 17:49

          Прочитал вторую половину статьи несколько раз, и, честно говоря, в очередной раз пришёл к выводу, что я уже вообще перестал понимать что происходит в С++ :) Так что теперь я уже жалею о том, что начал что-то там затирать про сигнатуры, хотя мне всё ещё кажется разумным в явном виде декларировать и себе, и другим, и компилятору о своих реальных намерениях и ожиданиях от выполнения. Мне почему-то не очень нравится идея того, что какой-то определённый компилятор начиная с какого-то года, иногда, может оптимизировать и превратить что-то, что должно было бы быть медленным, во что-то быстрое.


          1. storoj
            09.12.2022 17:53

            Мне вспомнилось это видео: https://www.youtube.com/watch?v=FJJTYQYB1JQ

            Если я всё правильно помню, то там Андрей Александреску говорит о том, что в современном мире верить можно только замерам, а все предположения о "быстром" и "медленном" часто оказываются ложными на практике.


          1. Dimalovanyy Автор
            09.12.2022 18:36
            +1

            Хех, а с шаблонной магией и forwarding ссылками вообще красота). Мне кажется что любая статья где хоть как-то участвует с++, сводится к какому-то недопониманию и непринятию :), в том числе это и тема оптимизации. Эх **неуверено говорит** раньше было лучше (и добавляет флаг std=c++20) :)


            1. storoj
              09.12.2022 18:53

              Что правда, то правда, я впредь подумаю не дважды, не трижды, а пять раз, прежде чем начинать какие-то беседы о С++.


    1. storoj
      09.12.2022 17:19
      +1

      Не очень ясно, включен ли Automatic Reference Counting.

      Если нет, то [[NSString alloc] initWithCString: message.c_str() encoding:NSUTF8StringEncoding] не хватает autorelease, а классу PrinterImpldealloc и освобождения ресурсов.

      Если же ARC включен, то PrinterImpl.message должен по-хорошему не доживать до использования, потому что у @property модификатор assign вместо retain или strong. assign не увеличивает счётчика ссылок, а значит только что созданный NSString должен был бы автоматически умереть, так как никто не держит сильной ссылки на него.

      Вне зависимости от включенности ARC, свойство должно было быть объявлено как @property (retain/strong) NSString *message;


      1. Dimalovanyy Автор
        09.12.2022 17:33

        ARC - нет


        1. storoj
          09.12.2022 18:19

          В не-ARC среде (и в бородатые времена) класс бы выглядел так:

          @interface PrinterImpl: NSObject
          // объявлять ivar уже не нужно
          @property (retain) NSString *message;
          
          - (instancetype)initWithMessage:(NSString *)message;
          - (void)print;
          @end
          
          @implementation PrinterImpl
          // synthesize уже не нужен,
          // и вроде как он сгенерирует ivar "_message", а не "message"
          
          - (void)dealloc {
              [_message release];
              [super dealloc];
          }
          
          - (instancetype)initWithMessage:(NSString *)message {
              self = [super init];
              if (self) {
                  // запись в ivar, чтобы избежать возможных сайд-эффектов
                  // когда перегружен setMessage
                  _message = [message retain];
              }
              return self;
          }
          


          1. Dimalovanyy Автор
            09.12.2022 18:31

            Спасибо, да, примерно так я себе и представлял класс, после того как начал изучать вопрос по вашим референсам.


  1. storoj
    09.12.2022 20:28
    +1

    Как я и думал, если включить ARC, то жизнь немного упрощается. Вот мой пример:

    PrinterImpl – ObjC принтер.

    // PrinterImpl.h
    
    #import <Foundation/Foundation.h>
    
    @interface PrinterImpl: NSObject
    @property (nonatomic, strong) NSString *message;
    
    - (instancetype)initWithMessage:(NSString *)message;
    + (instancetype)printerWithMessage:(NSString *)message;
    
    - (void)print;
    @end
    
    #import "PrinterImpl.h"
    
    @implementation PrinterImpl
    
    - (void)dealloc {
      NSLog(@"PrinterImpl dealloc");
    }
    
    - (instancetype)initWithMessage:(NSString *)message {
      self = [super init];
      if (self) {
        _message = message;
      }
      return self;
    }
    
    + (instancetype)printerWithMessage:(NSString *)message {
      return [[self alloc] initWithMessage:message];
    }
    
    - (void)print {
      NSLog(@"message: %@", self.message);
    }
    
    @end
    

    Его можно отдельно расширить поддержкой std::string:

    // PrinterImpl+Cpp.h
    
    #import "PrinterImpl.h"
    #import <string>
    
    @interface PrinterImpl(Cpp)
    - (instancetype)initWithMessageStdString:(const std::string&)message;
    + (instancetype)printerWithMessageStdString:(const std::string&)message;
    @end
    
    // PrinterImpl+Cpp.mm
    
    #import "PrinterImpl+Cpp.h"
    
    @implementation PrinterImpl(Cpp)
    
    - (instancetype)initWithMessageStdString:(const std::string&)message {
      return [self initWithMessage:[[NSString alloc] initWithCString:message.c_str()
                                                            encoding:NSUTF8StringEncoding]];
    }
    
    + (instancetype)printerWithMessageStdString:(const std::string &)message {
      return [[self alloc] initWithMessageStdString:message];
    }
    
    @end
    

    Принтер на C++, который использует "двойной impl". Printer::Impl только лишь имеет __strong PrinterImpl *impl_ сильную ссылку на Objective-C объект, жизненный цикл которого будет управляться ARC. т.е. PrinterImpl будет автоматически послан release деструктором Printer::Impl::~Impl.

    // Printer.hpp
    
    #pragma once
    #include <memory>
    #include <string>
    
    class Printer {
    public:
      Printer(const std::string& message);
      ~Printer();
      void Print() const;
    private:
      class Impl;
      std::string message_;
      std::unique_ptr<Impl> impl_;
    };
    
    // Printer.mm
    
    #import "Printer.hpp"
    #import "PrinterImpl+Cpp.h"
    
    class Printer::Impl {
    public:
      Impl(PrinterImpl* impl): impl_(impl) {}
      __strong PrinterImpl *impl_;
    };
    
    Printer::~Printer() = default;
    
    Printer::Printer(const std::string& message):
      message_(message),
      impl_(std::make_unique<Impl>([PrinterImpl printerWithMessageStdString:message]))
    {}
    
    void Printer::Print() const {
      [impl_->impl_ print];
    }
    

    Убедимся, что соседние файлы могут использовать Printer будучи обычным C++ кодом:

    // User.hpp
    
    #pragma once
    #include <string>
    
    void MyPrint(const std::string& message);
    
    // User.cpp
    
    #include "User.hpp"
    #include "Printer.hpp"
    
    void MyPrint(const std::string& message) {
      Printer(message).Print();
    }
    

    И, наконец, main.mm, который неспроста .mm, так как кто-то должен создать Autorelease Pool для Objective-C объектов, чтобы управление их памятью работало корректно. Без создания Autorelease Pool PrinterImpl не удалится. Чтобы убедиться в этом, я добавил -[PrinterImpl dealloc], который печатает сообщение при вызове.

    #include "User.hpp"
    
    int main(int argc, const char * argv[]) {
      @autoreleasepool {
        MyPrint("hello world");
      }
      return 0;
    }
    

    К сожалению, я не смог быстро найти как делать кат в комментариях, поэтому извиняюсь за разорванный монитор.


    1. Dimalovanyy Автор
      09.12.2022 20:40

      К вашему примеру и не придерешься, но все таки тут немного другая суть, из-за ARC получается что мы используем С++ в Objective-C, а у меня стояла задача именно Objective-C в С++. Что я конкретно имею ввиду: Если у меня сборка под macOS, я нахожу компилятор objective-C и линкую Objective-C обьектник, если же под Линукс (к примеру) то я даже не думаю про Objective-C, а просто забиваю на компиляцию данного обьектника и использую какой-то другой. В вашем же примере мне и под MacOS и под Linux, стоит иметь Objective-C, да и проект становится написанным на Objective-C а не на С++ по факту.
      Но ваш пример определенно красивый.


      1. storoj
        09.12.2022 20:45

        Я не очень понял. Ведь вне зависимости от того, хотим ли мы использовать С++ в Objective-C, или наоборот – оба сетапа называются "Objective-C++".

        В линуксе точно так же можно забить на Objective-C, просто опционально выкинув @autoreleasepool из main.m. Но весь остальной код ведь тот же самый, разве нет? Принтер на C++ такой же, его C++ версия impl практически та же самая, Objective-C часть тоже. Только управление памятью автоматическое.


        1. Dimalovanyy Автор
          09.12.2022 21:00

          Я имею ввиду что в системе которой нету Objective-C ваша программа не скомпилируется: ```gcc: fatal error: cannot execute ‘cc1obj```, а моя (если я ей скажу) чтобы вместо .mm для macOS, я использовал какой-то другой .cpp, для одного и того же хедера - скомпилируется.

          Только если использовать 2 разных main для Linux (main.cpp и другой принтер .cpp) для macOS (main.mm и принтер .mm)


      1. storoj
        09.12.2022 20:57

        Наверное, если хочется кроссплатформенности, то можно перенести Print() в C++-Impl, и тогда можно использовать разные реализации на разных платформах. А сам Printer останется неизменным. Но может я что-то не так понимаю в конечной цели.

        #include "Printer.hpp"
        
        #if (__APPLE__)
        #import "PrinterImpl+Cpp.h"
        class Printer::Impl {
        public:
          Impl(const std::string& message): impl_([PrinterImpl printerWithMessageStdString:message]) {}
          __strong PrinterImpl *impl_;
          
          void Print() {
            [impl_ print];
          }
        };
        #else
        #include <iostream>
        class Printer::Impl {
        public:
          Impl(const std::string& message): message_(message) {}
          std::string message_;
          
          void Print() {
            std::cout << message_ << "\n";
          }
        };
        #endif
        
        Printer::~Printer() = default;
        
        Printer::Printer(const std::string& message):
          message_(message),
          impl_(std::make_unique<Impl>(message))
        {}
        
        void Printer::Print() const {
          impl_->Print();
        }
        


      1. storoj
        09.12.2022 21:09

        Ещё обратил внимание на эту строку:

        К чему этот надуманный пример с принтером? Вместо NSString в последнем примере может быть любой объект из Apple SDK или с любой другой Objective-C библиотеки

        Если это "любая из системных" библиотек, то ладно. Но если захочется использовать какую-то стороннюю библиотеку, то как минимум её придётся с 99% вероятностью всё-таки собирать с ARC. Это не обязывает включать ARC и в Objective-C++ прослойке, но тем не менее.

        Ещё я опасался, что Objective-C код, который использует weak может не собраться в не-ARC, однако же я узнал о -fobjc-weak (CLANG_ENABLE_OBJC_WEAK).

        Compiles Objective-C code to enable weak references for code compiled with manual retain release (MRR) semantics.


  1. mapron
    10.12.2022 00:43

    Тут у меня явного ответа нету, скажу лишь что когда я хочу писать на С++ и использовать инструменты которые написаны на Objective-C последнее что я хочу видеть, это миграцию всего проекта на Objective-C++ и замена .cpp на .mm.

    Ничего не понятно.

    Вот есть проект, 1000 cpp файлов. на -x c++. добавляем 1001 файл, .mm или .cpp, не важно, -x objective-c++, либо весь проект собираем как objective-c++ и используем его фичи только в .mm не важно.

    В общем, получаем 1001-й файл в котором у нас Objective-C++. ну и используем для него плюсовый хедер и эппловое Objective-C апи. все отлично. платоформо специфичные штуки живут в отдельном файле.

    У нас в проекте используется чет около 40 фрейморков из Apple SDK, пока вообще не видел проблем с такой схемой.

    При этом сам код в mm файлах имеет базовую структуру С++ кода, с плюшками objective-c только там где без вызова сигналов прям уже ваще никак.
    Так получаем код который могут сопровождать разработчики которые знают C++, а objc не очень. А ваше решение, ну не знаю. Если вы 1 его и поддерживаете, то вроде как и норм


    1. Dimalovanyy Автор
      10.12.2022 01:15
      +1

      Опять таки, ваше решение не подходит для поставленной мною задачи: Мне нужно чтоб код который написаный на С++ мог использовать Objective-C в macOS среде для определенного хєдера, и c++ в Linux среде (Там где нету доступа к Objective-C и AppleSDK) для того же хєдера. Если я перепишу весь проект на .mm файлы он не соберется на линуксе.

      Так получаем код который могут сопровождать разработчики которые знают C++, а objc не очень

      Мне наоборот кажется что мое решение очень хорошо разграничивает C++ и Objective-C, так как:
      1) Objective-C имплементация полностью инкапсулирована PIMPL-ом
      2) Это код который может собирается отдельным обьектником (библиотекой если там много файлов)


      1. mapron
        10.12.2022 01:17
        +1

        код который написаный на С++ мог использовать Objective-C в macOS среде для определенного хєдера,

        Я что-то не понял, и как он не может использовать?

        ну так и мое предложение соответствует 1 и 2.

        разница только в том что инкапсулированный код так же пишется на С++ с objc вставками ,а не целиком файл на objc, вот и все. и Pimpl я ровно так же предлагаю


        1. Dimalovanyy Автор
          10.12.2022 01:37
          +1

          Давайте разберемся, может я вас не правильно понял. У меня есть хэдер файл который должен печатать строчку следующим образом:
          1) Для Linux должна быть реализация которая вызывает std::cout
          2)Для MacOS соответственно NSLog
          3)Реализация должна хранить в себе поле (std::string, NSString соответсвенно)

          Этот хедер вызывается из с++ кода (который должен работать и на Linux и на macOS)

          Ограничения:
          Linux абсолютно не может компилировать Objective-C/Objective-C++ код
          Никаких #ifdef APPLE, все решается на этапе сборки (Как в файле ниже, не важно Make, CMake)
          Makefile:

          UNAME := $(shell uname)
          ifeq ($(UNAME), Darwin)
            printer_source := printer.mm
          else
            printer_source := printer.cpp
          endif
          
          printer.o: $(printer_source)
            ...
          
          bin: ... printer.o
            ...

          Как ваш код решит такую проблему ? (как по мне достаточно классическая проблема для кросплатформенного кода который использует Apple SDK)


          1. mapron
            10.12.2022 01:41
            +1

            разница только в том что инкапсулированный код так же пишется на С++ с objc вставками ,а не целиком файл на objc, вот и все

            касаемо компиляции какой-то единицы в виде objc++ только под эппл я вообще в 1 комменте и написал. не про разделение файлов речь, а про использование С++ синтаксиса по максимуму. Зачем методы оформлять в этом одном mm файле у вас, вот что я в толк не возьму

            Ладно, не буду уже по три раз повторять одну и ту же мысль, не донёс да не донёс.


            1. Dimalovanyy Автор
              10.12.2022 01:45
              +1

              Я кажется вас понял :), это я для примера все в один mm засунул, в реальности есть еще хедер в котором интерфейс уже (Objective-C) класса, и .m файл с implementation


              1. mapron
                10.12.2022 01:52

                Допишите про это в статью :) зря наверное упростили. Из благих побуждений с водой выплеснули дитя, видите, я тупой, чего-то важного от вас не уловил.

                Теперь вопрос, а почему у вас реализация-то, m ,, а не mm? вы делаете эпл онли код. пимпл, все дела. хедер - C++ файл с методами.

                Зачем сами методы писать НЕ С++ кодом, вот в чем мой вопрос? я как разработчик кроссплатформенной кодовой базы, хочу как можно меньше видеть специфичного кода. платформенного кода. objective-c кода. поэтому в mm файле я пишу обычные плюсовые методы и фигачу вызовы objective-c объектов (да, сами объекты из NextStep и прочих API можно разместить в pimpl).


                1. Dimalovanyy Автор
                  10.12.2022 02:09

                  Это хороший вопрос, на него у меня ответа нету :)


                  Наверное у меня работает наоборот, я это все намудрил потому что хочу видеть Objective-C API исключительно в свойственной для него среде (Objective-C). И использовать Objective-C++ чисто как адаптер между Objective-C и C++.

                  Да и скорее всего когда человек отркывает .m файл он наверное ожидает видеть там Objective-C код а не Си с вызовами Objective-C API (Тоже самое и с Objective-C++ и С++).

                  Реальной кодовой базы с примерами подобных патеров у меня нету, по этому это исключительно мое собственное мнение и видиние


  1. pinbraerts
    10.12.2022 23:55

    // P.h
    
    #if defined __APPLE__
    @class Impl;
    #else
    class Impl;
    #endif
    
    class P {
      Impl* pimpl;
    
      void f();
    };
    
    // P.mm
    #include "ImplObjC.h"
    #import <Foundation/Foundation.h>
    
    
    void P::f() {
      NSString* str = pimpl->UseObjC();
    }
    
    // main.cpp
    #include "P.h"
    
    int main() {
      P p;
      p.f();
      return 0;
    }

    Можно просто использовать forward declaration