Originally Posted by
Frement
Yes, and I need a solution for that issue
There is simply undefined behaviour by accessing stack allocated objects.. You need a method of allocating on the heap while not destroying garbage collection. Your structure looks like a singly-linked-list but that isn't possible in Simba without hacks/tricks.
Problem:
Simba Code:
type
PTest = ^TTest;
TTest = record
__object: Integer;
__parent: PTest;
end;
Function TTest.Create(Obj: Integer): TTest;
Begin
Result.__object := Obj;
writeln('Constructed: ', Obj);
If (Obj <> 100) Then
Result.__parent := @(TTest.Create(100));
writeln('My parent is: ', Result.__Parent);
Writeln('Unwinding..');
End;
var
test: TTest;
begin
test := TTest.Create(50);
writeln();
writeln('Parent value: ', test.__parent^.__object);
writeln('Parent: ', test.__parent^);
writeln('Parent value: ', test.__parent^.__object);
end.
Progress Report:
Compiled successfully in 203 ms.
Constructed: 50
Constructed: 100
My parent is: nil
Unwinding..
My parent is: 0x911B850 ({__OBJECT = 100, __PARENT = nil})
Unwinding..
Parent value: 100
Parent: {__OBJECT = 100, __PARENT = nil ({__OBJECT = 543975790, __PARENT = 0x43450028})}
Parent value: 152156224 //UNDEFINED BEHAVIOUR..
Successfully executed.
In the above, Test.Create is called recursively. The first call is to create the current object that will be returned. If the parameter is NOT 100, then the stack is pushed down to save the return value. Finally, it calls itself again to allocate an object on the current stack. When the unwind section is hit, the stack unwinds/pops and the object created is destroyed. It should print nil.
When unwind is called again, the value of __parent should be undefined/random/nil because the pushed down stack is now popped and the result is returned. That result has nothing allocated to it. It isn't initialized to nil or 0 either..
Why does the object get destroyed during stack unwinding? Stack unwinding is how functions destroy local variables.. The memory is reclaimed upon stack unwinding. This is why we never have to "free" stack variables. This technique is known as setting up a stack-frame in assembly.
The trick to getting around this is to allocate memory on the heap. Memory on the heap lives until it is freed by the user manually.
There is ONLY ONE way to do that in Lape (dynamic arrays) and since there is garbage collection, even heap allocations get destroyed when their reference count reaches 0.
So how do we trick/hack this into leaving the memory alone? Well.. we simply allocate a large enough byte array and set is reference count to more than 1 so that it survives the garbage collection upon stack unwinding.. We cast this memory to our structure type and store what we want in it. When the stack unwinds once, the reference count will drop to 1. When the script terminates, that reference count will hit 0 and the heap will be de-allocated automatically. Rarely should you ever use a reference count greater than 2! If you do, you must free the heap allocation yourself!
With the above technique of keeping references at a max of 2, we don't leak memory AND we get to keep garbage collection without messing stuff up as well as surviving stack unwinding..
Solution:
Simba Code:
type
PTest = ^TTest;
TTest = record
__object: Integer;
__parent: PTest;
end;
Function HeapAlloc(Size, ReferenceCount: UInt32): Array Of Byte;
type
PUInt32 = ^UInt32;
Begin
SetLength(Result, Size);
(PUInt32(@Result[0]) - (sizeof(UInt32) * 2))^ := ReferenceCount;
End;
Procedure HeapFree(var Arr: Array Of Byte); //Only call if reference count > 2.
type
PUInt32 = ^UInt32;
Begin
(PUInt32(@Arr[0]) - (sizeof(UInt32) * 2))^ := 0;
SetLength(Arr, 0);
End;
Function TTest.Create(Obj: Integer): TTest;
var
P: ^Byte;
Begin
Result.__object := Obj;
writeln('Constructed: ', Obj);
If (Obj <> 100) Then
begin
P := @HeapAlloc(SizeOf(Self), 2)[0]; //Will only survive stack deallocation ONCE! 2 - 1..
PTest(P)^ := TTest.Create(100);
Result.__parent := PTest(P);
end;
writeln('My parent is: ', Result.__Parent);
Writeln('Unwinding..');
End;
type
PUInt32 = ^UInt32;
P = Array of Byte;
PP = ^P;
var
test: TTest;
begin
test := TTest.Create(50);
writeln();
writeln('Parent value: ', test.__parent^.__object);
writeln('Parent: ', test.__parent^);
writeln('Parent value: ', test.__parent^.__object);
writeln('Reference Count: ', (PUInt32(test.__parent) - (sizeof(Int32) * 2))^); //Should print 1 before script termination! Otherwise call HeapFree!
end.
Progress Report:
Compiled successfully in 719 ms.
Constructed: 50
Constructed: 100
My parent is: nil
Unwinding..
My parent is: 0x8EA9058 ({__OBJECT = 100, __PARENT = nil})
Unwinding..
Parent value: 100
Parent: {__OBJECT = 100, __PARENT = nil}
Parent value: 100
Reference Count: 1
Successfully executed.
Additional sweetness to the allocation and de-allocation routines.. Instead of returning array of bytes and having to cast, you can return an underlying opaque pointer to the memory:
Simba Code:
Function HeapAlloc(Size, ReferenceCount: UInt32): Pointer;
type
PUInt32 = ^UInt32;
var
Memory: Array Of Byte;
Begin
SetLength(Memory, Size);
(PUInt32(@Memory[0]) - (sizeof(UInt32) * 2))^ := ReferenceCount;
Result := @Memory[0];
End;
Procedure HeapFree(var Memory: Pointer); //Only call if ReferenceCount > 2.
type
PUInt32 = ^UInt32;
Ptr = Array Of Byte;
Begin
(PUInt32(@Ptr(Memory)[0]) - (sizeof(UInt32) * 2))^ := 0;
SetLength(Ptr(Memory), 0);
End;
Function TTest.Create(Obj: Integer): TTest;
var
Mem: Pointer;
Begin
Result.__object := Obj;
writeln('Constructed: ', Obj);
If (Obj <> 100) Then
begin
Mem := HeapAlloc(SizeOf(Self), 2);
PTest(Mem)^ := TTest.Create(100);
Result.__parent := PTest(Mem);
end;
writeln('My parent is: ', Result.__Parent);
Writeln('Unwinding..');
End;
NOTES: You might never ever actually have to call HeapFree. If you think about it, no matter what amount of reference counts an array has, it is always freed in Lape. That function is just there for safety and paranoia.