Сообщений 2    Оценка 315        Оценить  
Система Orphus

Проблемы взаимодействия кода на языках C/C++/Objective-C

Автор: Конева Анна Александровна
Опубликовано: 10.10.2013
Исправлено: 10.12.2016
Версия текста: 1.0
Введение
Ошибки компиляции
Проблема 1. Вызов конструктора C++-класса с параметрами из Objective-C кода
Проблема 2. Вызов Objective-C метода из метода C++-класса
Проблема 3. Шаблоны MAX/MIN при включении в Objective-C++/C++ код
Проблема 4. ARC запрещает объекты Objective-C типов в структурах
Проблема 5. Код с использованием блоков компилируется в Objective-C, но не компилируется в Objective-C++
Проблема 6. Невозможность присвоить лямбде блок в C++11
Проблема 7. Ошибки при присваивании блоков, возвращающих C++-объекты
Проблема 8. Ошибка при использовании некоторых C++-объектов в блоке
Проблема 9. Ошибки при передаче Objective-C-объектов в функции С при использовании ARC
Ошибки линковки
Проблема 1. Как вызвать функцию C++ из кода Objective-C
Проблема 2. Как вызвать inline C-функцию из Objective-C кода
Проблема 3. Странности линковки проекта под Mac OS X на Objective-C и статической C++ библиотеки
Предупреждения компилятора
Проблема 1. Вызов конструктора по умолчанию, определенного пользователем из кода Objective-C++
Заключение
Ссылки

Введение

Я думаю, многим известно, что при программировании под Mac OS/iOS возможно использование взаимодействия кода на языках C, Objective-C, С++, а также Objective-C++. В этой статье я хотела бы рассказать, какие проблемы могут возникнуть при этом. Все рассмотренные далее примеры проверялись в XCode 4.6.2 на компиляторах Apple LLVM 4.2 и LLVM GCC 4.2.

Ошибки компиляции

Проблема 1. Вызов конструктора C++-класса с параметрами из Objective-C кода

Описание

Допустим, у нас есть некий C++-класс, и у него наличествует конструктор с параметрами.

          class CppClass 
{
  public:
    CppClass(int arg1, const std::string& arg2): _arg1(arg1), _arg2(arg2) { }

    // ...  private:
    int _arg1; std::string _arg2;
};

И есть класс Objective-C, который содержит в себе объект предыдущего класса C++ в качестве члена.

          @interface ObjC: NSObject 
{
  CppClass _cppClass;
}
@end@implementation ObjC

- (id)init
{
    self = [super init];
    if ( self )
    {
      // Какой должен быть синтаксис вызова CppClass::CppClass(5, "hello") для       // _cppClass?
    }
    returnself;
}
@end

Как бы мы ни пытались его вызвать, будут ошибки компиляции.

Решение 1

Прежде всего, необходимо переименовать файл с исходным кодом реализации класса Objective-C из .m в .mm, чтобы можно было вызывать методы C++-объекта. В общем случае конструктор с параметрами просто для объекта вызвать нельзя. Возможен вызов только конструктора без параметров. Если уж очень нужно вызвать именно конструктор с параметрами, необходимо использовать вместо объекта C++ указатель на объект C++, и создавать его с помощью new в методе –init, а удалять – в методе –dealloc класса Objective-C соответственно с помощью delete:

          @interface ObjC: NSObject 
{
  CppClass *_pCppClass;
}

- (id)init
{
    self = [super init];
    if ( self )
    {
        // Какой синтаксис вызова CppClass::CppClass(5, "hello") для   //_cppClass?
        _pCppClass = new CppClass(5, "hello");
    }
    returnself;
}

- (void)dealloc
{
    delete _pCppClass;
    [superdealloc];
}

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

Решение 2

Можно изменить C++-класс следующим образом:

          template <int X, constchar Y[]>
class CPPClass
{
    int _x;
    constchar *_string ;
    
public:
    CPPClass();
};


template <int X, constchar Y[]>
CPPClass<X, Y>::CPPClass()
: _x(X)
, _string(Y)
{
}

Тогда класс Objective-C станет таким:

          extern
          const
          char kHelloWorld[] = "hello world";

@interface ObjC : NSObject
{
    CPPClass<5, kHelloWorld> thing;
}

Но здесь важно отметить, что шаблонный параметр int может быть литералом, а вот параметр const char[] должен быть объявлен переменной с внешней линковкой (как требует Clang). И, если мы, к примеру, уберем extern у строки, то получим ошибку компиляции при сборке:

«Non-template argument refers to object 'kHelloWorld' that does not have external linkage.»

В принципе, данные два решения работают и на Clang, и на GCC.

Проблема 2. Вызов Objective-C метода из метода C++-класса

Описание

Допустим, у нас есть класс на Objective-C:

          @interface ObjC : NSObject

- (int)someObjCMethod:(int)param;

@end@implementation ObjC

- (int)someObjCMethod:(int)param
{
    NSLog(@"param = %i", param);
    return param;
}

