Using RBXScriptSignals as keys in dictionarys

This is related to another post about using Vector3’s as dictionary keys, but seems to make less sense then the former post.

I don’t really know if this is a bug or is intentional, but RBXScriptSignal's (game.Changed for example) do not return consistent values between indexes. This doesn’t really make sense, as RBXScriptSignal's are not structure types like Vector3's or UDim's. You can observe this behavior by making a simple table and indexing the event twice:

local t = {}
t[game.Changed] = "test"
print(t[game.Changed]) --> nil, should be "test"

image

A quick fix I did for this was to use a metatable that manually invokes __eq to check if the signal is the same, but that is quite slow and should not be required. I also attempted using tostring, which does not work in this case as two different events could return the same value: (tostring(game.Changed) == tostring(game.Workspace.Changed) --> true)

local T = {}
setmetatable(T, 
{
     __index = function(t, k)
          for i,v in pairs(t) do
            if i == k then return v end
        end
     end
})

T[game.Changed] = "test"
print(T[game.Changed]) --"test", works

image

Is there any better way for me to do this?

7 Likes

I remember this, it caused issues with Lemur a while back.

@LPGhatguy what did you do to get around this in Roact?

1 Like

Why do you need to use events as keys?

1 Like

At least for instances, Roblox keeps an instance cache to prevent this exact issue from occurring. I would think the same concept can be used for this purpose also.

I’m trying to create a library to sandbox obfuscated/malicious scripts and allow me to see what specifically they call/etc. One of the features I was trying to add was the ability to hook signals and I ran into this issue when I was scripting the logic for this.

MSandbox:HookSignal(game:GetService("ScriptContext").Error, function(Orig, ...) --function never gets called as the table that stores these hooks does not match up with the real return values
    print(...)
    return Orig(...)
end)

The metatable solution works “good enough”, but for larger projects where I would want to do something similar would be a detriment for performance.

2 Likes

There’s this thread (pretty sure @einsteinK wrote it) on the old wiki about sandboxing code. Couldn’t you just add your logic to __index to detect whenever the player tries to access RbxScriptSignal.

1 Like

I (re)wrote most of the code, and part of the text, I think. There was a simple version available already.

Most sandboxes just return new (proxy) events, to copy the same behavior as roblox. Yes, a bit memory-inefficient with having to create a proxy every time, but shouldn’t matter too much.

For your API, you could change the method to MSandbox:HookSignal(instance, eventName, handler).

Also, you can use rawequal to see if two objects are actually the same, bypassing the __eq metafield. You’ll see that rawequal(game.Changed, game.Changed) returns false.

4 Likes

For anyone who’s unaware of what’s going on, a new object is created whenever game.Changed (or another similar index) is evaluated as the C++ boundary is crossed. Because of this:

print(rawequal(game.Changed, game.Changed)) --> false

local changed = game.Changed
print(rawequal(changed, changed)) --> true
5 Likes