Привет, меня зовут Александр Пахомов, я работаю в Альфа-Банке на проекте для юридических лиц Альфа Бизнес Мобайл. Начну с предыстории: в детстве у меня было два игрушечных пистолетика, и в какой-то момент один из них сломался. Тогда я придумал просто отличный план — почему бы не разобрать второй, посмотреть как он работает и починить первый? В итоге у меня стало два сломанных пистолетика. Но не только — я получил и некоторые знания. И, что важнее, я удовлетворил своё любопытство и тягу к знаниям.
Нам в IT, чтобы понять как что-то работает не нужно ничего ломать — мы будем разбираться на пальцах — погрузимся в теорию. Не спешите закрывать статью — практическая польза в теории тоже есть. Например, как-то ребята из соседней команды не могли разобраться с пребилдом, который они настраивали своем проекте. Не знаю, сколько бы они мучились с этой задачей, но, думаю, сэкономил прилично времени тем, что дал подсказку на основе тех знаний, что приобрел в процессе изучения.
В статье мы изучим Runtime: посмотрим на исходный код, попытаемся понять, что он делает, посмотрим, как Runtime взаимодействует с нашим кодом, где появляется и зачем. А в конце будут случайно найденные факты о языке Swift, которые мне показались интересными.

Что такое Runtime?
Наверное, вы уже использовали словосочетание «Библиотека Runtime». Можно предположить, что это и есть какая-то библиотека, а значит — у неё есть исходный код. Очевидно, что он находится в репозитории Swift.

Идём туда и видим кучу-кучу-кучу каталогов файлов. Когда я туда попал, почувствовал себя как маленький малыш Йода — стало немножко страшно и неуютно.
Если вдруг вы там ещё не бывали, то вот вам мини-гайд. Самое интересное, что там можно посмотреть, это:
-
/doсs— документация: README-файлики по различным тематикам. Но она не особо полная. Чтобы вы понимали, насколько не полная, то иногда, после долгих поисков, вы найдёте тот самый README-файлик, ту самую секцию, а там стоит TODO-шка из разряда «Саня! Не забудь дописать, мы тут оставили» -
/lib— исходники самого компилятора. Наверное, это самая сложная часть, ведь понять, как работает компилятор не просто. Поэтому сюда я бы рекомендовал залезать в самом конце. Самое любопытное лежит в каталоге/stdlib/public/. -
/stdlib/public/core— стандартная библиотека. -
/stdlib/public/runtime— Рантайм! Его-то мы и искали.
Далее открываем исходный код — то, что лежит в каталоге /stdlib/public/runtime.
И сначала немножко путаемся, потому что глазу не за что зацепиться — какие-то незнакомые функции. Но я потратил некоторое время и накопал вот такие функции, например:
HeapObject *swift::swift_nonatomic_retain(HeapObject *object)
Она принимает один объект и один объект отдаёт.
По названию и по телу функции можно предположить, что это работа механизма ARC — мы видим там какой-то инкремент ссылки.
if (isValidPointerForNativeRetain(object)) object->refCounts.incrementNonAtomic(1); return object;
Там же можно накопать аналогичные функции для strong ссылок, для weak ссылок и т.д.
Идём дальше и находим вот такую функцию:
/// Dynamically cast a class metatype to a Swift class metatype. static const ClassMetadata * _dynamicCastClassMetatype(const ClassMetadata *sourceType, const ClassMetadata *targetType)
Судя по названию, она выполняет динамическое преобразование одного класса к другому, и в теле…
do { if (sourceType == targetType) { return sourceType; } sourceType = sourceType->Superclass; } while (sourceType); return nullptr;
… просто проходится по супер-классам вверх, что полностью соответствует названию и нашему представлению о работе такого механизма в Swift. Можно предположить, что это нечто похожее на as? в нашем Swift.
Так что такое Runtime?
Возвращаясь к вопросу «Что такое Runtime», можно сказать, что это написанная на C++ библиотека, которая занимается обслуживанием встроенных в сам язык Swift функций. В частности, тут можно выделить как минимум две больших категории: работа с памятью (ARC), и работа с типами данных.
Конечно, там есть и другие функции, но они не представляют такого интереса в рамках данного разбора Runtime’а языка.
Теперь переходим к главному вопросу — как происходит это взаимодействие?
Где появляется Runtime и зачем?
Для поиска ответа нам придется погрузиться в процесс компиляции. Как обычно она у нас происходит? У нас есть исходный код → мы нажимаем Command+B → магия → получаем то, что можно запустить.

