Структуры
Структуры daScript, наследование
https://dascript.org/doc/reference/language/structs.html#struct-declaration
a,b,c,d,e - структуры, размещенные на стеке. В зависимости от способа объявления можно пропускать инициализацию полей по умолчанию — круглые скобки в объявлении добавляют код инициализации (в порядке от родительской структуры к дочерним). Синтаксис с квадратными скобками позволяет изменить значения отдельных полей. Неинициализированные явно или по умолчанию поля инициализируются нулями — получить в качестве значений неинициализированный мусор нельзя.
Ещё несколько примеров комбинаций синтаксиса инициализации, в первом случае создаётся структура, во втором — массив структур, в третьем — итератор
pd - указатель на структуру, размещенную в куче. Сама структура может быть инициализирована любым из перечисленных выше способов.
Финализаторы
https://dascript.org/doc/reference/language/finalizers.html#finalizers
Финализатор по умолчанию для структуры зануляет память (в порядке от потомков к родителям, при необходимости зовёт финализаторы для членов структуры в порядке объявления). Для указателей — после зануления полей структуры дополнительно меняет адрес указателя на null. Финализаторы для структур и классов зовутся вручную. Финализаторы не освобождают память, на которую указывает объект.
Освобождение памяти
Модель памяти по умолчанию в daScript не подразумевает очистки памяти в ходе выполнения скрипта, за очистку отвечает хост-приложение, которое может просто освободить всю память контекста целиком — пересоздание контекстов by design быстро и эффективно, так что такой способ предпочтительный.
Но можно настроить поведение контекста опцией persistent_heap
:
Я включил опцию DAS_SANITIZER
при сборке daScript, чтобы после освобождения объектов в случае с persistent_heap память перезаписывалась мусорными значениями (0xCD, -431602080 если интерпретировать 0xCDCDCDCD как float-значение). В данном случае программа по счастливому стечению обстоятельств не упала, но благодаря санитайзеру видно, что указатель pd2 после удаления pd стал висячим — указывает на свободную память, которая могла бы быть выделена другому объекту (объекту daScript того же контекста в случае persistent_heap=false
, или любому другому объекту хост-приложения с persistent_heap=true
).
Более “злобный” вариант примера:
После освобождения память на которую указывали pd и pd2 была повторно отдана новому объекту, на который указывает pd3. Этот объект теперь может поменяться через указатель pd2. Должно быть понятно, насколько unsafe операция удаления — код стал насколько же опасным (но и настолько же быстрым), как и код на языке си.
Кастомные финализаторы
Финализатор можно переопределить, пример: финализатор для структуры V2
Вместо финализатора зануления по умолчанию вызывается функция, логгирующая поле. Также можно заметить неочевидную вещь (если думать о финализаторах как о деструкторах, но лучше не думать) — финализаторы не зовутся в порядке от потомков к предкам, а как работают как обычные функции, daScript нашёл подходящую функцию, принимающую тип V2, и не вызвал зануления также и у поля z - т.е. финализатор родительской структуры “подошёл” к дочерней.
Более похожий на порядок вызова деструкторов в C/C++ код
(ещё раз, финализаторы — это не деструкторы, они вызываются только при явном вызове оператора delete!)
daScript не позволяет каст к дочерним типам explicit
-аргументов.
Вместо перегрузки finalize
можно перегрузить def operator delete(var v : V2 explicit)
— семантически более точно описывает, что для структур код финализатора будет вызван только в момент явного вызова оператора delete
.
Методы
Методы не могут быть объявлены при объявлении структур, но структуры могут хранить указатели на функции
Потомок может переопределить функцию
(https://github.com/GaijinEntertainment/daScript/blob/master/examples/test/unit_tests/override.das)
daScript позволяет создать две перегрузки функции set (а не определять дополнительное имя set_v3
), принимающие V2 и V3, тогда можно переписать пример без использования дополнительного имени, с уточнением типа функции set перед кастом и последующим автоматическим приведением этого указателя к правильному типу, определённому в V2:
Здесь explicit
в первом объявлении свободной функции set
позволяет сделать некоторую магию — несмотря на то, что эта функция “пропускает” только указатели на V2, это позволяет однозначно выделить эту функцию в приведении set = @@<(var thisV: V2; X, Y: float):void> set
среди двух прегруженных (иначе возникла бы неоднозначность — обе приводились бы с одинаковым приоритетом, и daScript выдавал бы ошибку). Но при этом сигнатура функции V2'set
уже не содержит этого explicit
(её тип выводится автоматически по правой части выражения, где явно указана сигнатура без explicit
). Таким образом V2'set
работает как виртуальная функция — может принимать первым аргументом как V2
, так и её потомков, которые не переопределили функцию.
Классы
https://dascript.org/doc/reference/language/classes.html#classes
Классы в daScript — это структуры “на стероидах”. Немного отличий:
- Класс может быть отнаследован от структуры, но структура не может быть унаследована от класса (связано с тем, что классы могут иметь инициализаторы)
- Объявление локального класса на стеке небезопасно (требует явного unsafe)
Методы реализованы как указатели на функции, но с возможностью объявлять их в теле класса и переопределять с помощью ключевого слова override
без явного каста типа метода, как было со структурами
class V2 |
Можно заметить, что имя объявленной внутри класса-функции манглится с помощью префикса-имени класса (set -> V2'set
). Также внутри метода доступен указатель self
, неявно передаваемый первым аргументов в методы класса.
Стоит более детально рассмотреть вывод результата.
Порядок полей объекта в памяти
https://dascript.org/doc/reference/language/classes.html#implementation-details
Содержимое a
:
Комментарии после имён функций — замангленное имя функции и её сигнатуры (аргументы и результаты).
Расположение в памяти серьёзно отличается от C++ — указатели на виртуальные функции хранятся не в отдельной таблице (vtable), а в каждом объекте класса в том порядке, в котором были объявлены функции. Это позволяет убрать один уровень индирекции при вызове функций (не нужно идти за адресом в виртуальную таблицу) и изменять адреса функций динамически для каждого отдельного объекта, но увеличивает расходы памяти на хранение указателей, и также может повлиять на выравнивание и padding между полями.
Тем не менее, для модификации порядка данных структур возможно написать собственный макрос, который будет хранить функции в самостоятельно сгенерированной таблице или выносить указатели на функции в конец структуры.
daScript macro — пример макроса перестановки порядка полей при определении структур
Существует также макрос [cpp_layout]
, который не меняет порядок членов класса/структуры, но добавляет дополнительное правило выравнивания, как делают С/C++ — в конце родительской структуры будет оставлено пространство для её выравнивания по максимальному выравниванию членов структуры — например, если добавить в конце V2 поле на 4 байта padding: uint8[4]
, то из-за выравнивания структуры в 8 байт (из-за указателей на 64-битной платформе), поле z, будет добавлено с отступом в 8 байт (без макроса daScript без проблем “встраивает” это поле сразу за 4-байтным отступом).
Переопределение метода в экземпляре класса
Как было замеченно выше, каждый экземпляр класса/структуры хранит собственные копии указателей на функции, так что можно переопределить метод не на уровне класса-потомка, а в экземпляре класса (в пост-инициализаторе, или в любой момент после создания):
(более практичное применение этого — паттерны типа event/callback)
RTTI
Пример вывода на экран информации о типе в runtime:
Доступна информация о названиях и типах полей, а также мета-информация (флаги класс/структура, выделена на стеке/хипе, аннотации и т.п.).
Для того, чтобы передать и распознать аннотации, необходимо включить опцию options rtti=true
(в противном случае, метаинформация о произвольных аннотациях выбрасывается после симуляции, линк). Пример:
Abstract и sealed-методы
Методы можно сделать абстрактыми, или закрытыми для переопределения
Видимость
- Из модуля экспортируются функции с аннотацией [export]
options always_export_initializer=true
позволяет проставить аннотацию для всех инициализаторов на уровне модуля
private
для переменных и типов ограничивает их доступность из других модулей
Приватными могут быть также поля и функции структур/классов
Инициализаторы
Инициализатор для класса — это функция, у которой имя совпадает с именем класса. Так как классы — надстройки над структурами, и все варианты синтаксиса иницилизации действуют и для них, то нет никакой гарантии того, что инициализатор класса будет вызван.
Интерфейсы
Библиотека interfaces
с помощью пары макросов позволяет реализовать паттерн интерфейса — классы, который содержит только абстрактные методы. Макрос implements
позволяет изобразить множественное наследование от интерфейсов.
Связь с C++-типами
update
- Связь daScript классов с C++-классами - перенёс в отдельную заметку