Why does Destroy() not immediately erase parts from memory?

What Instance:Destroy() does is set and lock the Parent to nil, disconnects all connections, and calls Destroy() on all descendants recursively.

But what if Destroy() just immediately erased the object from memory, and set any existing references to the object to nil immediately?

An instance value in Lua is a handle to some underlying instance data. When you try to access such a value, the interpreter checks to see if the data still exists. If it doesn’t, the value is treated as if it was nil, even though actually it probably has a pointer to some invalid data. The Lua side of the game has no control over how these data are allocated, so it wouldn’t make sense to give them direct control of deallocating them, nor would it really be meaningful if you did.
Is there something that you believe is not working as a result of it being this way?

1 Like

This would actually cause a lot of major issues.

Here’s the first part that you need to understand - Instances are just userdatas with metamethods that proxy calls to the underlying C++ API. They’re created on the C side, and metatables written, then they are exposed to the Luau side. This is what you reference in scripts. For example, calling Instance:GetFullName() triggers __namecall, which calls, and returns the values from, the corresponding function in C++.

The next part - Lua’s nil. This is not truely nothing; the same way NULL in C++ is typically just 0. nil is Lua’s way of representing the absence of data, but it is still data in itself. The closest you will ever get to nothing is C++'s nullptr, which points to memory location 0x0 (followed by however many 0s… basically just 0). It’s important to note that trying to reference nullptr as data leads to undefined behaviour, and typically causes a segmentation fault. This is when code tries to access invalid memory, i.e. it’s been cleaned up. Segmentation faults usually cause a crash - note this.

Luau closures (functions with an environment) capture upvalues. These are values which the function references within it’s code. They are pushed to and popped from the Luau stack, along with their closure, and are accessible from both C++ and Luau. If an instance and it’s corresponding userdata get cleaned up, i.e. freed from memory, that Luau upvalue now points to nothing, which I’m not even sure Luau allows support for. This would result in numerous seg faults as various sections of C++ and Luau code now references nullptr, thinking it’s still valid data - resulting in crashes.

Even if all references got set to nil immediately, how would your code know? You’d need to add an absolute tonne of if instance then checks all over the place, and if you don’t it’d error. Not to mention, this would take a lot more memory as all references would need to be tracked.

Luau is a memory-safe language, meaning that memory gets automatically cleaned - this is garbage collection. When a value is no longer referenced anywhere, only then will it be freed from memory. This removes the danger of segmentation faults and results in safe accessing of memory.

Hope this helps, any questions please ask!

TL;DR: would cause crashes and other issues (i cant really TLDR it more than that xd)

4 Likes

As a more concise answer, the Lua VM doesn’t support just ‘swapping’ references on the fly, so it wouldn’t be possible to change every instance reference to nil.

Now what could have been done was that destroying an instance could’ve changed the userdata so it would point to some sort of ‘nothing’ instance, but that would’ve opened up a whole new can of other issues which Roblox ultimately chose not to do.

1 Like

But are the references to an object already tracked anyway, for the purpose of garbage collection?

They are not tracked in the sense that every object knows what references point to it. Lua has classically used a mark and sweep collector which checks the list of objects to see which are still accessible from any location. For example if you have a local variable in an if block, it is impossible to access once that block is exited. There are ways it can “escape”, which all require making it accessible from another place. There have been some changes since, but they mostly concern performance, they don’t change the semantics:

More details:
Tracking where all references to data are is very expensive
Tracking just the number of references still tells you when you can safely collect them (The count can never go from 0 to 1 by the rules of the language), and is much cheaper (Python uses this)
Mark and sweep does not require any reference counting or tracking, but collection is delayed (Lua uses this)

1 Like