@end

И класс на C++:

          class CppClass
{
public:
    int someMethod(int param);
};

Файл CppClass.cpp:

          #import
          "ObjC.h"
          int CppClass::someMethod(int param)
{
    ObjC *objC = [[ObjC alloc]init];
    int ret = [objC someObjCMethod:param];
    [objC release];
    return ret;
}

Если мы так сделаем, то получим тьму ошибок компиляции вроде таких (от Clang):

«/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.8.sdk/System/Library/Frameworks/Foundation.framework/Headers/NSObjCRuntime.h:409:1: Expected unqualified-id
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.8.sdk/System/Library/Frameworks/Foundation.framework/Headers/NSObjCRuntime.h:411:19: Unknown type name 'NSString'
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.8.sdk/System/Library/Frameworks/Foundation.framework/Headers/NSObjCRuntime.h:417:50: Unknown type name 'Protocol'»

Но у этой проблемы есть решение.

Решение

Нужно переименовать файл реализации класс C++ из .cpp в .mm, что превратит класс из C++ в Objective-C++ и позволит делать как раз то, что нам нужно.

Файл CppClass.mm:

          #import
          "ObjC.h"
          int CppClass::someMethod(int param)
{
    ObjC *objC = [[ObjC alloc]init];
    int ret = [objC someObjCMethod:param];
    [objC release];
    return ret;
}

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

Проблема 3. Шаблоны MAX/MIN при включении в Objective-C++/C++ код

Описание

Представим, что у нас есть такой код в файле CppTemplate.h:

          template<class T>
inlineconst T &MAX(const T &a, const T &b)
{ return b > a ? (b) : (a); }

Файл ObjCppClass.h:

          #import <Foundation/Foundation.h>

@interface ObjCppClass : NSObject

@end

Файл ObjCppClass.mm:

          #import
          "ObjCppClass.h"
          #include
          "CppTemplate.h"
          @implementation ObjCppClass

@end

При попытке сборки будут выданы примерно такие ошибки компиляции (Clang):

«CppTemplate.h:17:17: Expected unqualified-id
CppTemplate.h:17:17: Expected ')'»

Причем, если включить файл CppTemplate.h в файл .cpp, то ошибки не будет.

Решение

Эта проблема возникает из-за того, что в Objective-C есть макросы с именами MAX/MIN, которые выглядят вот так (из файла NSObjCRuntime.h):

          #define MAX(x,y) ((x) > (y) ? (x) : (y))

Из-за этого ваш шаблон разворачивается вот в такое:

          template<class T>
inlineconst T& ((const T& a) > (const T& b) ? (const T&a) : (const
T&b)) (const T& a, const T& b) { ... }

что является совершенно бессмысленным и некомпилируемым.

Чтобы справиться с этой ошибкой, нужно либо дать шаблону другое имя, либо вставить перед шаблоном:

          #if defined(MAX)
#undef MAX
#endif

Проблема 4. ARC запрещает объекты Objective-C типов в структурах

Описание

Ниже представлена структура, содержащая в себе два объекта Objective-C класса NSString:

          typedef
          struct SStrings
{
    NSString* firstName;
    NSString* secondName;
} SStrings;

В проекте с включенным ARC (Automatic Reference Counting) при компиляции будет выдана ошибка «main.m:15:15: ARC forbids Objective-C objects in structs or unions», даже если установить флаг -fno-objc-arc в настройках проекта для данного конкретного файла.

Решение

Чтобы избежать ошибки, нужно поставить атрибут __unsafe_unretained перед объявлением объектов NSString в структуре:

          typedef
          struct SStrings
{
    __unsafe_unretained NSString* firstName;
    __unsafe_unretained NSString* secondName;
} SStrings;

Далее нужно объяснить, что же такое __unsafe_unretained.

По умолчанию все объекты в ARC имеют тип __strong. Это значит, что при присваивании переменной объекта его счетчик ссылок инкрементируется, и объект будет удержан столько, сколько переменная на него ссылается. Но это открывает возможности для циклических ссылок. Например, такое может произойти, когда объект содержит другой объект в качестве переменной класса, но этот второй объект также ссылается сильной ссылкой на первый, как делегат, поэтому оба объекта никогда не будут освобождены.

Для этих целей и существуют квалификаторы __unsafe_unretained и __weak. Их наиболее часто используют для делегатов. Это означает, что экземпляр делегата все так же будет указывать на первый объект, но этому объекту не будет увеличен счетчик ссылок, тем самым, разбивая цикл ссылок и позволяя обоим объектам освободиться.

Оба модификатора предотвращают удержание объектов, но немного по-разному. В случае __weak переменной будет присвоен nil после удаления объекта, что является очень безопасным поведением. Как и предполагает его имя, переменная с квалификатором __unsafe_unretained будет продолжать указывать на память, где находился объект, даже после его удаления. Это может привести к падению в связи с доступом к этому освобожденному объекту.

