Структуры#
Структуры daScript, наследование#
https://dascript.org/doc/reference/language/structs.html#struct-declaration
struct V2
x : float = -1.0f
y : float
struct V3: V2
z = 3.0f
[export]
def main
let a : V3
print("a = {a}\n")
let b = V3()
print("b = {b}\n")
let c = [[V3 y = 2.0f]]
print("c = {c}\n")
let d = [[V3() y = 2.0f]]
print("d = {d}\n")
let e = [[V3() y = 2.0f, x = 1.0f]]
print("e = {e}\n")
//
let pd = new [[V3() y = 2.0f]]
print("pd = {pd}\n")
//Output:
a = [[ 0.000000000; 0.000000000; 0.000000000]]
b = [[ -1.000000000; 0.000000000; 3.000000000]]
c = [[ 0.000000000; 2.000000000; 0.000000000]]
d = [[ -1.000000000; 2.000000000; 3.000000000]]
e = [[ 1.000000000; 2.000000000; 3.000000000]]
pd = [[ -1.000000000; 2.000000000; 3.000000000]]
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]]
print("d = {d}\n")
delete d
print("d = {d}\n")
var pd = new [[V3() y = 2.0f]]
print("pd = {pd}\n")
unsafe { delete pd; }
print("pd = {pd}\n")
//Output
d = [[ -1.000000000; 2.000000000; 3.000000000]]
d = [[ 0.000000000; 0.000000000; 0.000000000]]
pd = [[ -1.000000000; 2.000000000; 3.000000000]]
pd = null
Финализатор по умолчанию для структуры зануляет память (в порядке от потомков к родителям, при необходимости зовёт финализаторы для членов структуры в порядке объявления). Для указателей — после зануления полей структуры дополнительно меняет адрес указателя на null. Финализаторы для структур и классов зовутся вручную. Финализаторы не освобождают память, на которую указывает объект.
Освобождение памяти#
Модель памяти по умолчанию в daScript не подразумевает очистки памяти в ходе выполнения скрипта, за очистку отвечает хост-приложение, которое может просто освободить всю память контекста целиком — пересоздание контекстов by design быстро и эффективно, так что такой способ предпочтительный.
Но можно настроить поведение контекста опцией persistent_heap
:
options persistent_heap = true
var pd = new [[V3() y = 2.0f]]
var pd2 = pd
unsafe
print("addr(pd) = {reinterpret<uint>(pd)}\n")
print("addr(pd2) = {reinterpret<uint>(pd2)}\n")
delete pd
pd2.x = 33.0f
print("pd2 = {pd2}\n")
//Output
addr(pd) = 0xf1909e10
addr(pd2) = 0xf1909e10
pd2 = [[ 33.000000000; -431602080.000000000; -431602080.000000000]] //мусор
Я включил опцию 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)
print("kill V2 {v}\n")
var d = [[ V3 x = 11.0f, y = 22.0f, z = 33.0f]]
delete d
print("d = {d}\n")
//Output
kill V2 [[ 11.000000000; 22.000000000]]
d = [[ 11.000000000; 22.000000000; 33.000000000]]
Вместо финализатора зануления по умолчанию вызывается функция, логгирующая поле. Также можно заметить неочевидную вещь (если думать о финализаторах как о деструкторах, но лучше не думать) — финализаторы не зовутся в порядке от потомков к предкам, а как работают как обычные функции, daScript нашёл подходящую функцию, принимающую тип V2, и не вызвал зануления также и у поля z - т.е. финализатор родительской структуры “подошёл” к дочерней.
Более похожий на порядок вызова деструкторов в C/C++ код
(ещё раз, финализаторы — это не деструкторы, они вызываются только при явном вызове оператора delete!)
def finalize(var v : V2 explicit)
print("kill V2 {v}\n")
def finalize(var v : V3 explicit)
finalize(cast<V2> v)
print("kill V3 {v}\n")
[export]
def main
var d = [[ V3 x = 11.0f, y = 22.0f, z = 33.0f]]
delete d
print("d = {d}\n")
//Output
kill V2 [[ 11.000000000; 22.000000000]]
kill V3 [[ 11.000000000; 22.000000000; 33.000000000]]
d = [[ 11.000000000; 22.000000000; 33.000000000]]
daScript не позволяет каст к дочерним типам explicit
-аргументов.
Вместо перегрузки finalize
можно перегрузить def operator delete(var v : V2 explicit)
— семантически более точно описывает, что для структур код финализатора будет вызван только в момент явного вызова оператора delete
.
Методы#
Методы не могут быть объявлены при объявлении структур, но структуры могут хранить указатели на функции
struct V2
x : float
y : float
set = @@set
var a = V2()
a |> set(1.0f, 2.0f) //call function via pipe syntax
invoke(a.set, a, 1.0f, 2.0f) // exactly same thing as above
a->set(1.0f, 2.0f) // this one can call something else, if overridden in derived class.
Потомок может переопределить функцию
struct V2
x : float
y : float
set = @@set
struct V3: V2
z : float
override set = cast<auto> @@set_v3
def set(var thisV: V2; X, Y: float)
with thisV
x = X
y = Y
def set_v3(var thisV: V3; X, Y: float)
set(cast<V2> thisV, X, Y)
with thisV
z = 3.0f
[export]
def main
var a = V3()
a |> set_v3(1.0f, 2.0f) //non virtual call
invoke(a.set, a, 1.0f, 2.0f) // exactly same thing as above
a->set(1.0f, 2.0f) // this one can call something else, if overridden in derived class.
(https://github.com/GaijinEntertainment/daScript/blob/master/examples/test/unit_tests/override.das)
daScript позволяет создать две перегрузки функции set (а не определять дополнительное имя set_v3
), принимающие V2 и V3, тогда можно переписать пример без использования дополнительного имени, с уточнением типа функции set перед кастом и последующим автоматическим приведением этого указателя к правильному типу, определённому в V2:
struct V2
x : float
y : float
set = @@<(var thisV: V2; X, Y: float):void> set
struct V3: V2
z : float
override set = cast<auto> @@<(var thisV: V3; X, Y: float):void> set //<------ cast
def set(var thisV: V2 explicit; X, Y: float)
with thisV
x = X
y = Y
def set(var thisV: V3; X, Y: float)
set(cast<V2> thisV, X, Y)
with thisV
z = 3.0f
[export]
def main
var a = V3()
a |> set(1.0f, 2.0f) //virtual call
invoke(a.set, a, 1.0f, 2.0f) // exactly same thing as above
a->set(1.0f, 2.0f)
print("{a}\n")
Здесь 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
x : float
y : float
def set(X, Y: float)
x = X
y = Y
class V3: V2
z : float
def override set(X, Y: float)
V2`set(self, X, Y)
z = 3.0f
[export]
def main
var a = new V3()
a->set(1.0f, 2.0f) //V3`set(*a, 1.0f, 2.0f)
print("a = {a}\n")
//Output
a = [[ 0x29990d19b90; V3'__finalize/*V3'__finalize S<::V3>*/; 1.000000000; 2.000000000; V3`set/*V3`set S<::V3> Cf Cf*/; 3.000000000]]
Можно заметить, что имя объявленной внутри класса-функции манглится с помощью префикса-имени класса (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
print(
"\nsizeof(a) = {typeinfo(sizeof type<V3>)}\n
offset __rtti = {typeinfo(offsetof<__rtti> type<V3>)} {reinterpret<uint> addr(a.__rtti)}
offset __finalize = {typeinfo(offsetof<__finalize > type<V3>)} {reinterpret<uint> addr(a.__finalize )}
offset x = {typeinfo(offsetof<x> type<V3>)} {reinterpret<uint> addr(a.x)}
offset y = {typeinfo(offsetof<y> type<V3>)} {reinterpret<uint> addr(a.y)}
offset set = {typeinfo(offsetof<set> type<V3>)} {reinterpret<uint> addr(a.set)}
offset z = {typeinfo(offsetof<z> type<V3>)} {reinterpret<uint> addr(a.z)}\n"
)
Output:
sizeof(a) = 40
offset __rtti = 0 0x4adb5f80
offset __finalize = 8 0x4adb5f88
offset x = 16 0x4adb5f90
offset y = 20 0x4adb5f94
offset set1 = 24 0x4adb5f98
offset z = 32 0x4adb5fa0
Существует также макрос [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
...
var a = new V3()
print("class_info(a): {class_info(a)}\n")
describeStructure(*class_info(a))
//Output:
//https://dascript.org/doc/stdlib/rtti.html?highlight=rtti#StructInfo
class_info(a): [[ 0x6; V3; ; (_class|heapGC); 0x28; 0x93b8d07b5cc8cee; 0xcf51414d2d20b41e]]
struct V3
__rtti : void?
__finalize : function<(V2):void>
x : float
y : float
set1 : function<(V2;float const;float const):void>
z : float
Доступна информация о названиях и типах полей, а также мета-информация (флаги класс/структура, выделена на стеке/хипе, аннотации и т.п.).
Для того, чтобы передать и распознать аннотации, необходимо включить опцию options rtti=true
(в противном случае, метаинформация о произвольных аннотациях выбрасывается после симуляции, линк). Пример:
options rtti=true
...
class V3: V2
[[test]] z : float
...
def describeStructure(sinfo)
var anyAnn = false
structure_for_each_annotation(sinfo) <| $(ann; annArgs)
let argT = join([{for arg in annArgs; "{arg.name}{describeValue(get_annotation_argument_value(arg))}"}],",")
print("[{ann.name}({argT})]\n")
print("struct {sinfo.name}\n")
for sfield in sinfo
if sfield.annotation_arguments != null
for arg in deref(sfield.annotation_arguments)
print("\t[[{arg.name}]] ")
describeVariable(sfield,"\t")
...
var a = new V3()
print("class_info(a): {class_info(a)}\n")
describeStructure(*class_info(a))
//Output:
struct V3
__rtti : void?
__finalize : function<(V2):void>
x : float
y : float
set1 : function<(V2;float const;float const):void>
[[test]] z : float //<-- аннотация test
Abstract и sealed-методы#
Методы можно сделать абстрактыми, или закрытыми для переопределения
class Test
def abstract setX(X: int): void //необходимо явно определить сигнатуру метода -- тип аргументов и результата
def sealed setY(Y: int) //метод нельзя переопределить в потомках
pass
Видимость#
- Из модуля экспортируются функции с аннотацией [export]
options always_export_initializer=true
позволяет проставить аннотацию для всех инициализаторов на уровне модуля
private
для переменных и типов ограничивает их доступность из других модулей
//module1.das
module module1
class private V1
w : float
class private V2
x : float
y : float
class public V3: V2
v1 : V1
z : float = 3.0
...
//main.das
require module1
//можно звать инициализацию полей, и пост-инициализацию для V3 (также открываются поля V2), но нельзя инициализировать явно поле приватного класса V1
var a = new [[V3() x=1.0f, y=2.0f, z=3.0f]]
Приватными могут быть также поля и функции структур/классов
class MyClass
private a : int
def private set_a(val:int)
a = val
def get_a
return a
def MyClass()
self->set_a(42)
[export]
def main
var f = new MyClass()
print("f.a = {f->get_a()}\n")
Инициализаторы#
Инициализатор для класса — это функция, у которой имя совпадает с именем класса. Так как классы — надстройки над структурами, и все варианты синтаксиса иницилизации действуют и для них, то нет никакой гарантии того, что инициализатор класса будет вызван.
class Test
i : float
def Test(I : float)
i = I
def main
var a <- new Test(33.0f) //initializer called
var b <- new Test() //initializer don't called
var c <- new [[Test() i = 2.0f]] //initializer don't called
Интерфейсы#
Библиотека interfaces
с помощью пары макросов позволяет реализовать паттерн интерфейса — классы, который содержит только абстрактные методы. Макрос implements
позволяет изобразить множественное наследование от интерфейсов.
[interface]
class ITick
def abstract tick (dt:float) : void
[interface]
class ILogger
def abstract log (message : string) : void
[implements(ITick), implements(ILogger)]
class Foo
...
Связь с C++-типами#
update
- Связь daScript классов с C++-классами - перенёс в отдельную заметку