All you need to know about Metatables and Metamethods

Metatables! Truly an interesting subject. Through my activity in different forums and discord servers, I have seen many people get confused, on what they are. Main reason being, not a super wide collection of resources exists explaining them, and in general the idea of metatables is different and unique for someone who got used to simple notation, such as loops, and if statments.

I. Metatables

There isn’t really a definition to what a metatable is. It’s just that, any table can have a metatable. Stick with that idea. (and in addition, many tables can share the same metatable, and a table can be its own metatable). At the end, a metatable is a normal table itself.

More formally, you can think of a metatable, as a normal table that holds configurations/settings for another table that let’s you change the behaviour of that other table. In short, metatables help add more functionalities to a table.

The idea behind metatables is, to make tables a more powerful object! To turn them, from simple data structures, with just a small collection of abilities:

  • Storing pairs of keys and values
  • Getting values back from keys
  • The # operator

into something, with way more tools in the shed, using those configurations:

  • All the normal abilities
  • Do arithmetic on them (division, addition, subtraction…)
  • Compare them
  • Call them like functions
  • tostring() them
  • And much more!


(Image inspired from @BenSBk’s image)

To set a metatable mt to a table t, you need to use setmetatable(), and can use getmetatable() to get a table’s metatable if you need it.

local t = {}
local mt = {}
setmetatable(t, mt) --mt is now t's metatable

print(getmetatable(t)) --returns mt, t's metatable

An alternative, since setmetatable() returns the table we set the metatable of, we can do

local t = setmetatable({}, {})
--where the first table is returned to be t, and second table is the metatable of t that we will fill with metamethods

II. Metamethods

Metamethods are the main source of a metatable’s powers. They are the “configurations” that I mentioned earlier. They are fields we put inside of a metatable. They are prefixed with a __ (like __index, __newindex ect.), and most commonly set to a function (and in some special cases, set to a table or a string, we will cover these cases).

We will start with the __index metamethod, which is one of the basic ones. __index can be set to a table, or a function. I’m gonna be covering the function first, because I think explaining the table part makes understanding other metamethods harder.
I’m gonna write a piece of code that might be hard to understand at first, but we’ll examine what’s happening, and break down what’s going on in order to understand.

local t = setmetatable({}, { 
    __index = function(table, key)
        return key.." does not exist"
    end
})
 
print(t.x)
Just to get rid of confusion for beginners

As I said, this is the same as

local t = {}
local mt = {
    __index = function(table, key)
        return key.." does not exist"
    end
}
setmetatable(t, mt)

And if this still looks weird, we’re basically putting a function inside of a table. __index is being set to a function. We can do something else like

local t = {}
local mt = {}
mt.__index = function(table, key)
     return key.." does not exist"
end
setmetatable(t, mt)

Normally, t.x would be nil thus it would print nil, because there is no key inside of t called x, x doesn’t exist. Although, this is not the case here, what would happen is, it would print x does not exit, which is the same message we are returning inside of the function.

What’s happening?

So, to keep it short: you can think of metamethods as events. An event fires when something happens, a metamethod invokes when something happens to the table its metatable is connected with.

The __index metamethod fires/invokes when you index a table t with a value x, where x doesn’t exist. (Doing t[x] or t.x, when there isn’t an x in t).

Just like events, when they fire they run the function they’re connected to, metamethods run the function they’re set to. Yet again, just like events, they give additional information through parameters that you can use, metamethods do that aswell.

The __index metamethod gives back the table you indexed first, and the key that you indexed with second, as parameters if you wanna call them like that. (in this case, the independant table and key parameters)

As well, metamethods can return values, just like in the example I gave, we returned a string saying that the key you tried to look for doesn’t exist.

After all that, we added a new power to our basic table, the ability to tell the user that a key didn’t exist, instead of just giving back nil. Not interesting, but cool.

And just like that, if you understood this, you’re able to use almost any metamethod. Just like events, understanding how events work in general, makes the rest easy. It’s just a matter of asking “when that metamethod fires”. The Roblox Dev wiki shows you all the metamethods Roblox has, describing when they fire. I am gonna be covering most of them, even though that’s not needed, you can do it yourself.

(image from wiki)

And If you’re interested, here is a list of all the metamethods the latest version of lua has (the version Roblox uses is 5.1, try printing _VERSION)