Зачем тогда вы можете захотеть использовать __unsafe_unretained? К сожалению, __weak поддерживается только начиная с iOS 5.0 и Lion в качестве платформы. Если вы хотите запускать приложение на iOS 4.0 и Snow Leopard, вы должны использовать квалификатор __unsafe_unretained.

А теперь допустим, что вы могли бы при использовании ARC написать такой код:

          typedef
          struct 
{
    __strong NSObject *obj;
    int ivar;
} MyStruct;

Потом вы могли бы написать код типа такого:

MyStruct *thing = malloc(sizeof(MyStruct));

Проблема в том, что malloc не обнуляет память, которую он возвращает. Поэтому thing->obj – это некоторое случайное значение, необязательно NULL. Затем вы присваиваете ему значение как-то так:

thing->obj = [[NSObject alloc] init];

На самом деле ARC превратит этот код в нечто подобное:

NSObject *temporary = [[NSObject alloc] init];
[thing->obj release];
thing->obj = temporary;

Проблема здесь в том, что ваша программа только что послала release какому-то случайному значению. Ваше приложение, скорее всего, рухнет в этой точке.

Вы могли бы сказать, что ARC должно распознавать вызов malloc и позаботиться об установке obj в NULL для предотвращения этого. Проблема в том, что malloc может быть обернут в какую-то другую функцию типа этой:

          void *myAllocate(size_t size) {
    void *p = malloc(size);
    if (!p) {
        // malloc failed.  Try to free up some memory.
        clearCaches();
        p = malloc(size);
    }
    return p;
}

Ok, теперь ARC должно знать и о вашей функции myAllocate тоже, а она может быть внутри некой статической библиотеки, которую вы получили в бинарном виде.

Ваше приложение может также использовать свои собственные аллокаторы памяти, которые повторно используют старые аллокации без использования free и malloc. Поэтому даже изменение malloc так, чтобы оно обнуляло память перед возвратом, не будет работать. ARC должно будет знать обо всех специальных аллокаторах в вашей программе.

Было бы очень, очень тяжело заставить такое работать надежно. Поэтому вместо этого создатели ARC просто сдались и сказали «Забудьте об этом. Мы не позволим вам помещать в структуры объекты со __strong и __weak».

Вот почему вы можете помещать в структуры только объекты с квалификатором __unsafe_unretained, чтобы сказать ARC «Не пытайся управлять владением объекта, на который эта переменная ссылается».

Справедливо только для Clang, так как GCC вообще не поддерживает ARC.

Проблема 5. Код с использованием блоков компилируется в Objective-C, но не компилируется в Objective-C++

Описание

Допустим, есть такой код в Objective-C:

          @interface TestClass1 : NSObject
- (void)test;
@end@implementation TestClass1

- (void)test
{
    void (^d_block)(void) =
    ^{
        int n;
    };
}

@end

Примерно такой же в C++:

          class TestClass2
{
public:
    void TestIt();
};

void TestClass2::TestIt()
{
    void (^d_block)(void) =
    ^{
        int n;
    };
}

И такой же в Objective-C++:

          class TestClass3
{
public:
    void TestIt();
};

void TestClass3::TestIt()
{
    void (^d_block)(void) =
    ^{
        int n;
    };
}

При выборе компилятора Clang все 3 варианта компилируются, но выдаются предупреждения «Unused variable 'n'» 3 раза соответственно. Хотя по идее в C++-варианте Clang должен был выдать сообщение об ошибке, что он ничего ни про какие блоки не знает вообще, так как это возможность Objective-C. Сначала я думала, что это какой-то баг, но потом оказалось, что все-таки это фича.

Компилятор GCC ругается и на вариант Objective-C++, и на вариант C++ примерно одинаково:

«TestClass2.cpp:15: 'int TestClass2::n' is not a static member of 'class TestClass2'»

«TestClass3.mm:15: 'int TestClass3::n' is not a static member of 'class TestClass3'».

В C++-коде GCC тоже должен был сказать, что про блоки не осведомлен. Это тоже наводит на мысль об ошибке в компиляторе.

Решение

Похоже, что это баги компилятора GCC. На эту тему есть заведенный баг: http://lists.apple.com/archives/xcode-users/2011/Mar/msg00232.html. Проблему можно обойти, сделав переменную статической:

          class TestClass3
{
    staticint n;
public:
    void TestIt();
};

void TestClass3::TestIt()
{
    void (^d_block)(void) =
    ^{
    };
}

Проблема 6. Невозможность присвоить лямбде блок в C++11

Описание

Компилятор Clang поддерживает C++11. С этим связан ряд интересных моментов.

Мы можем присваивать блоку лямбду:

          void (^block)() = []() -> void {
NSLog(@"Inside Lambda called as block 1!");
};
    
block();

Мы можем присвоить std::function блок:

std::function<void(void)> func = ^{
NSLog(@"Block inside std::function");
};
    
func();

