Intro
Userdatas are one of the most unknown data types on Roblox and in Lua 5.1. There is little to no documentation about it and, if there is, it is terrible. This tutorial is meant to clear up any confusion about userdatas.
Prerequisites
- You must have a good knowledge of metatables and Luau (roblox lua)
Scripting Userdatas
Userdatas are created through a function called newproxy
.
local userdata = newproxy()
print(type(userdata)) -- "userdata"
print(typeof(userdata)) -- "userdata"
One of its parameters, addMetatable, is a bool. What this does is, just like in the parameter name, it adds a metatable to the userdata. This is the most important part of a userdata.
If you would like this functionality, you would do:
local userdata = newproxy(true)
To retrieve the metatable added to the userdata, we would use the getmetatable
function.
local userdata = newproxy(true)
local metatable = getmetatable(userdata)
Then, you would do what you would normally do with metatables.
For example:
local userdata = newproxy(true)
local metatable = getmetatable(userdata)
metatable.__index = {
oranges = 5,
}
print(userdata.oranges) -- shows 5 in the output
In a nutshell:
- Userdatas are created through a function called newproxy. You can add a metatable through the bool, addMetatable.
- If the addMetatable parameter is true, you can call
getmetatable
on the userdata. - You can add metamethods, like
__index
to the metatable retrieved fromgetmetatable
.
__len: a forgotten metamethod
I call __len
a forgotten metamethod because you can apply it to userdatas, however, you cannot apply it to normal tables.
The __len
metamethod is fired whenever #
is put before a userdata. For example:
local userdata = newproxy(true)
local metatable = getmetatable(userdata)
metatable.__len = function()
return 5
end
print(#userdata) -- it will print 5
NOTE
When using table.getn on the userdata, it will error because table.getn expects a table, specifically an array, not a userdata
In a nutshell:
- __len only works on userdatas
- __len is a metamethod that is called whenever
#
is used before a userdata
Applications
Although userdatas are unknown and undocumented, they can have some neat use-cases. These are not necessary for development.
DictionaryWrapper(dictionary dict)
A function that takes a dictionary and uses the __len
metamethod to determine how many values are in the table (dictionary).
local function DictionaryWrapper(dict)
local userdata = newproxy(true)
local metatable = getmetatable(userdata)
metatable.__index = dict
metatable.__newindex = dict
-- When using ':', the userdata is passed as the implicit 'self' parameter
function metatable.__len()
local length = 0
for _, _ in pairs(metatable.__index) do
length = length + 1 -- you can do length += 1 instead of this
end
return length
end
return userdata
end
local dict1 = DictionaryWrapper {
apples = 10,
oranges = 5,
pears = 50,
}
print(#dict1)
ReadOnlyTable(table tab)
A function that makes a table readonly. Even though you could achieve this with tables/metatables and metamethods, using userdatas solves edgecases you might not want in your code.
- You can call
rawset
andrawget
on the table, however you can’t call those functions on userdatas, meaning metamethods like__index
and__newindex
will be fired everytime you get a value or create a new value.
local function ReadOnlyTable(tab)
local userdata = newproxy(true)
local metatable = getmetatable(userdata)
metatable.__index = tab
metatable.__metatable = "LOCKED!"
return userdata
end
local t = ReadOnlyTable {
hi = 5,
devforum = 10,
people = "i don't know anymore",
}
t.thisIsIllegal = true
Symbol.named(string name)
(This is from Roblox CorePackages)
Symbols are used (in Roblox libraries like Roact) to represent markers (kind of like Enums).
For example (in Roact):
local ComponentLifecyclePhase = strict({
Init = Symbol.named("init"),
Render = Symbol.named("render"),
ShouldUpdate = Symbol.named("shouldUpdate"),
WillUpdate = Symbol.named("willUpdate"),
DidMount = Symbol.named("didMount"),
DidUpdate = Symbol.named("didUpdate"),
WillUnmount = Symbol.named("willUnmount"),
ReconcileChildren = Symbol.named("reconcileChildren"),
Idle = Symbol.named("idle"),
}, "ComponentLifecyclePhase")
Here is the source code
--[[
A 'Symbol' is an opaque marker type.
Symbols have the type 'userdata', but when printed to the console, the name
of the symbol is shown.
]]
local Symbol = {}
--[[
Creates a Symbol with the given name.
When printed or coerced to a string, the symbol will turn into the string
given as its name.
]]
function Symbol.named(name)
assert(type(name) == "string", "Symbols must be created using a string name!")
local self = newproxy(true)
local wrappedName = ("Symbol(%s)"):format(name)
getmetatable(self).__tostring = function()
return wrappedName
end
return self
end
return Symbol
Conclusion
Hopefully you learned more about userdatas and there is no confusion about the data type. Have a good day or night. Stay safe!