Дмитрий Викторов

Некоторое время назад все iOS-устройства имели 32-битную архитектуру. У них были 32-разрядные процессоры armv6, armv7 или armv7s. После того, как Apple начала встраивать в свои устройства 64-битные процессоры armv8. В настоящее время инструкции процессора armv8 есть у iPhone 5S, iPhone 6, iPhone 6 Plus, iPad Air, iPad mini с дисплеем Retina, iPad Air 2 и iPad mini 3. Эти устройства сейчас самые популярные и Apple объявила, что разработчики должны включить 64-битную поддержку архитектуры в своих двоичных файлах. В противном случае бинарные файлы будут отклонены проверкой Apple.

В настоящее время все более популярными становятся новые современные языки программирования, такие как swift, C# и Java. У них почти не возникает проблем с переходом из 32-битного режима в 64-битный, потому что они ограничены в сыром доступе к памяти и имеют более строгие стандарты по размерам базовых типов данных. Но у многих разработчиков есть много устаревшего кода на C, Objective C и C++, который был разработан и хорошо протестирован для 32-битных процессоров и опирался на фиксированный размер целочисленных типов данных. Языки на основе C не так строги в размерах интегральных типов данных. Поэтому перенос этого кода на новую архитектуру может стать действительно большой головной болью. Но я хочу показать вам, что это не очень сложно, это рутинная работа. В качестве примера могу сказать, что я успешно портировал на cocos2d 2.1.0 игру JELLIES! для работы с armv8. Хочу поделиться своим опытом и рассказать о возможных багах.

Все проблемы обычно связаны с размерами базовых типов данных, которые изменились для 64-х архитектур. У нас есть общая модель данных ILP32 для Windows, iOS, Mac OS X и других Unix-подобных ОС. Но после перехода на 64-бит у нас будут другие модели данных. Глядя на таблицу на вики-странице https://en.wikipedia.org/wiki/64-bit_computing#64-bit_data_models можно заметить, что Windows и Unix-подобные ОС различаются по размеру long int, потому что в Windows LLP64 модель данных, но Unix имеет модель данных LP64.

Итак, мы также можем заметить, что для 64-битных процессов в Windows изменился только размер указателей по сравнению с 32-битными процессами. Вот страница MSDN с описанием основных типов данных https://msdn.microsoft.com/en-us/library/cc953fe1.aspx

Другая ситуация с Mac OS X, iOS и другими Unix. Здесь у нас есть изменения для указателей и размера long int. Также платформы Foundation и Core Graphics определяют собственные типы данных для целых чисел и чисел с плавающей запятой, которые также имеют разные размеры в 64- и 32-битных архитектурах. Такими типами данных являются NSInteger, NSUInteger и CGFloat. NSInteger — это то же самое, что и long int, а NSUInteger — это то же самое, что unsigned long int. CGFloat имеет тип float в 32-битном режиме и double в 64-битном режиме. Кстати, я хотел бы предложить включить tgmath.h для математических функций для работы со значениями CGFloat. Этот заголовок имеет макросы и шаблонные функции, которые будут выбирать правильную математическую функцию в зависимости от того, что CGFloat теперь плавает или удваивается. Вот страница с портала разработчиков Apple, которая объясняет изменения и возможные проблемы https://developer.apple.com/library/ios/documentation/General/Conceptual/CocoaTouch64BitGuide/Major64-BitChanges/Major64-BitChanges.html

Такие обстоятельства могут привести к неожиданному поведению после попытки запуска старого 32-битного кода в 64-битном режиме. Некоторые проблемы могут быть обнаружены после компиляции как ошибки. Другие могут быть найдены как предупреждения. Поэтому рекомендую поставить компилятор в режим самых строгих предупреждений и исправить все предупреждения. Давайте компилятор поможет нам предотвратить некоторые ошибки. Плохо, если какая-то ошибка будет скрыта при тестировании и неожиданно появится в рабочем коде, потому что какое-то предупреждение было проигнорировано, но может быть найдено компилятором автоматически. Итак, после установки самых строгих настроек предупреждений необходимо просмотреть предупреждения и хорошо бы их проанализировать и сделать явные приведения. Также было бы неплохо просмотреть строковые выражения формата. Хорошо, что Xcode предлагает, какие изменения нужно исправить. Но некоторые ошибки могут быть скрыты от глаз компилятора. Чтобы найти некоторые из них, по-прежнему требуются тщательные тесты.

Что ж, когда мы узнали, что Apple будет принимать релизы только с поддержкой 64-битного режима, наша команда решила добавить архитектуру arm64 для JELLIES! Он будет включен в следующее обновление, которое будет выпущено в ближайшее время. Но наша версия cocos2d не была к этому готова и требовала некоторых изменений. На тот момент был cocos2dv3. Но он был полностью переписан и не совместим со 2-й версией. Также мы используем CocosBuilder, который тоже основан на второй версии cocos2d. В нашей игре очень много (очень огромное!) количество сцен в CocosBuilder и в этих сценах много анимации. Мы написали для него код кэширования сцен и немного настроили программу чтения сцен, чтобы оптимизировать использование памяти. Итак, наш код далек от официальных cocos2dv3 и SpriteBuilder (аналог CocosBuilder). Мы обнаружили, что переход на SpriteBuilder означает переписать игру с нуля, это было бы действительно большой головной болью.