Но мы не можем присвоить блок лямбде.

          auto lambda = []() -> void {
NSLog(@"Lambda!");
};
    
lambda = ^{ // error!
NSLog(@"Block!");
};
    
lambda();

Тогда мы получим ошибку компиляции «main.mm:40:12: No viable overloaded '='».

Решение

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

          auto lambda1 = []() { return 1; };
auto lambda2 = []() { return 1; };
lambda1 = lambda2; //Error

Возникает ошибка компиляции «main.mm:67:13: No viable overloaded '='».

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

          auto lambda = []() -> void { printf("Lambda 1!\n"); };
lambda = lambda;

Данный код тоже не компилируется с ошибкой «main.mm:63:12: Overload resolution selected implicitly-deleted copy assignment operator».

Просто лямбды - по реализации какие-то очень узкоспецифичные типы.

Но, что интересно, Visual Studio 2010 вполне себе компилирует самоприсваивание лямбды, но при этом IntelliSense выдает странное сообщение об ошибке:

«IntelliSense: function "lambda []void ()->void::operator=(const lambda []void ()->void &)" (declared at line 9) cannot be referenced -- it is a deleted function».

Все это оттого, что Visual Studio 2010 не поддерживает удаленные методы.

Проблема 7. Ошибки при присваивании блоков, возвращающих C++-объекты

Описание

Допустим, у нас есть некоторая простая иерархия классов Objective-C.

          @interface Fruit : NSObject
@end@interface Apple : Fruit
@end

Затем мы можем написать что-то вроде этого:

Fruit *(^getFruit)();
Apple *(^getApple)();
getFruit = getApple;

Это означает, что в отношении классов Objective-C блоки ковариантны в возвращаемом значении: блок, который возвращает что-то более специфическое, может быть рассмотрен как «подкласс» блока, возвращающего что-то более общее. Здесь блок getApple, который возвращает яблоко, может быть безопасно присвоен блоку getFruit. В самом деле, всегда безопасно принимать Apple *, когда вы ожидаете Fruit *. И, логично, что обратное не работает: getApple = getFruit; не компилируется, потому что когда мы действительно хотим яблоко, нам не очень нравится получить просто фрукт.

Также можно написать и такое:

          void (^eatFruit)(Fruit *);
void (^eatApple)(Apple *);
eatApple = eatFruit;

Это показывает, что блоки контрвариантны в типах аргументов: блок, который может обработать аргумент более общего типа, может быть использован там, где нужен блок, который обрабатывает более специфичный аргумент. Если блок знает, как съесть фрукт, он будет знать, как съесть и яблоко тоже. Опять же, обратное неверно и не будет компилироваться: eatFruit = eatApple;

Это все так в Objective-C. А теперь попробуем сделать что-то похожее в C++ и Objective-C++.

          class FruitCpp {};

class AppleCpp : public FruitCpp {};

И вдруг такие строки кода уже не будут компилироваться:

FruitCpp *(^getFruitCpp)();
AppleCpp *(^getAppleCpp)();
getFruitCpp = getAppleCpp; // error!void (^eatFruitCpp)(FruitCpp *);
void (^eatAppleCpp)(AppleCpp *);
eatAppleCpp = eatFruitCpp; // error!

Clang выдает ошибки:

«main.mm:28:17: Assigning to 'FruitCpp *(^)()' from incompatible type 'AppleCpp *(^)()'

main.mm:32:17: Assigning to 'void (^)(AppleCpp *)' from incompatible type 'void (^)(FruitCpp *)'»

GCC выдает похожие ошибки:

«main.mm:28: Cannot convert 'AppleCpp* (^)()' to 'FruitCpp* (^)()' in assignment

main.mm:32: Cannot convert 'void (^)(FruitCpp*)' to 'void (^)(AppleCpp*)' in assignment

main.mm:28:17: Assigning to 'FruitCpp *(^)()' from incompatible type 'AppleCpp *(^)()'

main.mm:32:17: Assigning to 'void (^)(AppleCpp *)' from incompatible type 'void (^)(FruitCpp *)'»

Решение

У этой проблемы решения нет и не предвидится. Хотя был даже заведен баг на эту тему на компилятор Clang: http://llvm.org/bugs/show_bug.cgi?id=15484. Но реализовано это, как я поняла, не будет. Это разграничение сделано намеренно, все дело в разнице между объектными моделями Objective-C и C++. Конкретнее: имея указатель на объект Objective-C, кто-то может сконвертировать его в указатель на объект базового класса или производного класса без фактического изменения значения указателя: адрес объекта остается тем же.

Так как C++ позволяет множественное и виртуальное наследование, это не для C++-объектов: если у меня есть указатель на класс C++, и я конвертирую его в указатель на объект базового класса или производного класса, то мне может потребоваться скорректировать значение указателя.

Рассмотрим пример:

          class A { int x; }
class B { int y; }
class C : public A, public B { }

B *getC() { 
  C *c = new C;
  return c;
}

