Welcome to hell, folks. This is a tutorial about how you can make your Roblox functions faster, somehow.
That’s right. If you are very concerned with performance like I do, look no further than possibly the most mind-boggling, confusing, and unnecessary tutorial you will ever lay your eyes on.
This tutorial is intended for people who like running headfirst into premature optimization and people who are very interested in how Roblox works under the hood.
How?
Paste this snippet. Don’t worry, it won’t complain if you’re in strict typechecking.
local rawRBXmemberGet
xpcall(function() return game[""] end, function() rawRBXmemberGet = debug.info(2, "f") end)
Now you have a function. Call it with any instance as the first argument and the string Name as the second argument and print it out.
print(rawRBXmemberGet(workspace, "Name"))
-- Output: Workspace
You just directly called the __index
metamethod of instances.
This also works with children, signals, and functions.
rawRBXmemberGet(workspace, "IsA")
rawRBXmemberGet(workspace, "Camera")
rawRBXmemberGet(workspace, "Changed")
Now for the next trick, copy this code and run it:
local rawRBXmemberSet
xpcall(function() return game[""] = nil end, function() rawRBXmemberSet = debug.info(2, "f") end)
You now have another function. Call it with any instance as the first argument, then specify and property on the next argument, then pass a compatible value on the third, then check the instance properties after running it.
rawRBXmemberSet(game.SoundService, "Archivable", false)
You have now directly changed an instance property.
For the last goodie, copy this code and run it:
local raycast = workspace.Raycast -- don't worry, its just to get the function
raycast(workspace, <your raycast arguments>) -- directly calls the function
You have successfully constructed a way to directly call workspace:Raycast()
.
Any other instance that either has or inherits the method can even be passed in, like so:
game.IsA(workspace, "DataModel") -- workspace:IsA
workspace.FindFirstChild(localPlayer, "Test", true) -- localPlayer:FindFirstChild
Why does it work?
Object-oriented programming in Luau depends on two key traits:
- Calling object functions with the colon operator is syntactic sugar for
obj.func(obj, ...)
- Retrieving a key within objects first checks if the key is in the object itself, then checks within the
__index
table/function if that fails.
Example:
local test = me.new() -- has an __index table of {test2 = function() blah blah end}
test.a = 8 -- creates a key in the object itself
test:test2() -- the object does not have test2, so it checks the index metatable instead
Object methods in the colon operator will always pass in the object and not the __index
metatable, so if you call :test2()
, it is syntactic sugar for test.test2(test)
.
Instances are type userdata
, so it can only rely on metatables for any work to be done. The __index
and __newindex
keys in their metatables are functions, and you can call them directly. We extract these functions with xpcall
and debug.info
as the stack trace is preserved in xpcall
. Both functions are universal and are the exact same functions for all other Instances, and they will by design work with all other Instances. This is also how some anti-cheats use this knowledge and check if the metatables of other Instances have been modified by an exploit, and even those same anti-cheats led me to this discovery.
Instance methods themselves, however, don’t work similarly. Getting the functions by dot operator will instead return a universal function that will work on all objects that inherit that function.
What are the use cases?
Practical use is extremely limited and niche, but a practical example is overhead reduction.
If you want to call multiple quick methods in one go, there is an unavoidable overhead as Luau goes through some code to make sure the method call works as intended. Direct calls can very slightly reduce the overhead, as Luau wouldn’t need to go this, there, and whatever. It may not be much on 10 calls, but this can scale tremendously with 40,000 calls and can greatly benefit if the fundamental design of the code cannot yield, in cases such as software rendering.
In most cases, you are completely fine with regular dot and colon operators, and I strongly discourage using direct calls unless you know what you are doing.