Pull to refresh
566.58
Альфа-Банк
Лучший мобильный банк по версии Markswebb

Раскладываю Swift Runtime на детали

Level of difficultyMedium
Reading time9 min
Views7.9K

Что такое 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 — там мы постим новости, опросы, видео с митапов, краткие выжимки из статей, иногда шутим.

Tags:
Hubs:
Total votes 37: ↑37 and ↓0+37
Comments1

Articles

Information

Website
digital.alfabank.ru
Registered
Founded
1990
Employees
over 10,000 employees
Location
Россия
Representative
София Никитина