Больше всего проблем было с кодом библиотеки cocos2d. У него уже было много предупреждений до перехода на 64-битную версию, но после добавления архитектуры arm64 в проект Xcode я получил от него огромное количество предупреждений. Мы попробовали запустить игру сразу после исправления ошибок компиляции и сразу же обнаружили визуальные баги. После некоторого тестирования мы также обнаружили некоторые логические ошибки. Я был очень разочарован, глядя на окно предупреждений. Но как говорят в России: «Глаза боятся, а руки делают» (кстати, полезно знать английский вариант этой идиомы). Итак, я начал исправлять их шаг за шагом. В результате я потратил один день на исправление предупреждений. Но это была отличная работа, теперь в нашем коде вообще нет предупреждений в самом строгом режиме предупреждений. Мне это очень нравится. Но, несмотря на то, что все предупреждения были исправлены, мы все же нашли одну ошибку, которую не заметили глаза компилятора. Эта ошибка была связана с исчезновением белого клинка в режиме берсерка. Это описано далее в статье в разделе про баги в CCBlade.

Теперь расскажу о самых интересных багах

Некоторые ошибки компиляции

Были ошибки компиляции в mat4.c и neon_matrix_impl.c из библиотеки kazmath. Необходимо заменить #ifdefined(__ARM_NEON__) на #ifdefined(_ARM_ARCH_7)

Пример задачи с целыми числами

Допустим, у нас есть следующий код, который вычисляет случайный угол в диапазоне [-45..45] градусов

В 32-битном режиме все будет нормально. NSInteger будет int32, arc4random_uniform вернет uint32. Компилятор также будет рассматривать 45 как uint32. Итак, если arc4random_uniform сгенерирует что-то меньше 45, допустим 25, то результатом будет uint32 25-45=4294967276. После того, как 4294967276 будет приведено к переменной угла int32, а угол получит -20, как и ожидалось, потому что размеры uint32 и int32 одинаковы.

Но что будет в 64-битном режиме? NSInteger будет int64, arc4random_uniform по-прежнему будет возвращать uint32, поэтому результат вычитания также будет 4294967276. После этого он будет приведен к int64, который имеет больший размер. 4294967276 хорошо вписывается в положительный диапазон int64, поэтому значение угла будет равно 4294967276! Это совершенно неожиданный ракурс.

Решение здесь состоит в том, чтобы привести результат arc4random_uniform к NSInteger, например, перед вычитанием. Такие ошибки можно исправить, просматривая предупреждения компилятора в строгом режиме, как я уже говорил.

Ротация в CCBAnimationManager.m (из CocosBuilder) и CCActionInterval.m

Это была странная ошибка со свойством «поворот» некоторых целей NSObject. Цель может быть любого типа и ожидать какой-то CCNode в cocos2d. В 64-битном режиме свойство вращения CCNode не вызывалось. Я не знаю почему. Итак, мне нужно было явно приводить цели к CCNode.

В файле CCActionInterval.m селектор -[CCRotateBy update:]

я изменился на

То же самое и с CCBAnimationManager.m

Был изменен на

Я также добавил NSAssert, чтобы проверить, является ли цель CCNode, чтобы убедиться, что мы не работаем с другими интерфейсами.

Создание CCTexture2D для CCLabelTTF

Были проблемы с устаревшим -[NSString drawInRect:withFont:lineBreakMode:alignment:], который используется для рендеринга CCLabelTTF в CCTexture2D. Кажется, есть ошибка с этим селектором на архитектуре arm64 внутри фреймворка Apple, потому что этот селектор отображает только первую строку многострочной строки. Я проанализировал память, выдаваемую этим селектором на обеих архитектурах с одинаковыми условиями, и обнаружил, что проблема проявляется только в 64-битном режиме. Итак, я решил переписать код и использовать вместо него -[NSAttributedString drawInRect:]. Также я избавился от устаревшего -[NSString sizeWithFont:constrainedToSize:lineBreakMode:] и заменил его на -[NSAttributedStringboundingRectWithSize:options:context:].size. Я покажу только идею, потому что весь код довольно большой.

CCBlade

На самом деле, CCBlade не является частью cocos2d, но я думаю, что с его помощью полезно делать эффекты Fruit Ninja. Мы использовали его для режима берсерка. Его можно скачать с https://github.com/hiepnd/CCBlade. Проблема в следующем месте:

Массивы вершин и координат здесь являются буферами CGPoint. Функция glVertexAttribPointer принимает const GLvoid* в качестве последнего аргумента. Вот почему компилятор ничего не говорит, потому что void* подходит для любого типа. Но на самом деле OpenGL ожидает буфер структур с 2 полями с плавающей запятой. И, как мы знаем, CGPoint имеет 2 поля CGFloat, которые плавают в 32-битном режиме и удваиваются в 64-битном режиме. Итак, после перехода на 64-битную систему мы получили неожиданное поведение. Я переписал этот код, чтобы использовать массивы структур ccVertex2F.

В итоге могу сказать, что переход на 64-битную архитектуру дело рутинное, но необходимое, потому что 64-битная архитектура сейчас становится наиболее значимой. Большинство 64-битных ОС позволяют запускать 32-битный код в режиме совместимости, но это может стать устаревшим. Современный код на языках на основе C должен быть написан с учетом кросс-архитектуры, чтобы свести к минимуму потенциальные проблемы и сократить время переноса.

PS: я не публикую весь код cocos2d, потому что мы внесли в него много изменений, и он отличается от оригинальной версии. Я не исключаю, что какие-то изменения могут нарушить ожидаемое поведение и найти проблему будет сложно. А вот здесь https://github.com/wefiftytwo/MigrateTo64bitExample я разместил некоторые файлы, о которых шла речь в этой статье. Они представлены в двух версиях — до и после изменений.

Извините за мой английский =)