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

Комментарии (0)