Here’s a simplified version of the “SafeCall” module I made for my projects.
While it’s possible to Connect/Disconnect methods from events while firing to get a fresh traceback, this implementation is faster while also allowing tables to be passed without serialization. This is significantly faster than alternatives that create/destroy BindableEvents upon calling.
This module is a great alternative to pcall
if you just need to call something safely but don’t want to silence errors (hence “SafeCall”.)
local globalMethod;
local globalArgs = {}
local globalArgsLen = 0
local onFire = function()
globalMethod(unpack(globalArgs, 1, globalArgsLen))
end
local fireDepth = 0
-- This cache could use __mode = "v" or some other cleanup mechanism, but it isn't very significant.
local bindableEventCache = setmetatable({}, {__index = function(self, i)
local event = Instance.new("BindableEvent")
event.Event:Connect(onFire)
-- Could potentially add the same event to the cache 4-ish times before hitting problems.
self[i] = event
return event
end})
local SafeCall = function(f, ...)
-- Would be smart to assert that f is callable here to prevent mysterious errors in 'onFire'.
-- I removed that because my code referenced a debug-only module used for making that assertion.
globalMethod = f
local argsLen = select("#", ...)
for i = 1, argsLen do
globalArgs[i] = select(i, ...)
end
-- Clean up previous args during recursion
for i = argsLen + 1, globalArgsLen do
globalArgs[i] = nil
end
globalArgsLen = argsLen
-- Fire
fireDepth = fireDepth + 1
bindableEventCache[fireDepth]:Fire()
fireDepth = fireDepth - 1
-- Clean up
globalMethod = nil
for i = 1, globalArgsLen do
globalArgs[i] = nil
end
globalArgsLen = 0
end
Now let’s test the recursion limit:
local function test(depth)
if depth > 256 then
print("Done!")
else
print("Depth", depth)
SafeCall(test, depth + 1)
end
end
test(1)
As you can see, it errors after a depth of 200:
This can be solved by falling back to coroutine.wrap
if fireDepth
gets too high, but I have never hit the limitation.
EDIT:
This implementation can get pretty close to coroutine.wrap
in terms of performance.
Here are some rough performance results comparing SafeCall
with coroutine.wrap
, Nevermore’s fastSpawn
, and a SafeCall
variation that localizes BindableEvent.Fire
: