Введение в Swift Runtime: разбираем на детали

от автора

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

Нам в 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, благодаря которой у нас и получается бесшовное присвоение одной структуры в переменную другого типа.

Почему я нахожу это забавным? Потому что это по-своему уникальное исключение из общего принципа работы языка.

Итог

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

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


Рекомендованные статьи:

Также подписывайтесь на Телеграм-канал Alfa Digital — там мы постим новости, опросы, видео с митапов, краткие выжимки из статей, иногда шутим.


ссылка на оригинал статьи https://habr.com/ru/companies/alfa/articles/750348/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *