Глобальная область видимости: почему в SOOT нет приватных имён
Оглавление
Введение
В статье о REPL уже сказано, что в интерактивной сессии каждый модуль экспортирует всё, что в нём определено. Это решение со временем распространилось на весь язык: в SOOT нет приватных имён, а все определения всех модулей попадают в одну глобальную область видимости. Здесь рассказано, почему я так решила, что это даёт и что стоит.
Что принято в языках общего назначения
Обычное правило для языков общего назначения такое: модуль скрывает то, что относится к его внутренностям, и экспортирует только то, что предназначено для других. Приватные функции и данные нельзя вызвать извне, а имена внутри разных модулей не конфликтуют. Для больших программ с множеством авторов это правильное решение: оно защищает внутренние детали и позволяет менять их, не ломая остальной код.
Чтобы так работать, каждое определение нужно помечать: экспортируется оно или нет. Отсюда следует флаг в записи и две области видимости: область файла и общая, глобальная.
Что это значило бы для виртуальной машины
В SOOT вызов функции устроен просто. Инструкция call берёт функцию из регистра, а в регистр она попадает единственным способом: поиском по имени. Значит, каждое обращение к функции, методу или структуре превращается в поиск по имени, и этот поиск должен быть быстрым.
Теперь представим приватные имена. Экспортируемые определения можно индексировать в общей таблице. Но у приватных определений имена могут совпадать с приватными именами других модулей, поэтому в общую таблицу их класть нельзя. Остаётся три способа:
- искать приватное имя перебором в самом файле, что медленно;
- завести вторую таблицу с ключом «модуль плюс имя»;
- держать у каждого модуля собственную хэш-таблицу нужного размера.
Любой из них даёт многоуровневый поиск: сначала область файла, потом глобальная. Усложняются и таблицы, и формат файла, и загрузка, а каждое обращение стоит дороже. Всё это ради инкапсуляции, которая в скриптах игры нужна мало: скрипты пишет небольшая команда, и защищать от неё внутренние функции другого скрипта обычно не требуется.
Решение: одна глобальная область
Я отказалась от этих сложностей. Флага экспорта нет, приватных имён нет, в языке одна глобальная область видимости. Любая сущность любого модуля находится по имени одним поиском.
За это отвечает класс Globals. Он устроен так же просто, как решение: это плоская хэш-таблица из идентификатора строки в запись Symbol:
ptr— адрес ячейки определения внутри памяти загруженного модуля. Это не само значение, а место, где оно лежит;typeId— тип определения;ownerModulePath— модуль, которому оно принадлежит.
Globals не хранит значения и не владеет файлами: им владеет реестр модулей. Он лишь отвечает на вопрос «в каком загруженном модуле и по какому адресу лежит сущность с таким именем». Адрес действителен, пока модуль остаётся загруженным.
Результат: один поиск в хэш-таблице на обращение, никаких вторых таблиц, никаких лишних структур в памяти. Важно не переоценивать выгоду: измерять разницу в цифрах я не пыталась, но один поиск вместо двух и отсутствие дополнительных таблиц в любом случае проще и дешевле.
Анонимные сущности сегодня
Есть случай, который кажется исключением: структуры без имени. Если при инициализации поля-указателя создаётся новая структура, компилятор даёт ей сгенерированное имя. Искать такую структуру по имени не будет никто, но в нынешней схеме она тоже попадает в Globals.
Это даёт неожиданный бонус для отладки. Любую такую структуру можно найти в таблице, увидеть, где она лежит в памяти и какие у неё поля. Для инструментов это бесплатный способ заглянуть внутрь модуля.
Принцип «индексируется всё» в итоге получился последовательным: сгенерированное имя ничем не отличается от остальных.
Цена решения
У единой области видимости есть цена, и её лучше назвать прямо.
- Коллизии имён. Если два модуля определяют одно и то же имя, побеждает загруженный позже: запись в
Globalsпросто перезаписывается. Для REPL это именно то, что нужно (новое определение вытесняет старое), а для обычных модулей это может стать неожиданностью. Естественное улучшение — предупреждение при загрузке, если имя уже занято. - Нет настоящей инкапсуляции. Скрипт может случайно переопределить функцию из библиотеки или вызвать то, что автор модуля считал внутренним.
- Выгрузка модуля. Его имена из таблицы исчезают, а определения, которые он затенял, не возвращаются. Пока модули не выгружаются, это не проблема.
Компенсировать всё это можно культурой имён: префиксы по подсистемам, различимые имена для библиотечных и пользовательских функций. Для скриптов игры, которые пишет небольшая команда, это приемлемая плата. Для языка общего назначения такое решение было бы плохим, и я не предлагаю его как образец. Для узкой ниши, ради которой создавался SOOT, оно честно подходит.
Что дальше: локальные определения
Единая область не закрывает дверь для локальных сущностей. Идея в том, что сегодняшнее define заменяется явным выбором: определение может быть публичным или локальным (define-private). Для приватной сущности компилятор генерирует код, который ищет её не по имени, а загружает адрес определения напрямую. Имя разрешается при компиляции, а не во время выполнения, поэтому в рантайме такая сущность по сути анонимна и в Globals ей делать нечего. Двухуровневая область видимости при этом не возвращается: поиск по имени остаётся единственным способом найти публичное, а локальное никто по имени не ищет.
В ту же категорию попадают безымянные структуры-инициализаторы: на них ссылаются только по указателю.
Как это отразится на формате файла
Простейшее решение, флаг «не индексировать» в каждой записи, не подходит: все записи в файле выровнены по 64 бита, и один бит флага стоил бы лишних 8 байт на запись. Поэтому список определений в файле делится на две части: сначала идут публичные, потом приватные. В заголовке файла хранится одно число: сколько определений публичные. При загрузке модуль регистрирует в Globals только первую часть. Формат почти не усложняется, а память на флаги не тратится.
Имена приватных определений можно оставить в файле для инструментов: отладчик найдёт их, обойдя модуль, а общая таблица останется чистой.
Сроков я не обещаю: пока индексируется всё, и система отлично работает именно так.
Выводы
Изоляция модулей хороша для языков общего назначения, но каждая приватная сущность требует отдельного уровня поиска. Для виртуальной машины, в которой любой вызов начинается с поиска по имени, это дорого и сложно. Поэтому в SOOT одна глобальная область видимости, а возникающие проблемы решаются дисциплиной именования.
Решение не слишком чистое, зато простое и честное, и в этом оно похоже на остальную архитектуру SOOT. Если когда-нибудь понадобятся локальные определения, формат файла готов их принять почти без изменений: достаточно разделить список определений на публичные и приватные и записать их число в заголовок.