Metatable __mode not working as expected

Suppose we have a model named Model in workspace. According to Lua documentation and Roblox Developer Wiki we know that it is possible to make a weak table with weak keys as follows:

local a = {}
setmetatable(a, {__mode = "k"})

Now we can assign something to it and print

a[5] = 10
wait(1) -- wait a while for the garbage collector to perform collection
print(a[5]) --> 10

We can also use references as keys

a[game.Workspace] = 1
wait(1)
print(a[game.Workspace]) --> 1

My question is: why doesn’t this work then

a[game.Workspace.Model] = 1
wait(1)
print(a[game.Workspace.Model]) --> nil

As you can see we get nil but the Model was not removed from the game, so that entry in the table should not be garbage collected.

I find this contradictory. This is what it says in the official documentation:

NOTE: References to Roblox instances are never weak. Tables that hold such references will never be garbage collected.

but I may also be wrong about this. I don’t know.

Here you get the nil because nobody is referring to Model (at least nobody within the scope of the script) and I suppose that containers like workspace are something else. If in your script you reference Model you will get the results you expect

local model = workspace.Model
a[workspace.Model] = 1
wait(1)
print(a[workspace.Model]) --> 1

If you remove the reference to Model you will get nil again

local model = workspace.Model
a[workspace.Model] = 1
model = nil
wait(1)
print(a[workspace.Model]) --> nil

By the way, numbers and strings are not considered objects so they will never be collected (that is, they do not serve as weak keys)

The documentation is incorrect in this case.


The userdata for an instance will be collected after it has no strong references, but instance properties do not count as references for the userdata. The reason game.Workspace wasn’t removed is because it has other references (like the global workspace and Workspace). game.Workspace.Model was removed from because it had no other references. Essentially, the userdata acts as a pointer to the instance, the userdata can be collected without the instance being collected, but the instance can’t be collected with the userdata not being collected.

2 Likes

then that entry from the table should not get removed but it is getting removed.

Yes that will work, but I thought my first example will give similiar results to this:

_G.test = {}
_G.test.test2 = {}

a = setmetatable({}, {__mode = "k"})
a[_G.test.test2] = 5
print(a[_G.test.test2]) --> 5
wait(5)
print(a[_G.test.test2]) --> 5

In this case _G.test.test2 is not a variable from the local scope but it works, and I thought game.Workspace.Model will work the same.

Do I understand correctly that

local a = game.Workspace.Model

is not actually a reference to the Instance but a reference to the Lua userdata which is sort of a proxy between Lua and the instance of our Model in the engine?

If so, do we receive a new instance of userdata class for the Model each time we reference it by game.Workspace.Model?

Yes, a will be a userdata which is a bridge between the instance and Lua.

Roblox ensures that an Instance will only have 1 userdata for it, when getting the instance again it will use the same userdata.

1 Like

Then that is strange, because if it is all the time the same userdata, then my weak table entry should not be garbage collected as this userdata has to be stored somewhere and referenced for a potential use.

It more looks like Roblox gives you a new userdata every time you reference the instance of a model, then it would explain why the garbage collector collects table[game.Workspace.Model] which is something like table[Userdata_of_the_Model].

From my tests it seems that you get new userdata each time you reference an instance, but if there is an existing userdata of an instance you get it instead of creating a new one.

The userdata isn’t referenced somewhere else, the userdata can be collected without the instance being collected. The userdata is only for Lua references. The instance doesn’t necessarily have a userdata for it, but if it does it will have only 1 userdata.

If it gave you a new userdata every time then it wouldn’t work with tables.

local t = {}
t[workspace.Baseplate] = 1
print(t[workspace.Baseplate]) --> 1
-- if it always created a new userdata, then it would print nil
-- BrickColor.Red() always creates a new userdata
t[BrickColor.Red()] = 1
print(t[BrickColor.Red()]) --> nil
-- you can test raw equality with rawequal
print(rawequal(workspace.Baseplate,workspace.Baseplate)) --> true
print(rawequal(BrickColor.Red(),BrickColor.Red())) --> false

Yeah as I said in my post before userdata of a model must be cached and GCed only if there are no references to it. That is why you get the same object in your example, but also my weak table is garbage collected if no references to the userdata exist.

_G is a global variable in Lua, so it’s not surprising that it works. The instances are not from Lua so they will not always be according to Lua logic.

The point operator applied to an instance is more like a Getter than a Lua point operator. I mean that when we have several instances with the same name (and path) we get some of them, we do not know which one. Then, as Getter, it returns the reference to the instance it found, which is not saved anywhere and as a weak key will be collected. And as Halalaluyafail3 mentioned before, the containers may have other references.

However, all this is pure speculation, because we are at the limits of the API, where the API itself is fuzzy or inexistent. Trying to decipher the architecture of the engine through experiments can be a really hard task.

you didn’t get why I put that example there, I wanted to show that variables from outside the script scope also work as weak keys whereas model instances didn’t work.

We have already solved the issue by recognizing that model instances are implemented in a more complex way in the engine.

The userdata reference is actually stored in some sort of cache in the game engine as our experiments have revealed, because

local a = game.Workspace.Model
local b = game.Workspace.Model
print(rawequal(a, b)) --> true

And the userdata which is referenced by variable a or b in this example seems to be garbage collected when all the references are removed.