I recommend you explore the code I wrote more, to get a bigger picture of what’s going on. Let me remind you that you’re free to do whatever you want inside of that function, you don’t necessarily need to return something, or return something logical. Here is a good demonstration

local adresses = {mike = "manhattan", george = "france"}
local metatable = {
    __index = function(t, k)
        print(k.."'s adress isn't found in "..tostring(t)..", creating a place for it")
        t[k] = "N/A"
        return t[k]
    end
} 

setmetatable(adresses, metatable)

print(adresses.starmaq)
--prints that message with the additional info
--and creates a place for that new adress returning "N/A"
--it's good to point out this also works with numerical indices (adresses[1], [2] ect.)

Also, what about the special case where __index can be set to a table. Well that doesn’t involve any relation with events. When you set __index to a table, instead of running the function it’s set to when it fires, it looks for the key you’re looking for inside of the table __index is set to, this will happen if it doesn’t exist in t of course.

local t = {}
local mt = {__index = {x = 5}}
setmetatable(t, mt)

print(t.x) --actually prints 5

It checks if t contains x, if it doesn’t, checks if t has a metatable, it has one (mt), checks if mt has an __index metamethod, it does, checks if __index's table contains x, it does, return that. Basically, t and __index share the same keys. You’ll often see this facility used a lot, whenever you want to have a backup table.

Now for a more interesting metamethod, __newindex. __newindex fires when you try to create a new index that didn’t exist before. (Doing t[x] = value or t.x = value, where x didn’t exist before in t)

local t = setmetatable({x = 5, y = 7}, {
     __newindex = function(t, k, value) 
         print("This is read-only table")
     end
})

t.z = 8 --prints that message

As you can see, you can come up with a lot of ideas. I just made a read-only table (even though you can change already existing keys). __newindex gives back the table, the key, and the value you wanted to set as a third parameter. Also, __newindex stops you from setting the value. It doesn’t create the new value and run the function, it stops you from creating the function and runs the function.

III. Operator Overloading

Operator overloading is making an operator (+, -, * ect. ==, >, < ect.) compatible with more than just one datatype. Meaning, you could do num + num, what about doing string + string? Of course here we are interested in tables. Yeah! We can actually add, subtract, multiply or do any sort of arithmetic on them. Here are the metamethods responsible for operator overloading.


Of course we can put many different metamethods into one metatable, like this

local t = {}
local mt = {__add = function(v1, v2) end, __sub = function(v1, v2) end, 
__mul = function(v1, v2) end, __div = function(v1, v2) end}

setmetatable(t, mt)

Let’s just start with one then fill the rest.

local t = {46, 35, 38}
local mt = {
     __add = function(v1, v2)
         return #v1 + v2 
     end
}
setmetatable(t, mt)

print(t + 5) --actually prints 8!

Pretty amazing right? We can now add table.
As I said earlier, you can do whatever you want inside of the function, there isn’t something exact. Adding tables doesn’t really have a rule, it’s weird. I came up with my own way of doing it, by adding #t (#v1), how many elements are in t, with 5 (v2). You could’ve done something else, like looping through t (v1) and adding all of its elements (46, 35, 38) with 5 (v2).

Note, if I did

print(5 + t) --this would error

Order matters. In the first script, t is v1, and 5 is v2. In the second script, 5 is v1, t is v2, which means I’m doing #v1, thus #5, which would error. So you need to make a bunch of if statments to watch out from cases like this.

Now what about adding tables? Same thing really. But both tables needs to have the __add metamethod. You can’t add a table that has an __add with one that doesn’t.