Но хватит шуток, мы все понимаем, что в реальности там довольно много этапов. Если очень упрощенно их описать, то можно выделить вот такие:
-
AST — абстрактное синтаксическое дерево.
-
SIL — Swift Intermediate Language.
-
IR — Intermediate Representation.

При этом важно подчеркнуть, что к компилятору Swift относятся первые три шага (поэтому они выделены). Дальше, когда компилятор выдает то, что называется IR (Intermediate Representation), он отдаёт это в LLVM и там оно уже преобразуется в объектный файл. Поэтому мы будем рассматривать первые три шага.
Я бы ещё откинул AST, потому что, фактически, это скорее результат парсинга исходного кода и он не представляет достаточного интереса для изучения. Если вам кажется, что этот этап имеет значение, напишите в комментариях, возможно, я заблуждаюсь.
Ищем зацепки
Runtime реализует работу с памятью. Напишем довольно простой исходный код, в котором точно будет ARC. Скомпилируем и посмотрим, как он выглядит на уровне SIL и IR.
Где будем искать? Здесь.
class MyClass {} func main() { let object = MyClass() } main()
Конкретнее — внутри тела метода main. Там происходит создание и уничтожение объекта, что должно сопровождаться инкрементом и декрементом ссылки на него.
let object = MyClass()
Вот функция main на уровне SIL.
// main() sil hidden @$s4mainAAyyF : $@convention(thin) () -> () { bb0: %0 = metatype $@thick MyClass.Type // function_ref MyClass.__allocating_init() %1 = function_ref @$s4main7MyClassCACycfC : $@convention(method) (@thick MyClass.Type) -> @owned MyClass %2 = apply %1(%0) : $@convention(method) (@thick MyClass.Type) -> @owned MyClass debug_value %2 : $MyClass, let, name "object" strong_release %2 : $MyClass %5 = tuple () return %5 : $() } // end sil function '$s4mainAAyyF'
Разберём её по строкам.
Первое, что происходит — берётся сам класс, с помощью которого дальше будет создан экземпляр…
%0 = metatype $@thick MyClass.Type
Дальше вызывается конструктор, куда передается метатип. В итоге мы получаем экземпляр класса.
// function_ref MyClass.__allocating_init() %1 = function_ref @$s4main7MyClassCACycfC : $@convention(method) (@thick MyClass.Type) -> @owned MyClass %2 = apply %1(%0) : $@convention(method) (@thick MyClass.Type) -> @owned MyClass
Ну и третья строчка — самая важная.
strong_release %2 : $MyClass
Мы видим, что компилятор проставил некое ключевое слово strong_release. То есть на уровне SIL неявный механизм работы ARC стал явным.
Но мы понимаем, что это лишь ключевое слово — в дальнейшем оно может быть преобразовано во всё, что угодно. Во что, нам неизвестно, поэтому идём дальше и смотрим на IR.
define hidden swiftcc void @"$s4file4mainyyF"() #0 { entry: %object.debug = alloca %T4file7MyClassC*, align 8 %0 = bitcast %T4file7MyClassC** %object.debug to i8* call void @llvm.memset.p0i8.i64(i8* align 8 %0, i8 0, i64 8, i1 false) %1 = call swiftcc %swift.metadata_response @"$s4file7MyClassCMa"(i64 0) #4 %2 = extractvalue %swift.metadata_response %1, 0 %3 = call swiftcc %T4file7MyClassC* @"$s4file7MyClassCACycfC"(%swift.type* swiftself %2) store %T4file7MyClassC* %3, %T4file7MyClassC** %object.debug, align 8 call void bitcast (void (%swift.refcounted*)* @swift_release to void (%T4file7MyClassC*)*)(%T4file7MyClassC* %3) #2 ret void }
Я не буду здесь показывать каждую строку, потому что это будет слишком подробно. Лишь подчеркну самое важное.
В последней строке мы видим вызов функции @swift_release. А чуть ниже есть декларация (именно декларация, без определения) функции:
declare void @swift_release(%swift.refcounted*) #2
Таким образом получается, что компилятор на месте ключевого слова strong_release оставил вызов неизвестной нам функции swift_release, реализация которой будет найдена уже на этапе линковки.
Предполагаем, что эта функция есть в исходном коде Runtime’а и идём искать там. Благодаря обычному поиску по тексту находится вот такая функция:
static void _swift_release_(HeapObject *object)
И всё, что происходит внутри, это декремент ссылки:
if (isValidPointerForNativeRetain(object)) object->refCounts.decrementAndMaybeDeinit(1);
Ура! Мы нашли то, что искали. Осталось только собрать всё воедино.
Общая картина
Итак, если составлять общую картину на основе того, что мы изучили, можно описать процесс примерно так.

