Структуры
Структуры daScript, наследование
https://dascript.org/doc/reference/language/structs.html#struct-declaration
struct V2 |
a,b,c,d,e - структуры, размещенные на стеке. В зависимости от способа объявления можно пропускать инициализацию полей по умолчанию — круглые скобки в объявлении добавляют код инициализации (в порядке от родительской структуры к дочерним). Синтаксис с квадратными скобками позволяет изменить значения отдельных полей. Неинициализированные явно или по умолчанию поля инициализируются нулями — получить в качестве значений неинициализированный мусор нельзя.
Ещё несколько примеров комбинаций синтаксиса инициализации, в первом случае создаётся структура, во втором — массив структур, в третьем — итератор//where clause: post init function
let f = [[V3 where $(var self) {
self.x = 11.0f;
self.y = 11.0f;
self.z = 11.0f;
}]]
//array initialization, g_arr: V3[2]
let g_arr = [[V3
x=1.0f, y=1.0f, z=1.0f;
x=2.0f, y=2.0f, z=2.0f
]]
//array comprehesion, h_arr : iterator<V3>
let h_iter <- [[ for i in range(0, 10); [[V3 x=float(i), y=float(i), z=float(i)]]; where (i&1)==1 ]]
let g_iter <- [[ for i in range(0, 10); invoke({ let fi = float(i); return [[V3 x=fi, y=fi, z=fi]]; }); where (i&1)==1 ]] // same
pd - указатель на структуру, размещенную в куче. Сама структура может быть инициализирована любым из перечисленных выше способов.
Финализаторы
https://dascript.org/doc/reference/language/finalizers.html#finalizers
var d = [[V3() y = 2.0f]] |
Финализатор по умолчанию для структуры зануляет память (в порядке от потомков к родителям, при необходимости зовёт финализаторы для членов структуры в порядке объявления). Для указателей — после зануления полей структуры дополнительно меняет адрес указателя на null. Финализаторы для структур и классов зовутся вручную. Финализаторы не освобождают память, на которую указывает объект.
Освобождение памяти
Модель памяти по умолчанию в daScript не подразумевает очистки памяти в ходе выполнения скрипта, за очистку отвечает хост-приложение, которое может просто освободить всю память контекста целиком — пересоздание контекстов by design быстро и эффективно, так что такой способ предпочтительный.
Но можно настроить поведение контекста опцией persistent_heap
:
options persistent_heap = true |
Я включил опцию DAS_SANITIZER
при сборке daScript, чтобы после освобождения объектов в случае с persistent_heap память перезаписывалась мусорными значениями (0xCD, -431602080 если интерпретировать 0xCDCDCDCD как float-значение). В данном случае программа по счастливому стечению обстоятельств не упала, но благодаря санитайзеру видно, что указатель pd2 после удаления pd стал висячим — указывает на свободную память, которая могла бы быть выделена другому объекту (объекту daScript того же контекста в случае persistent_heap=false
, или любому другому объекту хост-приложения с persistent_heap=true
).
Более “злобный” вариант примера:var pd = new [[V3() y = 2.0f]]
var pd2 = pd
unsafe { delete pd; }
var pd3 = new [[V3() x = 33.0f, y = 33.0f, z = 33.0f]]
pd2.x = -100.0f // <----- kaboom!
print("pd2 = {pd2}\n")
print("pd3 = {pd3}\n")
//Output
pd2 = [[ -100.000000000; 33.000000000; 33.000000000]]
pd3 = [[ -100.000000000; 33.000000000; 33.000000000]]
После освобождения память на которую указывали pd и pd2 была повторно отдана новому объекту, на который указывает pd3. Этот объект теперь может поменяться через указатель pd2. Должно быть понятно, насколько unsafe операция удаления — код стал насколько же опасным (но и настолько же быстрым), как и код на языке си.
Кастомные финализаторы
Финализатор можно переопределить, пример: финализатор для структуры V2
def finalize(var v : V2) |
Вместо финализатора зануления по умолчанию вызывается функция, логгирующая поле. Также можно заметить неочевидную вещь (если думать о финализаторах как о деструкторах, но лучше не думать) — финализаторы не зовутся в порядке от потомков к предкам, а как работают как обычные функции, daScript нашёл подходящую функцию, принимающую тип V2, и не вызвал зануления также и у поля z - т.е. финализатор родительской структуры “подошёл” к дочерней.
Более похожий на порядок вызова деструкторов в C/C++ код
(ещё раз, финализаторы — это не деструкторы, они вызываются только при явном вызове оператора delete!)
def finalize(var v : V2 explicit) |
daScript не позволяет каст к дочерним типам explicit
-аргументов.
Вместо перегрузки finalize
можно перегрузить def operator delete(var v : V2 explicit)
— семантически более точно описывает, что для структур код финализатора будет вызван только в момент явного вызова оператора delete
.
Методы
Методы не могут быть объявлены при объявлении структур, но структуры могут хранить указатели на функции
struct V2 |
Потомок может переопределить функцию
struct V2 |
(https://github.com/GaijinEntertainment/daScript/blob/master/examples/test/unit_tests/override.das)
daScript позволяет создать две перегрузки функции set (а не определять дополнительное имя set_v3
), принимающие V2 и V3, тогда можно переписать пример без использования дополнительного имени, с уточнением типа функции set перед кастом и последующим автоматическим приведением этого указателя к правильному типу, определённому в V2:
struct 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
:a = [[
0x29990d19b90; //указатель на rtti информацию
V3'__finalize/*V3'__finalize S<::V3>*/; //указатель на функцию-финализатор
1.000000000; //x
2.000000000; //y
V3`set/*V3`set S<::V3> Cf Cf*/; //указатель на виртуальную функцию set
3.000000000 //z
]]
Комментарии после имён функций — замангленное имя функции и её сигнатуры (аргументы и результаты).
Расположение в памяти серьёзно отличается от C++ — указатели на виртуальные функции хранятся не в отдельной таблице (vtable), а в каждом объекте класса в том порядке, в котором были объявлены функции. Это позволяет убрать один уровень индирекции при вызове функций (не нужно идти за адресом в виртуальную таблицу) и изменять адреса функций динамически для каждого отдельного объекта, но увеличивает расходы памяти на хранение указателей, и также может повлиять на выравнивание и padding между полями.
Тем не менее, для модификации порядка данных структур возможно написать собственный макрос, который будет хранить функции в самостоятельно сгенерированной таблице или выносить указатели на функции в конец структуры.
daScript macro — пример макроса перестановки порядка полей при определении структур
unsafe |
Существует также макрос [cpp_layout]
, который не меняет порядок членов класса/структуры, но добавляет дополнительное правило выравнивания, как делают С/C++ — в конце родительской структуры будет оставлено пространство для её выравнивания по максимальному выравниванию членов структуры — например, если добавить в конце V2 поле на 4 байта padding: uint8[4]
, то из-за выравнивания структуры в 8 байт (из-за указателей на 64-битной платформе), поле z, будет добавлено с отступом в 8 байт (без макроса daScript без проблем “встраивает” это поле сразу за 4-байтным отступом).
Переопределение метода в экземпляре класса
Как было замеченно выше, каждый экземпляр класса/структуры хранит собственные копии указателей на функции, так что можно переопределить метод не на уровне класса-потомка, а в экземпляре класса (в пост-инициализаторе, или в любой момент после создания):class V2
x : float
y : float
def set(X, Y: float)
x = X
y = Y
[export]
def main
let fn <- @@ <| ( a : int )
return a
unsafe
//inplace init syntax
var v_customset = [[ V2()
set <- @@ (var self : V2; X,Y : float) {
self.x = X * 100.0f;
self.y = Y * 100.0f;
}
]]
v_customset->set(1.0f, 2.0f)
print("{v_customset.x}, {v_customset.y}\n")
//Output: 100.000000000, 200.000000000
//reset after construction, pipe + block syntax
v_customset.set = @@ <| (var self : V2; X,Y : float)
self.x = X * 200.0f
self.y = Y * 200.0f
print("{v_customset.x}, {v_customset.y}\n")
//Output: 200.000000000, 400.000000000
(более практичное применение этого — паттерны типа event/callback)
RTTI
Пример вывода на экран информации о типе в runtime:
require rtti |
Доступна информация о названиях и типах полей, а также мета-информация (флаги класс/структура, выделена на стеке/хипе, аннотации и т.п.).
Для того, чтобы передать и распознать аннотации, необходимо включить опцию options rtti=true
(в противном случае, метаинформация о произвольных аннотациях выбрасывается после симуляции, линк). Пример:
options rtti=true |
Abstract и sealed-методы
Методы можно сделать абстрактыми, или закрытыми для переопределения
class Test |
Видимость
- Из модуля экспортируются функции с аннотацией [export]
options always_export_initializer=true
позволяет проставить аннотацию для всех инициализаторов на уровне модуля
private
для переменных и типов ограничивает их доступность из других модулей
//module1.das |
Приватными могут быть также поля и функции структур/классов
class MyClass |
Инициализаторы
Инициализатор для класса — это функция, у которой имя совпадает с именем класса. Так как классы — надстройки над структурами, и все варианты синтаксиса иницилизации действуют и для них, то нет никакой гарантии того, что инициализатор класса будет вызван.
class Test |
Интерфейсы
Библиотека interfaces
с помощью пары макросов позволяет реализовать паттерн интерфейса — классы, который содержит только абстрактные методы. Макрос implements
позволяет изобразить множественное наследование от интерфейсов.
[interface] |
Связь с C++ типами
https://github.com/GaijinEntertainment/daScript/blob/master/examples/tutorial/tutorial03.cpp#L15
Базовый пример прокидывания C++ класса в daScript. Похоже на другие скриптовые языки, создаётся класс-обёртка (ManagedStructureAnnotation
) над типом, которая позволяет привязать и настроить отображение полей и методов структуры на тип в daScript, а также переопределить группу методов, определяющих свойства этого типа в daScript.
struct Color { |
Более продвинутые примеры, также можно смотреть код модулей.
Наследование
Отнаследоваться от C++ типа нельзя (ну, или я не нашёл способа сделать такой тип).
Существует возможность передать в daScript связь родитель-потомок между C++-типами (пример), для upcast-приведения типов аргументов функций.
Пример организации связи между С++ и daScript-классами - tutorial04.
В примере связь организуется через класс BaseClassAdapter
, который наследуется одновременно от базового C++-класса и сгенерированного по das-коду C++-классу-адаптеру
options remove_unused_symbols = false |
TutorialBaseClass
- интерфейс между C++/daScript, который используется генератором C++-обёрток log_cpp_class_adapter
, на выходе получается примерно такой C++-код:
class TutorialBaseClass { |
Вызов:class BaseClassAdapter : public BaseClass, public TutorialBaseClass {
public:
// in the constructor we store pointer to the original class and context
// we also pass StructInfo of the daScript class to the generated class
BaseClassAdapter ( char * pClass, const StructInfo * info, Context * ctx )
: TutorialBaseClass(info), classPtr(pClass), context(ctx) { }
...
virtual float3 getPosition() override {
// we check if daScript class has 'get_position'
if ( auto fn = get_get_position(classPtr) ) {
// we invoke it, and return it's result
return invoke_get_position(context, fn, classPtr);
} else {
return float3(0.0f);
}
}
protected:
void * classPtr; // stored pointer to the daScript class
Класс не содержит особой магии, а просто хранит адреса daScript-функций и позволяет прозрачно для вызывающего C++-кода их вызывать и изменять.