SUNC Closure Internals
Almost every defensive hook in Roblox scripting is built on three primitives: newcclosure, iscclosure (and its sibling islclosure), and checkcaller. They look unrelated at first glance, but they all answer the same question — "what kind of function is this, and who called it?" — which is the question every detection routine eventually asks.
C closures vs Lua closures
Luau has two function flavours. C closures are functions implemented in C and registered with the VM as callable values — print, type, table.insert, and every method on a Roblox instance. Lua closures are functions you wrote in script. They have different memory layouts, different calling conventions, and the VM can tell them apart cheaply.
Two SUNC functions surface that distinction directly:
iscclosure(fn: function): boolean
islclosure(fn: function): booleanbooleanprint(iscclosure(print)) --> true
print(islclosure(function() end)) --> trueDetection routines lean on this constantly. "Is game.HttpGet still a C closure?" is the quickest way to tell whether someone has replaced it with a Lua hook.
newcclosure: wearing a C closure disguise
newcclosure takes a Lua function and returns a wrapper that looks like a C closure to inspection. The wrapper itself is a real C closure registered with the VM; when called, it dispatches to your original Lua function. Outside the wrapper everything sees a C closure; inside it, your Lua code runs normally.
newcclosure(fn: (...any) -> ...any): (...any) -> ...any(...any) -> ...anylocal hidden = function(...) return ... end
local wrapped = newcclosure(hidden)
print(iscclosure(hidden)) --> false (still a Lua closure)
print(iscclosure(wrapped)) --> true (now looks like C)
print(wrapped(1, 2, 3)) --> 1 2 3The mechanics of newcclosure
Under the hood, newcclosure creates a fresh C closure with a single upvalue: the Lua function you handed it. When the C closure is called, it pushes its arguments onto the stack, invokes the upvalue, and returns whatever the upvalue returned. From the perspective of any inspection that asks "what type of closure is this?", the answer is C.
The thing this doesn't hide is the upvalue itself. Tools that walk a function's upvalue list — likedebug.getupvalues on the wrapper — can see the inner Lua closure and inspect it further. That's a deeper detection path that most games don't take, but it exists, and it's why "wrap with newcclosure" is necessary but not always sufficient.
checkcaller: who called this?
The third primitive is the one that quietly makes anti-detection tractable.
checkcaller(): booleanbooleanif checkcaller() then
-- We're the ones running this; safe to do executor-only things
else
-- Game code called us; pass through transparently
endThis is the lever that lets a hook be invisible to game probes. Detection routines run from game threads, so checkcaller() returns false during them. Your hook delegates to the original. The detection sees the original behaviour. Pass.
Conversely, when you call the hooked function from your own script, checkcaller() is true and your hook can do whatever modification it was built for. Game probes and your real usage take different code paths through the same function.
The canonical hook idiom
Combining all three primitives, the textbook robust hook looks like this:
local original
original = hookfunction(target, newcclosure(function(...)
if checkcaller() then
return doMyThing(original, ...)
end
return original(...)
end))hookfunctionreplacestargetin place.newcclosurewraps the hook so the replaced function still reports as a C closure.checkcallerguards the "do my thing" path so game-originated calls pass through untouched.- Delegating to the captured
originalmeans probes that check argument shape, return values, or error messages still see Roblox's real behaviour.
clonefunction: hand out copies, not references
One more closure primitive worth knowing. clonefunction returns a copy of a function — same implementation, different identity. Useful when you want to give game code a function that "looks like" the original but isn't the same reference, so identity checks on your side don't conflict with theirs.
clonefunction(fn: function): functionfunctionlocal copy = clonefunction(print)
print(copy == print) --> false
copy("hello") --> helloA common use case: you saved an original function pre-hook, and you want to expose the original behaviour from your script without revealing that you have the reference. Handing out clonefunction(original) gives callers something they can use without being able to compare it to the "real" original they might have stashed elsewhere.
Common mistakes
- Wrapping twice. Calling
newcclosure(newcclosure(fn))stacks two wrappers, each adding one level of indirection. Harmless in tests, ugly in production. The outer wrapper isn't necessary if the inner one already covers the case. - Forgetting checkcaller in hot paths. A
__namecallhook that doesn't early-exit oncheckcallerruns your logic on every method call in the game, including the engine's own. The performance hit is immediate, and the detection surface grows massively. - Trusting iscclosure on unknown functions.
iscclosure(unknown)tells you what the closure type currently is. A game might have wrapped its own function innewcclosurefor legitimate reasons.iscclosurealone isn't a tampering signal — change in identity over time is.
Wrap-up
The three primitives are small but they cover most of what a robust hook needs to be invisible. The mental model: closures are two-flavoured, newcclosure changes the visible flavour, and checkcaller tells you whose code is running right now. Wire those into a hook idiomatically and the detection routines covered in our namecall detections article all pass automatically.
Ather is the lead developer behind Atherhub. He's been writing Luau and Roblox tooling for the better part of a decade, with a focus on the messy interface between game-script internals and the platforms that host them. Have feedback on this article? Drop it in the Discord.