Скажем, новый объект класса C в методе getC() был выделен по адресу 0x10. Значение указателя c равно 0x10. В выражении return этот указатель на C должен быть скорректирован так, чтобы он указывал на подобъект B в пределах C. Так как B идет вслед за A в списке наследования класса C, то (обычно) в памяти B будет следовать за A, что означает прибавку смещения в 4 байта (== sizeof(A)) к указателю, поэтому возвращаемый указатель будет 0x14. Аналогично, приведение B* к C* привело бы к вычитанию 4 байт из указателя. Когда мы имеем дело с виртуальными базовыми классами, то идея та же, но смещения уже неизвестны.

Теперь, рассмотрим, какой эффект все это производит на такое присваивание:

C* (^getC)();
B* (^getB)();
getB = getC;

Блок getC возвращает указатель на C. Чтобы превратить его в блок, возвращающий указатель на B, нам надо было бы корректировать указатель, возвращаемый в каждом вызове блока, прибавляя 4 байта. Это не корректировка для блока, это корректировка значения указателя, возвращаемого блоком. Можно было бы реализовать это, создав новый блок, который оборачивает предыдущий блок и выполняет корректировку, например:

getB = B* (^thunk)() { return getC(); }

Это реализуемо для компилятора, который уже предоставляет похожие «трюки» при переопределении виртуальной функции той, которая возвращает ковариантный возвращаемый тип, которому нужна корректировка. Однако в случае блоков это вызывает дополнительные проблемы: для блоков разрешена операция сравнения на равенство ==, поэтому, чтобы определить, "getB == getC", мы должны были бы иметь возможность взглянуть через эту обертку, которая бы генерировалась присваиванием "getB = getC", чтобы сравнить нижележащие указатели, возвращаемые блоками. Опять же, это реализуемо, но потребовало бы гораздо более тяжеловесного рантайма для блоков, способного создавать уникальные обертки, которые могут выполнять эти корректировки возвращаемого значения (так же, как и для любых контрвариантых параметров). В то время, как все это технически возможно, цена (в размере рантайма, сложности и времени выполнения) перевешивает выигрыш.

Возвращаясь назад к Objective-C, замечу, что модель одиночного наследования никогда не потребует никаких корректировок к указателю на объект: есть только один адрес в данном Objective-C объекте, и тип статического указателя не важен. Поэтому ковариантность/контрвариантность никогда не потребует каких-то оберток, и присваивание блока – это просто присваивание указателей (+ _Block_copy/_Block_release при использовании ARC).

Проблема 8. Ошибка при использовании некоторых C++-объектов в блоке

Описание

Ниже представлен код, где мы пытаемся использовать объявленную вне блока переменную типа std::ifstream внутри блока.

          __block std::ifstream file("/tmp/bar") ;
// tried this with and without the __blockvoid (^block)() = ^
{
  file.rdbuf();
  file.close();
  file.open("/tmp/bar");
};
block() ;

Если мы объявляем эту переменную с квалификатором __block, то получим следующее сообщение об ошибке (Clang):

«main.mm:28:27: Call to implicitly-deleted copy constructor of 'std::ifstream' (aka 'basic_ifstream<char>')»

Если без этого модификатора, то другие:

«main.mm:31:9: Call to implicitly-deleted copy constructor of 'const std::ifstream' (aka 'const basic_ifstream<char>')
main.mm:32:9: Member function 'close' not viable: 'this' argument has type 'const std::ifstream' (aka 'const basic_ifstream<char>'), but function is not marked const»

GCC выдаст похожие сообщения об ошибках, но, правда, при использовании __block у GCC вообще сначала случилась внутренняя ошибка компиляции («Internal compiler error: Segmentation fault: 11»).

Решение

Если дело касается локальных переменных обычных типов Objective-C, то по умолчанию внутрь блока они передаются по значению и изменяться не могут. При попытке изменения такой переменной внутри блока будет выдана ошибка. Если перед такой переменной поставить __block, то она будет передана по ссылке и внутри блока уже может менять свое значение. На C++ объекты __block действует по-другому.

Если мы используем __block с C++-объектами, то компилятор пытается вызвать copy constructor для этого объекта, который является удаленным (=delete) в классе std::ifstream. Соответственно, от этого и первая ошибка.

Если мы не используем __block, то должен быть вызван const copy constructor, который также является удаленным для этого класса (отсюда вторая ошибка). Третье же сообщение об ошибке вызвано тем, что мы пытаемся вызвать неконстантный метод close на константном объекте std::ifstream.

Решением здесь может быть использование объекта через указатель. Справедливо и для Clang, и для GCC.

Проблема 9. Ошибки при передаче Objective-C-объектов в функции С при использовании ARC

Описание

Допустим, что мы хотим передать какой-то объект Objective-C типа в функцию C в качестве параметра с типом void*, например, в pthread_create.

pthread_t thread;
NSObject *obj = [[NSObject alloc]init];
pthread_create(&thread, NULL, startThread, obj);