-
У нас есть исходный код (сверху слева).
-
На уровне SIL компилятор явно реализует неявные внутренние механизмы языка Swift.
-
На уровне IR компилятор преобразует ключевые слова из SIL в конкретные вызовы Runtime’а (упрощённое описание).
-
Динамический Линковщик соединяет наш вызов функции с её реализацией в библиотеке Runtime’а, которая есть в системе.
Ответ на второй вопрос «Где появляется Runtime и зачем?» будет таким: «Компилятор неявно для нас проставляет вызовы к Runtime библиотеке там, где это требуется для реализации встроенных в язык Swift функций. Например, ARC или работа с типами данных. Во время динамической линковки эти вызовы соединяются с реализацией»
Итак, Swift Runtime…
Если подводить некоторые итоги, в целом, по Runtime, то можно сказать, что:
-
Это библиотека, написанная на языке C++.
-
Она реализует внутренние механизмы работы самого языка Swift.
-
Принцип работы основан на внедрении вызовов на этапах компиляции.
Интересные факты
В процессе изучения исходного кода я наткнулся на несколько любопытных решений в языке Swift, которыми хотел бы поделиться.
Сломать всё одной функцией
Когда я изучал символьные таблицы у полученных объектных файлов, я задался вопросом: а что если в мой исходный код добавить функцию swift_release? Ведь компилятор, проставляя вызов, рассчитывает, что функция с таким названием найдётся только в Runtime библиотеке. А я возьму и создам свою функцию с аналогичным именем. Что будет?

