Objective-C Runtime для Си-шников. Часть 3

    image

    Всем привет. Сегодня я продолжу рассказывать вам о внутреннем устройстве Objective-C Runtime, а конкретно — о его реализации на уровне языка C.

    В прошлых статьях мы с вами подробно разобрались с селекторами и механизмом посылки сообщений объектам и классам. Сегодня я хотел бы закончить с сообщениями и рассказать о принципах работы некоторых встроенных возможностей языка Objective C.

    Тем, с кем мы ещё не знакомы, я предлагаю для начала прочитать первую и вторую части, а прочитавших и заинтересовавшихся прошу под кат.

    Ещё немного об objc_msgSend()


    Документация невозбранно лжёт нам, что существует целых четыре функции objc_msgSend():

    • objc_msgSend()
    • objc_msgSend_stret()
    • objc_msgSendSuper()
    • objc_msgSendSuper_stret()


    Переведу кусочек этой документации:

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


    На самом деле список этих функций намного шире, и их количество различается для каждой из платформ. Например, для i386 этот список выглядит так:

    .long	_objc_msgSend
    .long	_objc_msgSend_fpret
    .long	_objc_msgSend_stret
    .long	_objc_msgSendSuper
    .long	_objc_msgSendSuper_stret


    , а для arm64 всего лишь так:

    .quad   _objc_msgSend
    .quad   _objc_msgSendSuper
    .quad   _objc_msgSendSuper2


    Функции с суффиксом «stret» используются для тех методов, которые возвращают переменные сложного типа (структуры), в то время как функции без такого суффикса возвращают значения простых типов. В качестве примера можно привести следующий код:

    #import <Foundation/Foundation.h>
    #import <objc/runtime.h>
    
    struct TestStruct {
      long firstValue;
      long secondValue;
      long thirdValue;
    };
    
    @interface TestClass : NSObject
    @end
    
    @implementation TestClass
    + (struct TestStruct)someMethod {
      struct TestStruct * s = malloc(sizeof(struct TestStruct));
      return *s; // returns a whole struct
    }
    + (struct TestStruct *)anotherMethod {
      struct TestStruct * s = malloc(sizeof(struct TestStruct));
      return s; // returns just a pointer
    }
    @end
    
    int main(int argc, const char * argv[]) {
      // objc_msgSend_stret()
      struct TestStruct s = [TestClass someMethod];
      
      // objc_msgSend()
      struct TestStruct * ps = [TestClass anotherMethod];
      
      return 0;
    }


    Функции с суффиксом «Super» используются при вызове методов родительских классов, например:

    #import <Foundation/Foundation.h>
    #import <objc/runtime.h>
    
    @interface TestClass : NSObject
    @end
    
    @implementation TestClass
    - (NSString *)description {
      // objc_msgSendSuper2()
      return [super description];
    }
    @end
    
    int main(int argc, const char * argv[]) {
      TestClass * myObj = [[TestClass alloc] init];
      [myObj description];
      return 0;
    }


    И, наконец, функции с суффиксом «fpret» используются там, где нужно вернуть простой тип, но который не умещается в регистр процессора, как например «long double»:

    #import <Foundation/Foundation.h>
    #import <objc/runtime.h>
    
    @interface TestClass : NSObject
    @end
    
    @implementation TestClass
    + (long double)someMethod {
      return 0.0;
    }
    @end
    
    int main(int argc, const char * argv[]) {
      // objc_msgSend_fpret()
      [TestClass someMethod];
      return 0;
    }


    Конечно, всегда можно рассказать о механизме сообщений Objective C еще что-то, но мне хотелось бы закончить с ним именно сейчас. Поэтому, если у вас возникли какие-либо вопросы, вы всегда можете узнать ответы на них в исходных кодах Objective C Runtime. А теперь давайте двигаться дальше.

    Применяем полученные знания и делаем выводы



    Можно долго рассуждать на тему того, тратим мы с вами время впустую или действительно занимаемся делом. Однако, например, теперь реализация удалённого вызова процедур на Objective C для нас с вами является достаточно просто задачей. По крайней мере, согласитесь, вы сразу мысленно представили как это реализовать:

    1. Получаем по сети имя метода
    2. Берем селектор
    3. Вызываем нужную функцию
    4. PROFIT!!!!


    Однако, наше дело — разбираться дальше и не задавать лишних вопросов, кроме одного: а можно ли добавлять методы к классам прямо во время выполнения? Конечно можно!

    Те, кто уже имеет опыт разработки приложений на Objective C, сразу вспомнили такой механизм как категории. Категории позволяют расширять функциональность любых классов даже не имея их исходных файлов. Например, вот таким вот образом можно добавлять новые методы к классу NSObject:

    #import <Foundation/Foundation.h>
    
    @interface NSObject (APObjectMapping)
    + (NSMutableDictionary *)objectMapping;
    - (instancetype)initWithDictionary:(NSDictionary *)dictionary;
    - (NSDictionary *)mapToDictionary;
    @end


    Очевидно, что во время исполнения новые методы просто будут добавлены в dispatch table класса NSObject. А значит все остальные классы, наследуемые от NSObject, также получат добавленные нами методы. Красота да и только!

    Давайте попробуем сделать то же самое без использования категорий, исключительно средствами языка C. Для этого воспользуемся функцией class_addMethod:

    #import <Foundation/Foundation.h>
    #import <objc/runtime.h>
    
    void appleSecret(id self, SEL _cmd) {
      NSLog(@"Tim Cook is so gay...");
    }
    
    int main(int argc, const char * argv[]) {
      class_addMethod([NSObject class], @selector(appleSecret), (IMP)appleSecret, "v@:");
      NSObject * myObj = [[NSObject alloc] init];
      [myObj performSelector:@selector(appleSecret)];
      return 0;
    }


    С первыми тремя параметрами функции class_addMethod() все и так понятно, а вот четвертый — это спецификация аргументов нашей функции. В данном случае «v» означает, что тип возвращаемого значения — void, "@" — первый параметр функции типа «объект», и ":" — второй параметр функции типа «селектор». Например, если бы наша функция принимала еще один параметр типа int, то её спецификация выглядела бы так: «v@:i».

    Подводим итоги



    Механизм сообщений это сердце языка Objective C. Именно этот механизм предоставляет в языке уровня самого обычного Си все те прелести, к которым мы так привыкли в Java, C#, Python и т.д. Разобравшись с этим механизмом, мы стали понимать как работают те или иные возможности Objective C, такие как категории. Мы так же при желании можем понять каким образом устроены протоколы.

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

    Подробнее
    Реклама
    Комментарии 15
    • +1
      Спасибо вам за серию статей. С удовольствием читаю, хотя никогда не писал на obj c.

      «моего любимого языка»
      Не холивара ради, хочу спросить. Скажите, а это действительно правда? Ну, то есть вас не раздражает синтаксис а-ля «вырви глаза», а также, например, огромное количество скобочек и @? Особенно, когда вышел свифт.
      • +1
        Спасибо за отзыв. Да, я действительно люблю этот язык, как и C++ или Python. Синтаксис — согласен с Вами, уродливый. Но, как заметил один из комментаторов предыдущей статьи: «любовь зла, полюбишь и козла» :).
        • 0
          А я даже проникся синтаксисом и люблю в нём почти всё. Особенно, когда появились литералы @{}, @[] и прочие. Для примера, по мне следующий код гораздо читабельнее, чем его C*/Java аналог:
          // ObjC
          [obj setName:@"Foo" age:10];
          

          // C++
          obj.setNameAndAge(@"Foo",10);
          
          .

          А часто методы еще более сложные, и в скобках получается каша из параметров.
      • +1
        Спасибо, жду продолжения!
        Кстати, в примере с добавлением метода вместо
         [myObj performSelector:@selector(appleSecret)];

        можно просто написать
         [myObj appleSecret];

        будет warning, но все будет работать.
        Для тех, у кого нет OSX и желания/возможности возиться с виртуалками, рекомендую www.tutorialspoint.com/compile_objective-c_online.php — там хоть и линукс, но с gnustep, что ближе к реальной макоси чем просто gcc (который тоже умеет компилировать objc).

        Кстати, возникает вопрос — возможно ли прототипное ООП на objc, иными словами, можно ли добавить метод не в класс, а в объект; или сделать в рантайме копию объекта класса и использовать ее для некоторых объектов…
        • 0
          Нет, в Xcode 6 (Mac OS X Yosemite, LLVM) код,

          [myObj appleSecret];


          компилироваться не будет.

          По поводу ссылки на gnustep — я бы тоже поспорил, так как там реализация рантайма именно от GNU, а она имеет фундаментальные отличия от apple-овского рантайма.

          И большое спасибо за Ваш интерес к статьям, уже не первый раз замечаю Вас в комментариях :).
          • +1
            И все из-за ограничений ARC, хотя в этой одной строчке вся суть механизма сообщений — посылай что хочешь кому хочешь, это не вызов функции. Сейчас же с появлением ARC и swift исчезает все больше динамики, а swift так вообще просто еще один мультипарадигменный язык со своим синтаксическим сахаром.
            • 0
              Вы абсолютно правы! Проблема в том, что я занялся iOS-разработкой уже после появления ARC, и понял причину проблемы только после Вашего замечания. Спасибо! :)
              • 0
                А можно подробней, при чём тут ARC?
                • 0
                  Насколько я помню, тут все из-за возвращаемого значения. По-умолчанию для всех подобных вызовов возвращаемым типом является id и ARC не знает, что с ним делать. Для ARC эта информация обязательна.
                  Достаточно в проекте отключить ARC и подобный код будет без проблем компилироваться и максимум выдаст предупреждение о неизвестном селекторе. Этот код полностью валидный, но с приходом ARC он теперь является ошибкой.
                  • 0
                    Чем тогда эта ситуация отличается от [myObj performSelector:(@)selector(appleSecret)]?
                    • +3
                      Да ничем, по сути. Просто в первом случае ARC выдает ошибку, а во втором случае выдает только предупреждение — мол, надеюсь ты знаешь, что ты делаешь, потому что я не ручаюсь за возвращаемое значение. И это действительно так — с performSelector предупреждение и гласит, что может быть утечка памяти — ARC дает возможность сделать такое, но не ручается за управление памятью, потому что он без понятия о том, что делать с возвращаемым значением.

                      Такие уж костыли на пути отказа от динамики — какую-то обратную совместимость надо было оставить. До ARC это все было валидно и предупреждало только о неизвестном селекторе. С ARC всеми силами предупреждают, что код небезопасен и лучше так не делать. Из swift это вообще полностью выпилили как небезопасный код
                      The performSelector: method and related selector-invoking methods are not imported in Swift because they are inherently unsafe.

                      developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/BuildingCocoaApps/InteractingWithObjective-CAPIs.html
                      • 0
                        Понятно. Спасибо за разъяснения.

                        • +1
                          Спасибо за минусы, но можно поинтересоваться, за что? Я думаю всем будет полезно узнать как же все работает на самом деле, раз я написал что-то неправильно.
            • 0
              А что за параметры передадуться в appleSecret при вызове и что такое IMP?

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.