При включенном ARC мы получим ошибку компиляции

«main.m:36:48: Implicit conversion of Objective-C pointer type 'NSObject *' to C pointer type 'void *' requires a bridged cast»

ARC не даст так просто сделать подобное преобразование.

Решение

Вам нужно использовать bridge-преобразование для перемещения между двумя моделями памяти. Самая простая форма этого такая (ее же и предлагает сам XCode, если навестись на ошибку в коде):

pthread_create(&thread, NULL, startThread, (__bridge  void*)obj);

Это эквивалентно исходному коду, но теперь, так как мы в режиме ARC, есть дополнительное предостережение. Дело в том, что оптимизатор ARC использует локальную балансировку, и будет считать, что может освободить obj, если это последняя ссылка на него. К сожалению, pthread_create ничего не знает об объектах, поэтому не будет вызывать retain для аргумента. К тому времени, как поток будет запущен, объект может быть уже удален. Правильное решение следующее:

pthread_create(&thread, NULL, startThread, (__bridge_retained  void*)obj);

Это вызовет objc_retain() перед передачей аргумента.

С другой стороны, функция потока должна выглядеть примерно так:

          void* startThread(void *arg)
{
    id anObject = (__bridge_transfer id)arg;
    arg = NULL;
    //...
}

Это поместит объект назад под контроль ARC. Эта форма bridge-преобразования рассчитывает, что объекту уже был вызван retain, и поэтому освободит его (вызовет release) в конце блока. Строчка arg = NULL необязательна, но является хорошим стилем. Вы поместили ссылку, которую содержал arg, под контроль ARC, поэтому обнуление указателя является прозрачным.

ПРИМЕЧАНИЕ

Вот что говорит документация по Automatic Reference Counting Clang:

«A bridged cast is a C-style cast annotated with one of three keywords:

• (__bridge T) op casts the operand to the destination type T. If T is a retainable object pointer type, then op must have a non-retainable pointer type. If T is a non-retainable pointer type, then op must have a retainable object pointer type. Otherwise the cast is ill-formed. There is no transfer of ownership, and ARC inserts no retain operations.

• (__bridge_retained T) op casts the operand, which must have retainable object pointer type, to the destination type, which must be a non-retainable pointer type. ARC retains the value, subject to the usual optimizations on local values, and the recipient is responsible for balancing that +1.

• (__bridge_transfer T) op casts the operand, which must have non-retainable pointer type, to the destination type, which must be a retainable object pointer type. ARC will release the value at the end of the enclosing full-expression, subject to the usual optimizations on local values.

These casts are required in order to transfer objects in and out of ARC control; see the rationale in the section on conversion of retainable object pointers.

Using a __bridge_retained or __bridge_transfer cast purely to convince ARC to emit an unbalanced retain or release, respectively, is poor form.»

Справедливо только для Clang.

Ошибки линковки

Проблема 1. Как вызвать функцию C++ из кода Objective-C

Описание

Допустим, есть функция на C++, которую нам нужно вызвать из кода Objective-C.

Файл File.h:

          int DoSomethingCppFunction(int param);

Файл File.cpp:

          int DoSomethingCppFunction(int param)
{
    cout << "param = " << param << endl;
    return param;
}

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

Решение

Эта ошибка возникает из-за искажения имен функций C++. Чтобы его отключить, нужно добавить к объявлению C++-функции (причем, достаточно, только к объявлению) обозначение extern "C":

          #ifdef __cplusplus
extern"C" 
{
#endifint DoSomethingCppFunction(int param);
    
    
#ifdef __cplusplus
}
#endif

Проблема решена. Верно для обоих компиляторов.

Проблема 2. Как вызвать inline C-функцию из Objective-C кода

Описание

Допустим, у нас есть такой код в файле main.m:

          #import <Foundation/Foundation.h>

inlinevoid foo()
{
    printf("Hello, World\n");
}

int main(int argc, constchar * argv[])
{
    foo();
        
    // insert code here...
    NSLog(@"Hello, World!");
        
    return 0;
}

При попытке сборки мы получим ошибку линковки, в которой сказано, что метод foo не найден.

Решение

Это новая особенность диалекта C99/GNU99, который стоит в настройках проекта XCode по умолчанию. Если у inline-функции нет никакого спецификатора хранения, то для нее не будет сгенерировано тела. Соответственно, мы получим ошибку линковки.

Чтобы устранить ее, в данном случае необходимо добавить модификатор static для функции foo. Также можно поменять в настройках используемый диалект C на GNU89.

Эта проблема может проявиться и по-другому.

Описание

Допустим, у нас есть файлы Foo.h и Foo.c.

Файл Foo.h:

          inline
          void foo();

Файл Foo.c:

          inline
          void foo()
{
    printf("Hello, World\n");
}

А файл main.m выглядит следующим образом:

          #import <Foundation/Foundation.h>
