Userdatas, how to use them

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 from getmetatable.

__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 and rawget 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!

59 Likes

I would recommend adding a section explaining what userdata actually are in general, not just newproxy. Also explain the difference between userdata created by newproxy and C-side userdata.

9 Likes

Wow, I did’nt know you could use them for anything.

1 Like

Tbh I am unclear for what userdata is actually used for?

Edit: wait this is an old thtead? I am extremely sorry to boost it idk how it got boosted automatically though…

1 Like

Userdatas are for storing and managing arbitary values stored in C and representing them as an object in Lua. Vector3, CFrame etc is an example of a userdata. newproxy creates an empty userdata for presumably creating a proxy object.


You can share metatables of a proxy created using newproxy in vanilla Lua 5.1 using that argument so I wouldn’t say it’s a boolean.

local foo = newproxy(true)
local bar = newproxy(foo)
print(getmetatable(foo) == getmetatable(bar))

Vector3, CFrame, Color3, UDim2, UDim, Instance etc. They’re an example of userdata not created by newproxy.
Outside of Roblox, io.open(file) is an example of a userdata not created by newproxy.

Last time I checked the source code (lvm.c) of Lua 5.1, the length operator didn’t specifically checks for userdata. Just because it doesn’t work for tables or strings that doesn’t mean it won’t work for numbers, booleans, nils, threads, functions, (metatable set through debug.setmetatable) etc.

      case OP_LEN: {
        const TValue *rb = RB(i);
        switch (ttype(rb)) {
          case LUA_TTABLE: {
            setnvalue(ra, cast_num(luaH_getn(hvalue(rb))));
            break;
          }
          case LUA_TSTRING: {
            setnvalue(ra, cast_num(tsvalue(rb)->len));
            break;
          }
          default: {  /* try metamethod */
            Protect(
              if (!call_binTM(L, rb, luaO_nilobject, ra, TM_LEN))
                luaG_typeerror(L, rb, "get length of");
            )
          }
        }
        continue;
      }

The __len metamethod now works on tables for Lua 5.2+ so I wouldn’t call it a “forgotten metamethod”.

I wouldn’t call it undocumented when the PIL documented userdata: Programming in Lua : 28.1

17 Likes