local t1 = {"hi", true}
local t2 = {79, "bye", false}
local mt = {__add = function(v1, v2) return #v1 + #v2 end}

setmetatable(t1, mt)
setmetatable(t2, mt) --both need to have __add, you can see they can share the same metatable, of course thisbwould still work if they werebtwo seperate metatables with an __add

print(t1 + t2) --prints 5

Two things to point out, order matters here as well, and also if you’re wondering the metamethod will only invoke once and not twice. So yeah, you can do this with the other mathematical operations as well.

You can even concatenate (using the .. operator on strings) tables, using __concat.

local t1 = {"hi", true}
local t2 = {79, "bye", false}
local mt = {
    __concat = function(v1, v2)
        local output = {}
        for i, v in pairs(v1) do
            table.insert(output, v)
        end
        for i, v in pairs(v2) do
            table.insert(output, v)
        end
    end
    return output
}
setmetatable(t1, mt)
setmetatable(t2, mt) --they gotta have it both as well

local t3 = t1..t2 --we merged t1 and t2 together, as you can see you can get creative
print(unpack(t3)) --t3 contains all of t1 and t2's members

You also got __lt (less then), __le (less or equal to) and __eq (equal) which you can explore yourself. A __gt (greater then) and __ge (greater or equal to) or __ne (not equal to) don’t exist, but you can access them in a really weird way. As @incapaxx noted:

You also got __unm, which is basically the inverter operator, like doing -5, inverse of 5
you can do -table. For example you can invert all of the table’s elements.

IV. Proxy tables

In the II. section, I made a kind of read-only table, it wasn’t really one because you could still mutate (change) made indices, you just couldn’t make new ones. Proxy tables let you track down anything a user does on a table (and by anything I mean either indexing or creating new indices, and what other metamethods let you do). Basically, you need to two tables, the first one would be your original one, the one that contains your indices and keys. The second one would be the one that will have a metatable, that has a __index and _newindex. This second table would be empty, it’s what we will be using as a way to detect when a user wants to index or create a new index in our first table. If __index fires, that means he wanted to get an index from the first table, and we do something about it, if __newindex fires that means he wanted to create a new index, and we do something about it. More info.

Let me write an example (proxy means a connections, here we’re talking about a connection between the second table and first table, where the accesses and updates on the second table result into side effects on the first table, technically the metatable should be called the proxy, because it is doing the connection, but I ended up calling the second table that)

local t = {x = 5}
local proxy = setmetatable({}, {
__index = function(_, key)
    print("User indexed t with "..key)
    return t[key]
end,
__newindex = function(_, key, value)
   print("User made or update the field "..key.." with the value "..value.." in t")
   t[key] = value
end
})

print(proxy.x) --detects that we accessed x, and also prints 5
proxy.y = 6 --prints that we made or updated a field, and the field is added

We might even make this into a beautiful function

local function TrackDownTable()
  local t = {x = 5}
  local proxy = setmetatable({}, {
  __index = function(_, key)
      print("User indexed table with "..key)
      return t[key]
  end,
  __newindex = function(_, key, value)
   print("User made or update the field "..key.." with the value "..value.." in table")
   t[key] = value
  end
  })
  return proxy
end

local t = TrackDownTable()
t.x = 5 --prints
print(t.x) --prints

Let’s make a read-only table that! Basically we will just do nothing when __newindex is fired, and __index will return the value from the original table.

local function ReadOnlyTable()
  local t = {x = 5}
  local proxy = setmetatable({}, {
  __index = function(_, key)
      return t[key]
  end,
  __newindex = function(_, key, value)
   warn("This is a read-only table")
  end
  })
  return proxy
end

local t = TrackDownTable()
t.x = 5 --warns
print(t.x) --prints

V. Weak tables

In this section, we will be talking about __mode, a rather unique metamethod. We will be covering a feature that’s partially disabled in Roblox, if you’re interested.

Before covering weak tables and __mode, let’s talk about something else, garbage collection. Most language have a garabge collector (some don’t like C and C++), which is responsible for getting rid of unwanted and untracked data to prevent memory leaks. Basically, when a lua object (a table, a function, a thread (couroutines), userdata and strings) is overwritten or removed (by setting it to nil), it’s technically gone, but it’s not freed from the system’s memory, it’s still there, but it’s unreachable.

local t = {} 
t = nil --now t is unreachable

local str = "hi"
str = "bye" --now hi is lost, it's unreachable

For lua’s garbage collector, anything unreachable, meaning nothing no longer has a reference to it, is considered garbage, meaning it’s a target for the garbage collector, to collect it and get rid of that uneeded trash data. The lua garbage collector makes a cycle automatically every once in a while, all though you can manually call collectgarbage() to launch a garbage collceting cycle, which will get rid of unreachables. And this is exactly the feature that roblox disables, you can not force a garbage collection, calling collectgarbage() will do nothing, but in a normal lua compiler it would.

local t = {} 
t = nil

local str = "hi"
str = "bye" 

collectgarbage() --garbage cleared! {} and "hi" are freed

Note that, I chose a string and a table because those are lua objects that get collected, litterals like numbers and booleans don’t get garbage collected, because they don’t need to. Also, we can print collectgarbage("count") before and after the collectgarbage(), and you’ll see that the number decreased. This returns how much memory is used by lua in KB, and funny enough lua has this feature enabled. More info on garbage collection can be found here.

I think this is an opportunity to talk about memory leaks

You probably encountered this problem with :Destroy()

local part = Instance.new("Part")
part.Parent = workspace

part:Destroy()
print(part) --Part
--what??

This is because internally, Destroy() is just

function instance:Destroy()
    --do something to remove all event connections, and recursively destroy children
    self.Parent = nil --parent to nil
end

You can see that here we’re not setting any references to nil, setting self to nil is useless, because that’s a reference on its own, and after the function is done it’s garbage collected automatically, we still have another reference to the Part, which is the part variable at the start. There is at least one reference to the part, meaning it is not garbage collected (instances are userdata, so they are also garbage collected) and you can still reference it.

Now, let’s take a more complicated example

local val = {}
local t = {x = val}

val = nil

collectgarbage() --you'd expect {} to be collected

for i, v in pairs(t) do
    print(v) --prints the table
end

In this code, technically the table val contains is unreachable, we set val to nil, and garbagecollect()'d. Although it’s still not removed, not just from memory, but from the program itself, because it still exists inside of t, it’s printed in the pairs loop. Know why?
As I said, an object is considered garbage if it has 0 references, but that {} still has a reference, it’s the table containing it, it’s referenced by that, so it’s not considered garbage. That could be a problem. Here is where weak tables come in.

A weak table is a table containing weak references (either weak keys, or weak values, or both). If it’s a weak reference, it will not prevent the garbage collection cycle from collecting it, if it has no other reference then the weak table containing it.

__mode is responsible for making a table weak. It’s the special case that I mentioned at the beginning that can be set to a string. The string can either be “v”, meaning table has weak values, or “k”, meaning table has weak keys.

"v" will let the cycle collect the key/value pair if the value only has one reference and that reference is the containing table. Weak values.

"k" will let the cycle collect the key/value pair if the key only has one reference and that reference is the containing table. Weak keys.

"kv" will let the cycle collect the key/value pair if the key or the value have only one reference each and that reference is the containing table. Weak keys and values.

local val = {}
local t = {x = val}

local mt = {__mode = "v"}
setmetatable(t, mt)

val = nil --now {} only has one reference, which is t

collectgarbage() 

for i, v in pairs(t) do
    print(v) --doesn't print anything, {} and it's corresponding key x are removed!
end

I hope you understood how it works
What if you wanted weak keys? The key would need to be the {}

local val = {}
local t = {[val] = true}

local mt = {__mode = "k"}
setmetatable(t, mt)

val = nil --now {} only has one reference, which is t

collectgarbage() 

for i, v in pairs(t) do
    print(v) --doesn't print anything, {} and it's corresponding value true are removed!
end

Let me introduce an even more complicated example

local t1, t2, t3, t4 = {}, {}, {}, {} --4 strong references for all tables
local maintab = {t1, t2} -- strong references to t1 and t2
local weaktab = setmetatable({t1, t2, t3, t4}, {__mode = "v"}) --weak references for all tables

t1, t2, t3, t4 = nil, nil, nil, nil --no more strong references for all tables

print(#maintab, #weaktab) --2 4

collectgarbage() --t3 and t4 get collected

print(#maintab, #weaktab) --2 2

And just wanted to mention this since it has a relation with garbage collection, there is a __gc metamethod, which is supposed to invoke when a table is garbage collected (the table and not a weak key/value inside of it). Although this metamethod is disabled in roblox as well.

VI. Rawset, Rawget, Rawequal

rawset(), rawget() and rawequal() all have the same idea. To put it simply, they’re supposed to do something without invoking a certain metamethod.

rawset(t, x, v) sets a key x with the value v inside of t. If x didn’t exist before, where it would normally invoke __newindex if it was present, rawset() prevents __newindex from invoking.

rawget(t, x) will return the key x from table t. If x didn’t exist, rawget() prevents __index from invoking.

rawequal(t1, t2) compares if table t1 and t2 are equal without invoking __eq, this can be used to check if two tables are equal the normal way.

There are a lot of cases where you find yourself wanting to do one of these three actions but don’t wanna invoke a metamethod.
The wiki gives a really good example. Let’s say you had a table, and each time you indexed something that didn’t exist, you create it. The problem is, this table has a __newindex as well. Remember that __newindex stops you from setting a new value, it will not let you create that new value. In fact it will even cause an error, a C-Stack overflow, which happens when a function is called excessivly, it’s __index's function, being called a lot of times trying to set the value but __newindex is not letting it. We are not using __newindex on anything, we can technically remove it, but let’s just say we are going to use it for something else. What do we need to do? Well, use rawset(t, x, v) instead of doing t[x] = v, which will prevent __newindex from invoking.

local t = setmetatable({}, {
    __index = function(t, i)
        rawset(t, i, true) --there you go, just chose true as a placeholder value
        return t[i] 
   end,
   __newindex = function(t, i, v)

   end
})
print(t[1]) -- prints true

(code from wiki)

VII. Strings are Tables

Well not really, but, suprisingly, strings can have metatables as well! Kind of weird, but it’s logical I guess, considering that in binary, strings are just an array of characters. You don’t have to setmetatable() a string’s metatable, a string already has a metatable, you have to getmetatable() it. Really interesting in my opinion.

local str = "starmaq"
local mt = getmetatable(str)

Now, a problem if I print the metatable

print(mt)

It prints "The metatable is locked". And attempting to add any metamethod to it, will throw an error.

mt.__index = function() end

Well darn it, this is happening because of the __metatable metamethod. This metamethod prevents you from getting a table’s metatable, returning something else instead. Also this metamathod will throw an error if you try to setmetatable() another metatable.

local t = {}
local mt = {__metatable = function() return "This metatable is locked" end}

print(getmetatable(t)) --prints the message
setmetatable(t, {}) --errors

Which is sad, but outside of Roblox, in a normal lua compiler, you can actually get the metatable’s table, and add metamethods to it. So let’s just see what we can do with that. For example, in some languages like C and C++ you can index strings, meaning if you had a string str equal to "good", doing str[3] will give back d (in C arrays start at 0). In lua this isn’t a thing, you’d have to do string.sub(str, 4, 4), but with metatables, we can create a way to index strings.

local str = "starmaq"
local mt = getmetatable(str)
mt.__index = function(s, i) return string.sub(s, i, i) end

print(str[5]) --prints m, correct

What’s even crazier, all strings share the same metatable, meaning if I indexed any other string, it would as well have that functionality.

local str2 = "goodbye"
print(str2[6]) --y

And you can come up with a lot of create stuff to do.

There is also something else that can have a metatable, userdata. A userdata is an empty allocated piece of memory with a given size. Roblox developers don’t have access to create an empty userdata, because it involves a lot of lua C api (info on userdata if you’re interested) stuff which is obviously not accessible in roblox. Although, Roblox instances (parts, scripts ect.) and some built-in objects (CFrames, Vector3s ect.) are all userdata, and all have a metatable, although it’s locked.

local part = Instance.new("Part")
local cf = CFrame.new()
local v3 = Vector3.new()

print(getmetatable(part)) --"The metatable is locked"
print(getmetatable(cf)) --"The metatable is locked"
print(getmetatable(v3)) --"The metatable is locked"

For example, since Vector3s are userdata, and can be added (you can do Vector3.new() + Vector3.new()), or any operation can be applied on them, as well as comparing, that’s done using metamethods, added to the Vector3 userdata.

Although, you can create your own userdata, using newproxy(), which has almost no documentation online.

local userdata = newproxy()
print(getmetatable(userdata)) --nil

As I said, userdata has a metatable, how does this one not have one? In order to make the userdata have a metatable, you need to set its first parameter to true

local userdata = newproxy(true)
print(getmetatable(userdata)) --table: 0x5266c8363b5b0144, now it has one
--and you can add metamethods to it
local mt = getmetatable(userdata)
mt.__index = function() print("hi") end

Note that you can’t index userdata unless thay have a metatable with an __index, else they error. (Same for most metamethods). Which fires __index of course.

But still, userdata is useless, it’s just an empty raw piece of data unless you have access to lua c api, which we don’t. What can we use this for? The answer is in the name, newproxy. You can use this for proxy tables, but instead of the second table that has the metatable bound to it, you have a userdata instead. Why is that better? Because, that way your objects will be almost custom objects, doing type(object) will return userdata.

VIII. About exploiting

Exploiting has a big relation with metatables. This section will link between V and VI.

Often, I find people asking: “Is making an if statment checking if a player’s speed is big, if so kick him a good anti-speed exploit”.

if character.Humanoid.WalkSpeed > 16 then
     player:Kick("Yeet'd out of the universe")
end

The answer is no. Because exploiters can change what the WalkSpeed shows up as. His walkspeed can be 10000, but scripts view it as 16. How? Well, as I said, Roblox instances (humanoid in this case) have a metatable, using __index, the expoiter can check when a property is indexed (doing Humanoid.WalkSpeed for example) and if so return 16, instead of letting Roblox return the actual walkspeed. But also, I said Roblox instances’ metatable is locked, you can’t add metamethods to it, well, most exploits have the lua debug library in their terminal, which contains a function that can get a metatable without invoking __metatable, which is debug.getmetatable (note that roblox has access to lua debug library as well, but most of the good stuff is disabled), otherwise called getrawmetatable() (getrawmetatable, just like the other raw functions that does something without invoking any metamethod, note that this isn’t a lua thing, it’s something exploits implement). It might also be good to point out that a __setrawmetatable exists as well, it’s purpose is clear I assume.

--inside of the exploit's terminal
local obj = --wherever it is
local metatable = getrawmetatable(obj)
metatable.__index = function(_, k) return 16 end --haha idiots

obj.WalkSpeed = 1000
print(obj.WalkSpeed) --16

if obj.WalkSpeed > 16 then --this is useless now
     player:Kick("Yeet'd out of the universe")
end

Also doesn’t __index fire when a key doesn’t exist? Obviously the WalkSpeed property (which is a key inside of the userdata) exists? Well as I said

Note that you can’t index userdata unless thay have a metatable with an __index, else they error. (Same for most metamethods).

So indexing a userdata even if the index exists does fire __index.

Another important thing, metatables don’t replicate. Meaning if you had a part in the client, the exploiter edited the part’s metatable, the original part that was replicated from the server to the client won’t have its metatable edited. And properties like .WalkSpeed for example, don’t replicate as well, so checking if speed is high from server is useless. Which means, this checking is done from the client, and that’s also useless, the checking is wrong because the exploiter is changing the property, and even if it was right, the exploiter can remove the script in the first place.

And with this combination, you can trick local scripts, and sometimes it might ruin client sanity checks.


This is it! The end of a long article, I hope you gathered some new information (you definitely did). Just wanted to add one thing, the answer to the common question “what are metatables used for?”, and if you know what OOP is, you might say “what are metatables used for, besides OOP?”. As I’ve shown you through out the whole article, I made multiple things, like making a read-only table, creating a place for values that didn’t exist before, string indexing, let’s not forget operator overloading! We made it possible to add tables, we made them get merged together just like strings, in fact I’m currently working on a matrix module, where matrices are represented as tables (more exactly 2D arrays), and when I implemented matrix addition, I made a matrix.add() function, as well as just using the + operator to actually directly add matrices, which as I said are described as tables, I did this thanks to metatables! Heck, all Roblox userdatas (Vector3,CFrame,BrickColor…) are centered around metatables. Even instances. Basically the way instances work is whenever one is inserted, its metatable is set to an internal metatable which contains the properties and methods of that instance’s class. So, don’t say something is useless because you can’t think of something to use it for, what a good programmer should do, is just work on something, and when he gets stuck, he asks himself “what can I use to beat this obstacle”, and maybe metatables are the answer.

That’s it! As usual, have a wonderful day.

172 Likes

Great tutorial!

I love this tutorial. I personally know this information already, but for those who may not this is a good reference.

But as I read along, there were a few mistakes and oversights.

Now this was not necessarily a mistake, you probably just didn’t know about it.

No _gt for >, __ge for >=, or __ne for ~= metamethods exist, but a ~= b is the same as not (a == b), a < b is the same as not (a >= b), a <= b is the same as not (a > b) thus the need for extra metamethods is redundant.

Might also be a good idea to mention Roblox uses operator overloading all the time. This screenshot shows one example. (but just imo, I wouldn’t rely on it)

image


Additionally,

"kv" will make both the keys and values weak.


I wouldn’t say they’re the same, even if internally they are, that idea is meant to be abstracted away from the programmer. type("") ~= type({ }).


Might be good to note that getrawmetatable is not a Lua thing, but rather something exploiters use, and it just happens to look official. I’m pretty sure it does point to debug.getmetatable though, which is the function that bypasses __metatable. Additionally, debug.setmetatable exists, and doesn’t respect the __metatable field either (i.e. it sets a table’s metatable with no respect to __metatable), but ofc it is unusable in Roblox.


Finally, you forgot to return output :wink:

local mt = {
    __concat = function(v1, v2)
        local output = {}
        for i, v in pairs(v1) do
            table.insert(output, v)
        end
        for i, v in pairs(v2) do
            table.insert(output, v)
        end
+       return output
    end
}

Anyways that is all and I wish you luck with the tutorial!

24 Likes

Thank you for this. I always wondered how metatables worked, now I feel like my keyboard is 10x more powerful than it used to be!

I liked the use of events in describing the inner workings, that helped a lot in trying to understand what each metamethod does. I hope others who have had issues understand what metatables are and how they function will benefit from this tutorial and detailed explanation.

Also, about this…

Lol I love how you mention C and C++ a lot but it doesn’t have a garbage collector. I’m guessing it’s more meant to be “every language has a system to dereference and destroy untracked data” but regardless most languages these days have garbage collection built-in.

Btw,

C and C++ define it as an array of characters. It’s not just a binary representation under the hood for a lot of languages (looking at you, Java), but yeah I guess you’re right, Lua is kind of abstracted away. Kind of makes me sad, I’ve been wondering why I can’t index strings in Lua for the past 6 months. Now I know why - Roblox likes to hide metamethods :frowning:

But yeah thanks for the tutorial and thanks @incapaxx for clarifying some points and adding on to the already amazing post.

7 Likes

I was just reading the programming in lua manual (PiL) and was confused on the OOP chapter since metamethods and meta tables explanation were pretty brief. From that I knew they were really powerful tools but didn’t have a solid grasp of them up until I read this! Really, thanks for these easy to understand explanations :slight_smile:

3 Likes

Thank you a lot for the precious corrections! I find all your notes very reasonable and logical, and I will definitley apply these changes!

1 Like

Thank you! I’m glad you loved it!

1 Like

Excellent tutorial!

The last part about exploiting is very interesting to me, is there any way to prevent exploiters from changing metamethods?

3 Likes

Glad you liked it! Nope, sadly you can’t. Although, don’t worry, since if the metatable of an Instance is altered, that metamethod would be added to the local Instance, the one on the exploiter’s client, so there still wouldn’t be a problem.

If he added a metamethod to a part, it would just be added to the part in the client, and not the original part in the server that was replicated to the client.

2 Likes

Is the humanoid an exception because the client has network ownership over their character?

1 Like

Lol my dark days trying to learn metatables are over

3 Likes

Not totally sure, but network ownership of the humanoid shouldn’t affect replication, so no it’s not an exception.

1 Like

I am kinda confused on this part tho what do you mean by five

v2 is the parameter, in that script, an example of a parameter, I did 5.

By doing t + 5, is replacing #v1 + v2, the parameter v1 was t, and v2 was 5

so could we add more paramters liek print(t + 100 + 2)? so it looks for the paramater when you add

so hello = t + 5

print(hello) so basically this event fires whenever the table is added? But one more question what are the paramters for __Add is it just the table?

Ok my question is what are the paramters for the __Add methamethod, since when I try to add more paramters it doesnt work so like what are the paramters

The + operator only takes two operands. a + b + c would be the same as (a + b) + c but realistically order wouldn’t matter in math, but in the sense of overloading operators, order does matter.

2 Likes

yah but I mean like Im asking like what are the paramters of the methamethod __Add

As @incapaxx said, doing a + b + c, where a and b and c are all tables that have a metatable with an __add metamethod, it will calculate a + b first, then add that result to c. Same thing if they were different.

If a was a table with an __add, and b and c were int values, it will calculate a + b, then the result of that with c

The operands given to the + operator.

local mt = {
    __tostring = function(this)
        return string.format("%s, %s, %s", this.X, this.Y, this.Z)
    end,
    __add = function(lhs, rhs) -- left hand side, right hand side
        return Vector3.new(lhs.X + rhs.X, lhs.Y + rhs.Y, lhs.Z + rhs.Z)
    end
}

local Vector3 = {
    new = function(x, y, z)
        return setmetatable({ X = x, Y = y, Z = z }, mt)
    end
}

local v = Vector3.new(1, 2, 3)
local v2 = Vector3.new(4, 5, 6)
print(v + v2) -- 5, 7, 9
2 Likes

I don’t get why someone would lock metatables for strings and such.

1 Like