#include"Foo.h"int main(int argc, constchar * argv[])
{

  foo();

  // insert code here...
  NSLog(@"Hello, World!");
  
  return 0;
}

Тогда мы снова получим ту же ошибку линковки.

C99 предлагает такой подход: он требует от программиста наличия модуля, содержащего вызываемую копию extern inline-функции. По умолчанию в C99 inline-функции без какого-либо спецификатора хранения считаются extern. Если все объявления inline-функции в модуле не имеют спецификатора хранения, тогда функция считается extern inline, и для нее не будет сгенерировано тело. С другой стороны, если одно из объявлений inline-функции в модуле явно содержит ключевое слово extern, то именно этот модуль сгенерирует вызываемую копию функции. Это приводит к следующей организации исходных файлов.

Решение

Вставьте определение extern inline-функции в заголовочный файл, не используя ключевое слово extern. Этот заголовок может быть включен во столько модулей, во сколько вы хотите. В один-единственный модуль вы должны включить этот заголовок и объявить прототип функции с использованием extern, чтобы получить вызываемые копии функции (в этом модуле необязательно повторять ключевое слово inline).

Пример изменится следующим образом.

Файл Foo.h:

          inline
          void foo()
{
    printf("Hello, World\n");
}

Файл Foo.c:

          extern
          inline
          void foo();

Файл main.m остается таким же.

И ошибка будет устранена. Еще одно решение проблемы – можно без изменения исходников переключить диалект C в настройках среды на GNU 89. Справедливо для обоих компиляторов.

Проблема 3. Странности линковки проекта под Mac OS X на Objective-C и статической C++ библиотеки

Описание

Представим, что у нас есть статическая C++ библиотека, в которой есть класс:

Файл Test.h:

          class Test {
public:
  Test();
  ~Test();
  int addTwoNums(int a, int b);
};

Файл Test.cpp:

Test::Test(){}
Test::~Test(){}

int Test::addTwoNums(int a, int b)
{
  return (a + b);
}

И есть простое консольное приложение на Objective-C, в котором мы пытаемся использовать эту библиотеку.

Компилятор один и тот же — дефолтный Apple LLVM 4.2. Но вот есть разница в результате при следующих настройках использования библиотеки C++. Далее представлены различные комбинации использования приложения, библиотеки и рантайма C++ в них.

Случай 1.

приложение — libc++

библиотека — libc++

Результаты: ошибок линковки нет.

Случай 2.

приложение — libstdc++

библиотека — libc++

Результаты: ошибок линковки нет.

Случай 3.

приложение — libc++

библиотека — libstdc++

Результаты:

ошибки линковки

«Undefined symbols for architecture x86_64:

"std::ios_base::Init::Init()", referenced from:

___cxx_global_var_init in lib01SampleCppLibrary.a(Test.o)

"std::ios_base::Init::~Init()", referenced from:

___cxx_global_var_init in lib01SampleCppLibrary.a(Test.o)

ld: symbol(s) not found for architecture x86_64

clang: error: linker command failed with exit code 1 (use -v to see invocation)»

Случай 4.

приложение — libstdc++

библиотека — libstdc++

Результаты: ошибок линковки нет.

Решение

Библиотека libstdc++ является частью GCC. Старая, т.к. GCC на Mac OS X давно не поддерживатся Apple. Хотя можно поставить его из портов, тогда данная библиотека вполне может быть новой.

libc++ – часть Clang. Новая, поддерживается Apple.

Если проект использует C++11, то вы должны линковаться с libc++, если нет, то можно линковаться либо с libstdc++, либо с libc++.

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

Предупреждения компилятора

Проблема 1. Вызов конструктора по умолчанию, определенного пользователем из кода Objective-C++

Описание

Начиная с версии Mac OS X 10.4 и GCC 4, стала доступна возможность включать объекты C++-классов в качестве переменных экземпляра в классы Objective-C при условии, что объект должен создаваться конструктором без параметров.

Допустим, у нас есть класс Objective-C и класс C++:

Файл ObjCClass.h:

          #include
          "CppClass.h"
          @interface ObjCClass : NSObject
{
    CppClass cppClass;
}

- (void)SomeObjCMethod;
@end

Файл ObjCClass.mm:

          #import
          "ObjCClass.h"
          @implementation ObjCClass

- (void)SomeObjCMethod
{
    cppClass.someMethod(43);
}
@end

Файл CppClass.h:

          class CppClass
{
public:
    CppClass();
    int someMethod(int param);
};

Файл CppClass.cpp:

          #include
          "CppClass.h"
          #include
          <iostream>
          using
          namespace std;

CppClass::CppClass()
{
    _var = 26;
}

int CppClass::someMethod(int param)
{
    cout << "param = " << param << endl << "_var = " << _var << endl;
    return param;
}

При смене компилятора на «LLVM GCC» с «Apple LLVM compiler» (выбран по умолчанию после создания проекта) в настройках проекта появляется галочка (которая отсутствует для компилятора Clang) «Call C++ Default Ctors/Dtors in Objective-C» (GCC_OBJC_CALL_CXX_CDTORS, -fobjc-call-cxx-cdtors).