Я добавил в свой код вот такую функцию, которая принимает один параметр (как и в требуемой сигнатуре) и печатает строку “Release”.
func swift_release(_ objet: AnyObject) { print("Release") }
С первой попытки сломать всё у меня не получилось, но я продолжил и кое-что выяснил: в символьной таблице указано не просто имя функции, а её идентификатор, называемый mangled name. По сути, это строка, которая содержит в себе всё описание сигнатуры функции, включая язык программирования, имя файла, принимаемые параметры и тип возвращаемого значения.
И вот моя функция swift_release на уровне символьной таблице уже имела совершенно другое имя:
func swift_release(_ objet: AnyObject) ↓ "$s4file13swift_releaseyyyXlF"
В итоге из-за того, что я не учёл ‘name mangling’, моя функция и не была слинкована с тем самым вызовом. Но в Swift есть возможность переопределить это поведение с помощью специального атрибута:
@_silgen_name("swift_release") func swift_release(_ objet: AnyObject) { print("Release") }
Дальше, при запуске моей программы с такой функцией в исходном коде, произошла магия — хотя эта функция ниоткуда не вызывалась, в консоль печаталась строка “Release”! И, очевидно, все объекты просто перестали уничтожаться.
Что ещё интереснее — если то же самое сделать с функцией swift_retain, то при запуске программы вы получите ошибку сегментации. Причина — теперь все объекты не могут произвести инкремент ссылки, из-за чего получается некоторая несостыковка состояния памяти. Как по мне, это очень забавно.
Исключения type-checker’а
Дальше расскажу о том, что мне понравилась, наверное, больше всего.
Предполагаю, что вы пользовались функцией type(of:) . Вот так выглядит её сигнатура:
public func type<T, Metatype>(of value: T) -> Metatype
И вот, что интересно, она ведь реализована в стандартной библиотеке, которая, в свою очередь, написана на Swift. Но возникает вопрос — а как её реализовать?
А если глянуть на реализацию в стандартной библиотеке, мы увидим такой комментарий:
// This implementation is never used, since calls to `Swift.type(of:)` are // resolved as a special case by the type checker. Builtin.unreachable()
По комментарию можно понять, что на самом деле эта функция, можно сказать, ненастоящая, и вызовы к ней обрабатываются каким-то особым образом.
Стоит ещё заменить необычный атрибут @_semantics("typechecker.type(of:)") — он понадобится чуть позже.
@_semantics("typechecker.type(of:)") public func type<T, Metatype>(of value: T) -> Metatype
Идём искать в исходник компилятора. И что мы там видим?
DeclTypeCheckingSemantics TypeChecker::getDeclTypeCheckingSemantics(ValueDecl *decl) { // Check for a @_semantics attribute. if (auto semantics = decl->getAttrs().getAttribute<SemanticsAttr>()) { if (semantics->Value.equals("typechecker.type(of:)")) return DeclTypeCheckingSemantics::TypeOf; if (semantics->Value.equals("typechecker.withoutActuallyEscaping(_:do:)")) return DeclTypeCheckingSemantics::WithoutActuallyEscaping; if (semantics->Value.equals("typechecker._openExistential(_:do:)")) return DeclTypeCheckingSemantics::OpenExistential; } return DeclTypeCheckingSemantics::Normal; }
Функция, которая парсит тот самый атрибут @_semantics и для трёх уникальных значений выдаёт три уникальных способа обработки вызова к функции. Или тип ‘Normal’, имя в виду обычный вызов обычной функции.
Чтобы больше не тратить ваше время на погружения в исходный код компилятора поясню это таким образом: в языке Swift есть три функции-исключения, особенности которых связаны с одновременной необходимостью быть доступными прямо из Swift-кода и невозможностью реализации в самом Swift-коде. Поэтому для таких трёх функций компилятор делает особое исключение — в стандартной библиотеке находится, по сути, декларация, с пустым телом, а на этапе компиляции вызовы к таким функции заменяются специальными конструкциями.
По сути, эти три функции-исключения скорее стоит определить, как инструмент самого языка программирования (подобно as?, await и тд), который просто для нашего с вами удобства представлен не в виде особого синтаксиса, а в виде обычной функции.
Магия AnyHashable
Последний занимательный факт, который я нашел, связан с AnyHashable.
Возьмём самую обычную конструкцию из структуры, которая реализует протокол Hashable, и переменной типа AnyHashable, который мы присваиваем её экземпляр.
struct Model: Hashable {} let hashable: AnyHashable = Model()
Казалось бы, что в этом необычного? А вот то, что AnyHashable — это структура. Поэтому возникает вопрос, каким образом мы присваиваем переменной с типом структуры другую структуру?
@frozen public struct AnyHashable { internal var _box: _AnyHashableBox internal init(_box box: _AnyHashableBox) { self._box = box } }
Оказывается, если посмотреть SIL, то можно увидеть, как компилятор «заботливо» оборачивает правую сторону выражения в функцию _convertToAnyHashable, благодаря которой у нас и получается бесшовное присвоение одной структуры в переменную другого типа.
Почему я нахожу это забавным? Потому что это по-своему уникальное исключение из общего принципа работы языка.
Итог
У меня давно было желание разобраться в тех вопросах, что я описал в статье. Но подтолкнул меня к ней доклад моего коллеги Максима Крылова (на его основе Максим подготовил статью). Ведь если он может рассказать на большую аудиторию о своих исследованиях, значит и я могу. И я хочу верить, что для кого-нибудь из вас моя статья также станет отправной точкой вашего собственного исследования.
Если у вас есть вопросы, пожелания, проклятия — пишите в комментариях, буду отвечать по мере возможностей.
Рекомендованные статьи:
-
Хочу в iOS-разработку: к чему готовиться на собеседовании в продуктовую команду
-
100 дней из жизни новичка: как устроен онбординг в мобильной разработке
-
Как катить фичи без релизов. Часть 2: про низкоуровневый Server Driven UI
-
Как снимать логи с устройств на Android и iOS: разбираемся с инструментами
Также подписывайтесь на Телеграм-канал Alfa Digital — там мы постим новости, опросы, видео с митапов, краткие выжимки из статей, иногда шутим.
ссылка на оригинал статьи https://habr.com/ru/companies/alfa/articles/750348/
Добавить комментарий