Warning: this resource delves into some pretty complex topics with Roblox.
Bindables can act strangely when you pass functions, or tables with metatables or functions. This guide aims to explain why this happens and what’s actually happening.
Luau VMs
Roblox isolates every script context into it’s own global state, in this resource I’m going to refer to them as ‘VM stacks’.
Edit Note
Most of this thread was written under the assumption that Roblox creates new VMs when it comes to script contexts, what’s actually happening is Roblox creates a global state for each context. I dont want to edit the entire thread so just go off this when I’m talking about VM stacks
Each of these stacks have an isolated memory, which means Luau itself cant fetch values from one stack and give it to another, this is the reason for bindables.
If we declared shared.Boop
in a script, but then tried to read it from the CommandBar, we’d get nil
, as Boop
does not exist in the command bar context.
Roblox does this for better security isolation, we dont want a regular script being able to mutate a CoreScript variable thats vital for the game to be running (not that Roblox should be using shared
anyway but whatever)
Bindables
The way you send data between stacks is with Bindables, this behaviour is intentional (and actually allows for some funny exploits in its own right).
If we want to send Boop
to the CommandBar, we’d first hook up a BindableFunction from a script, then invoke it from the CommandBar.
We can now read the value of Boop
!
The main meat of this post
Now its good an all knowing how bindables work, I’m pretty sure most people know about this behaviour, so whats this post actually about? We’re talking about how functions and tables behave when crossing VM stacks.
On their own, functions cannot be sent if the Bindable is being listened to from multiple contexts
- Attempt to load a function from a different Lua VM
This behaviour is only applied if you actually attempt to call the function, interestingly, if you keep the bindable isolated to the same context, you can call it normally.
Slightly different logic applies for tables, where it actually rebuilds the table, this will happen regardless of which context it’s being accessed from. This recreation will also strip metatables.
You will get the same function behaviour if it exists in a table where it errors if you try to load it cross-vm
local t1 = {
bar = print
}
local Event = Instance.new("BindableEvent")
Event.Event:Connect(function(t2)
print(t2.bar) --> function 0x00000000
print(rawequal(t1, t2)) --> false
end)
Event:Fire(t1)
Keeping tables the same
If you can ensure that your events are only going to stay in one memory stack, it may be better to use a custom signal library to implement your events, as you can ensure the table will stay the same
Now, why does all this happen? Simple: if we have isolated memory, its impossible to access a function from one memory sandbox that would have to cross into another memory sandbox, since to that VM stack. The only memory that exists is it’s own sandbox and such does not exist.
The Attempt to load a function from a different Lua VM
error is actually an error that warns you that a function cannot be read, if this safeguard did not exist, you could get very unintenional things happen.
There’s also a weird oddity of bindables where strings are recreated internally.
Side Effects of this restriction
SetCore requires you to pass BindableEvents
or BindableFunctions
through it instead of functions because the same cross-vm restriction applies here. The actual function call restriction is not isolated to bindables but exists in any piece of code that deals with going between VM stacks, again, tables are recreated and stripped before being sent over the stacks.