Случай 1. Галочка сброшена.

Результаты: предупреждения компилятора

«Type 'CppClass' has a user-defined constructor»

«C++ constructors and destructors will not be invoked for Objective-C fields»

Последовательность вызовов (в порядке сверху вниз):

— [ObjCClass init]

— [ObjCClass SomeObjCMethod]

CppClass::someMethod(int)

Что интересно, программа успешно выполняется и не падает.

При этом наш конструктор (CppClass::CppClass) действительно не вызывается, но есть такое впечатление, что объект все-таки создается. Я так понимаю, что вызывается конструктор, сгенерированный самим компилятором, а наш игнорируется. При этом переменная _var получает, похоже, значение 0, хоть в релизе, хоть в дебаге, хотя, насколько я помню, в C++ переменные класса не инициализируются значением по умолчанию (в отличие от C#). Пока четких выводов, что так будет всегда, из этого делать не берусь.

Случай 2. Если галочка поставлена, то наш конструктор будет успешно вызван.

Результаты:

предупреждений компилятора нет

Последовательность вызовов:

_objc_rootAllocWithZone

object_cxxConstructFromClass(objc_object*, objc_class*)

— [ObjCClass .cxx_construct]

CppClass::CppClass()

— [ObjCClass init]

— [ObjCClass SomeObjCMethod]

CppClass::someMethod(int)

ПРИМЕЧАНИЕ

Вот что говорит документация Apple про этот флаг компилятора GCC:

«-fobjc-call-cxx-cdtors

For each Objective-C class, check if any of its instance variables is a C++ object with a non-trivial default constructor. If so, synthesize a special - (id) .cxx_construct instance method that will run non-trivial default constructors on any such instance variables, in order, and then return self. Similarly, check if any instance variable is a C++ object with a non-trivial destructor, and if so, synthesize a special - (void) .cxx_destruct method that will run all such default destructors, in reverse order.

The - (id) .cxx_construct and/or - (void) .cxx_destruct methods thusly generated will only operate on instance variables declared in the current Objective-C class, and not those inherited from superclasses. It is the responsibility of the Objective-C runtime to invoke all such methods in an object's inheritance hierarchy. The - (id) .cxx_construct methods will be invoked by the runtime immediately after a new object instance is allocated; the - (void) .cxx_destruct methods will be invoked immediately before the runtime deallocates an object instance.

As of this writing, only the NeXT runtime on OS X v10.4 and later has support for invoking the - (id) .cxx_construct and - (void) .cxx_destruct methods.»

Решение

Можно включить эту галочку в настройках или перейти на другой компилятор. При смене компилятора на Clang обнаруживается интересная особенность (у него такой специальной настройки нет). Но, похоже, что он тоже умеет обрабатывать подобные случаи и вызывать конструктор для объекта класса C++. Рассмотрим разные случаи запуска.

Случай 1. Конструктор (CppClass::CppClass) не закомментирован.

Результаты:

предупреждений компилятора нет.

НО! CppClass::CppClass() как будто бы вызывается почему-то два раза подряд (в отличие от GCC, где вызов происходит один раз)! И сначала я была невнимательна и действительно так подумала и даже завела баг на эту тему на Clang http://llvm.org/bugs/show_bug.cgi?id=16198. Но, поставив оператор вывода строки внутрь конструктора, я убедилась, что на самом деле конструктор реально вызывается один раз, но баг в том, что если поставить точку останова в конструкторе, то отладчик lldb останавливается там два раза и в стеке вызовов тоже видно два одинаковых вызова. Так что баг есть, но несколько другой.

Ниже приведена основная последовательность вызовов в порядке сверху вниз.

_objc_rootAllocWithZone

object_cxxConstructFromClass(objc_object*, objc_class*)

— [ObjCClass .cxx_construct]

CppClass::CppClass()

CppClass::CppClass()

— [ObjCClass init]

— [ObjCClass SomeObjCMethod]

CppClass::someMethod(int)

Случай 2. Конструктор (CppClass::CppClass) закомментирован.

Результаты:

предупреждений компилятора нет

Последовательность вызовов:

— [ObjCClass init]

[ObjCClass SomeObjCMethod]

CppClass::someMethod(int)

При этом переменная _var также получает, похоже, значение 0, хоть в релизе, хоть в дебаге, как и в случае компилятора GCC.

Как видим, в случае наличия определенного нами конструктора CppClass::CppClass тоже вызывается некая функция – [ObjCClass .cxx_construct], как и при использовании компилятора GCC. Объяснения тому, почему конструктор вызывается как бы два раза подряд, пока мной не найдено.

Заключение

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

Ссылки


Эта статья опубликована в журнале RSDN Magazine ##4-2009. Информацию о журнале можно найти здесь
    Сообщений 2    Оценка 315        Оценить