Open Mazdaywik opened 5 years ago
Входные файлы компилятора могут быть не только исходниками, но и скомпилированной парой .rasl
+.cpp
(в дальнейшем может быть и .rasl
+.c
). Откуда линковщик узнает имя инициализатора?
Предлагается в первых пяти строках файла размещать комментарий, который обязан содержать строку
INITIALIZER initializer_name
Например:
// INITIALIZER initializer_123456_ABCDEF
/*
INITIALIZER initializer_ABCDEF_09876
*/
И это имя берётся для extern const Initializer …;
.
Если такой строки нет — это не ошибка. Это может быть файл рантайма, у которого нет инициализатора.
UPD 22.10.2020: описано в #324.
Задача переписывания всего на Си делится на две сравнительно независимые части:
Причём первая мягко блокирует вторую. Можно и вполне естественно переписать генерацию кода и используемый из неё API на Си, оставляя реализацию на C++. В этом случае реализация на C++ будет использовать определения структур данных языка C, которые будут вынесены в API (описание типов данных узла, например).
А вот обратный вариант получается неестественным. Реализация зависит от API рантайма, в особенности описания структур данных. Оставлять их на C++, переписывая реализацию на Си, не получится. Не, конечно, можно изловчиться и нагородить костыли, но это глупо.
Переписывание API рантайма и генерации кода тоже можно формально разделить на две части: собственно, переписывание API и переписывание генерации кода. Но в этом смысла нет. Во-первых, зачем нужен C API, если используемый код остаётся плюсовым? Во-вторых, если переписать API, то от C++ в генераторе кода ничего, кроме расширения .cpp
, не останется.
На данный момент скомпилированные единицы трансляции представляют собой либо одиночные файлы .rasl
, либо пары .rasl
+ .cpp
. Нужно обеспечить поддержку пар .rasl
+ .c
.
Эта задача — подзадача для #185.
Гипотеза: на языке Си получаются более эффективные программы, чем на Си++. Предпосылки.
Поэтому ради повышения быстродействия разумно рассмотреть вариант переписывания на Си. И посмотреть, к каким следствиям это приведёт.
Очевидные изменения
Очевидно, что в рантайме и
Library
придётся отказаться от классов, пространств имён и стандартной библиотеки Си++.Классы придётся заменять на структуры, методы — на функции, принимающие указатели на класс. Пространства имён придётся заменять префиксами.
Вместо контейнеров стандартной библиотеки придётся писать свои велосипеды, либо делать интруизивные контейнеры — просто предусматривать, что объекты могут объединяться в список и добавлять для этого поля вроде
next
.Возникнет головная боль с явным освобождением памяти и других ресурсов.
Но это всё по большей части тривиальный рефакторинг, не вносящий содержательную сложность (но, по опыту разработки Рефала-05, довольно увлекательный).
Будет два очевидных положительных последствия:
Нетривиальные изменения
Нетривиальной здесь будет компиляция файлов с нативным кодом. Сейчас она завязана на вызов конструкторов глобальных объектов — возможность, которая есть в стандартном Си++ и которой нет в стандартном Си (есть непереносимые средства в различных реализациях).
Что у нас есть
Текущее представление развивалось эволюционно, путём постепенного переноса части знаний из Си++ в RASL, внесения реентерантности и т.д. Поэтому (как дальше будет видно), оно кривоватое.
Сгенерированный файл можно условно разбить на несколько частей:
#include "refalrts.h"
#define cookie_ns cookie_ns_MMMMM_NNNNN
— объявление уникального имени пространства имён, поскольку Open Watcom криво работает с безымянными пространствами имён.IdentReference
), которые используются в нативном коде. В исходнике они должны быть обозначены как$LABEL
. Имена декорируютсяExternalReference
) видаСсылки даются на все функции из области видимости: как объявленные в
$EXTERN
'ах, так и определённые в файле. Для ссылок указывается имя функции и идентификатор области видимости (два нуля для глобальных, extern или entry, ненулевые cookie для локальных функций).Глобальные нативные вставки и определения нативных функций. Последние имеют вид
(длинные строки разбил)
Все эти объекты —
IdentReference
,ExternalReference
,NativeReference
,GlobalRef<>
полагаются на свои конструкторы для инициализации.При этом, рантайм у нас реентерантный (#163). А это значит, что два независимых домена могут загрузить один и тот же модуль, и значения, доступные через эти переменные, должны быть в каждом домене свои. Эти значения хранятся в доменах, а в
IdentReference
,ExternalReference
иGlogalRef
находятся только смещения для внутреннего массива.А что же делают глобальные конструкторы. Для типов
*Reference
они провязывают их в односвязный список, голову помещают в глобальную переменную. Для первых двух сущностей устанавливается глобальный счётчик. ДляGlobalGen<>
вычисляется смещение этой переменной во внутримодульной памяти. После инициализации эти объекты более не меняются. Результаты их инициализации (головы списков, максимумы счётчиков) сохраняются в глобальной структуреNativeModule g_module
— собственно, конструкторы кладут себя на голову списка и увеличивают счётчики в этой структуре.При загрузке модуля в смысле Рефала (#167) по содержимому
g_module
инициализируются вектора для идентификаторов и внешних ссылок, из списка нативных функций по именам извлекаются указатели на них, для глобальных переменных выделяется соответствующий объём памяти. Модуль может быть загружен в несколько доменов — соответствующие вектора везде будут свои, но индексы одни и те же.К идентификаторам, внешним функциям и глобальным переменным приходится обращаться вызовом вида
object.ref(vm)
, для того, чтобы обратиться к соответствующему массиву, доступному внутри виртуальной машины.NativeReference
в пользовательском коде не вызываются.Все эти объекты возникли эволюционно — сначала они содержали реальные данные, а вместо
GlobalRef<>
были обычные глобальные переменные. Потом с реализацией реентерантности (#163) они стали содержать ссылки.Недостаток очевиден — сложность и куча уровней косвенности. Он станет ещё очевиднее, когда мы далее рассмотрим более простое и эффективное решение.
Что с этим делать
Все четыре вида сущностей нужно инициализировать при загрузке модуля (имеются ввиду I- и N-модули). Но конструкторами мы их инициализировать не можем, потому что у нас Си. Можем только константами во время компиляции. Нам нужно, чтобы кто-то дёргал функции инициализации для них.
Отложим на секунду вопрос о том, кто же их дёргает. Подумаем: как?
Допустим, в файле можно использовать объект с конструктором, но только один. Что можно сделать? Можно сформировать массивы указателей соответствующих объектов, и адреса этих массивов передать инициализатору. Примерно так:
Нули символизируют значения счётчика-смещения,
NULL
в конце массива — признак конца. Провязка ссылкамиnext
здесь уже не нужна (поскольку они доступны из массивов). Остальная логика остаётся неизменной.Теперь, что делать с инициализатором. Он дёргает глобальные сущности, а кто его дёргает? Чтобы его кто-то дёрнул, он должен быть доступен извне. Плюс его имя должно быть уникальным (чтобы не было конфликта имён с другими единицами трансляции). Плюс его имя должно быть известно извне.
У каждого модуля есть свой уникальный идентификатор — пара cookies. Их можно использовать для формирования уникального имени:
На стадии компоновки можно обеспечить знание идентификаторов каждого из модулей, а это значит, можно сделать их «дёргалку»:
А рантайм уже знает о том, что в любом модуле с нативным кодом есть глобальный массив
all_initializers
, и по нему нужно пробежать при запуске.Собственно, вот минимальное изменение инициализации нативных модулей, которое работает без конструкторов глобальных переменных. Фактически, это перенос того, что делает рантайм Си++, в рантайм Рефала. C++, конечно, использует не предопределённые имена переменных (вроде
all_initializers
), а имена секций, но суть не меняется.С глобальными конструкторами множество всех объектов обходилось дважды — рантаймом C++ для вызова конструкторов и рантаймом Рефала для уже своей инициализации. Список глобальных объектов не был виден рантайму Рефала, поэтому на первом проходе объекты провязывались в списки (доступные рантайму Рефала).
В предлагаемой реализации объекты складываются в массивы, которые видны рантайму Рефала, а значит, (а) обе фазы инициализации выполняются за один проход, (б) провязка ссылками становится не нужна.
Флаг
module_initialized
нужен для обеспечения реентерантности — чтобы избежать повторной инициализации этих объектов из другого домена. Возникает проблема многопоточности, но мы закроем на неё глаза. В дальнейшем мы откажемся от этого флага.Но это решение почти ничего не упрощает по сравнению с имеющимся — ну, нет односвязных ссылок, и что? А то, что дальше пойдут оптимизации.
Оптимизации нового представления
Во-первых, можно заменить глобальные объекты и массивы указателей на одни только массивы:
где
ident_ref
определена какА функция
Identifier *ident_by_id(VM *vm, unsigned id)
будет находить поid
соответствующий идентификатор. Аналогично делаем сExternalReference
.По факту нативный код в этой ситуации идентифицирует объекты по целочисленному индексу. И эти индексы свои для каждой единицы трансляции. По этим индексам из массивов
idents
илиexternals
извлекается другой индекс, по которому уже производится обращение ко внутренней таблице модуля.Почему так сложно? Потому что раньше все объекты объединялись в один список и получали последовательные глобальные номера. Различить объекты из разных единиц трансляции было нельзя. А теперь можно это обеспечить — достаточно
initializer_AAAAA_BBBBB
инициализировать куками.Лишний уровень косвенности пропадает — у каждой единицы трансляции теперь будут свои массивы идентификаторов и внешних функций, индексация будет одна.
Но ведь у каждой единицы трансляции уже есть свои массивы с локальной нумерацией! При генерации RASL’а строятся таблицы внешних функций и идентификаторов, а в команды кода помещаются номера из этих таблиц.
А это значит, что параллельных массивов (для RASL’а и нативного кода) строить не надо. Достаточно использовать те же номера.
IdentReference
иExternalReference
отпадают за ненадобностью.Как можно упростить
GlobalRef
? Что делает сейчасGlobalRef<>
— его инициализатор определяет смещение в памяти для значения + подсчитывает суммарный объём памяти. Но если память глобальных переменных выделять не для модуля, а для единицы трансляции, то и смещения, и суммарный объём можно определить на стадии компиляции. Достаточно все переменные сложить в одну структуру.Получаем такую картину:
Поскольку в инициализаторе хранятся куки, их не нужно хранить в
NativeReference
. Если пользователь описал структуру, то он должен определить макросVAR_TYPE
, раскрывающийся в имя этой структуры. Если макрос определён, то размер соответствующего типа заносится вInitializer
.Заметим, что теперь все структуры данных инициализируются на стадии компиляции и для рантайма они могут быть доступны только для чтения. Никакого
module_initialized
более не нужно.Вывод
Даже если не переходить на Си, то поразмышлять об этом полезно. В частности, можно прийти к существенному упрощению компиляции нативного кода. Можно даже обойтись без списка инициализаторов на стадии компоновки, а инициализировать
Initializer
конструктором с побочным эффектом. Всё равно будет польза.Более того, если полностью реализовать этот подход, упростится реализация модулей типа N (#170), ведь нативный код по сути — ни что иное, как ссылка на массив
initializers
. Возникают нюансы с передачей функций рантайма — их можно передавать как указатели на функции в виртуальной машине..rasl
+.c
как единиц трансляции, переписывание генератора кода на C++.