Lua 5.1 C API: One Reference Maps the Stack, Metatables and Registry
By Indie Kings | October 16, 2026
Updated October 16, 2026: A single reference published July 24, 2026 on backend-development.net maps the entire Lua 5.1 C API in one page: the shared stack, all 17 metamethods, the registry, coroutines and a complete working example. If you embed Lua 5.1 in C++ or poke at game clients built on it, this guide covers every call you will actually need, and we summarize it here section by section.
Image: Lua language logo. Credit: Wikimedia Commons.
Twenty Years Old and Still Everywhere
Lua 5.1 shipped in 2006 and its C API is still the scripting boundary where most game embedding happens. Per the guide's own framing, its examples compile against the Lua 5.1 headers and apply to any environment embedding that version, including the WoW 3.3.5a client from the Blizzard-era World of Warcraft. In general terms, and consistent with where the guide points with its gamedev and reverse-engineering tags, the private server cores that keep that client alive still embed Lua 5.1 for boss scripts and custom content, which is why a 5.1 API reference written today has a live audience.
The guide, titled "Lua 5.1 C API. Functions, Metatables, and the Stack" and dated July 24, 2026, is a practical, code heavy walkthrough. We count 164 separate code blocks in the page, each demonstrating one pattern, and it opens with an educational purposes only disclaimer. Per the guide, its value is scale: the stack model and how values cross the C to Lua boundary, every metamethod, the registry as the C side storage workaround, first class coroutines driven from C, and a full working example tying it together.
The Lua State and the One Shared Stack
Per the guide, everything revolves around lua_State, which holds the entire runtime: the stack, globals, loaded modules, garbage collector state and the call stack. Multiple independent states can live in one process and they share no data unless you pass it across explicitly. luaL_newstate() allocates a state with the default allocator, while lua_newstate() accepts a custom allocator, which the guide demonstrates with a pooled or memory limited sandbox setup.
The stack is the only communication channel between C and Lua. Every argument, return value, table key and metamethod result crosses through it, and the guide calls understanding it the single most important thing in the document. Positive indices count from the bottom of the current call frame, negative indices count from the top, and index 0 is never valid.
The default guaranteed stack space is 20 slots, the constant LUA_MINSTACK. Before pushing many values you call lua_checkstack to grow it, and the guide also covers lua_settop, lua_pop, lua_pushvalue, lua_remove, lua_insert and lua_replace for rearranging what is already there, plus a debug routine that prints the full stack contents slot by slot.
Types, Pushing, and Safe Reads
Lua has eight basic types, and every stack slot's type is queryable with lua_type, with lua_typename giving the human readable name. The lua_is* family tests for booleans, tables, functions, userdata and the special none or nil cases, where lua_isnone means the index is out of range and lua_isnoneornil covers both.
Reading values back is done with lua_tonumber, lua_tointeger, lua_toboolean, lua_tostring and friends. The guide flags two traps: lua_tostring returns NULL for non strings and may silently coerce a number, and the returned string pointer is only valid while the value stays on the stack, so copy it if you pop.
Inside registered functions the guide steers you to the luaL_check* family, which throws a Lua error with a useful message on a type mismatch instead of returning garbage: luaL_checknumber, luaL_checkinteger, luaL_checkstring and luaL_checkudata for typed userdata, plus luaL_optnumber and luaL_optstring for optional arguments with defaults and luaL_argcheck for custom condition errors.
Pushing covers the full set: lua_pushnil, lua_pushboolean, lua_pushnumber, lua_pushinteger (which pushes as a number in 5.1), lua_pushstring and lua_pushlstring for strings with embedded nulls, lua_pushfstring for printf style formatting, lua_pushlightuserdata for a raw void*, and lua_pushcfunction or lua_pushcclosure for C functions and closures.
Registering Functions, Globals, and Tables
Every function exposed to Lua must have the lua_CFunction signature: it takes one lua_State*, receives its arguments on the stack starting at index 1, and returns the number of result values it pushed. That makes multiple returns trivial (push two values, return 2) and variadic functions a matter of looping from 1 to lua_gettop(L) as the guide's Sum example shows.
lua_register exposes a function as a global, and luaL_register groups a list of luaL_Reg entries into a namespace table such as a Vec library with Add, Dot and Len; the guide notes the array must end with a NULL sentinel and that luaL_register leaves the table on the stack to pop. Passing NULL as the library name registers the functions into a table that already exists on the stack, which is how you extend an existing namespace.
The variables section walks the complete ladder: setting, reading and deleting globals with lua_setglobal and lua_getglobal; setting a global by executing a Lua chunk; building tables field by field with lua_setfield, including nested tables such as an App table with a server subtable holding a host and a port; filling array keys with lua_rawseti since Lua arrays are 1 indexed by convention; and arbitrary keys with lua_rawset and lua_rawget, which bypass metamethods and so are faster and safer for plain storage. Iteration runs lua_next in a loop pushed with a nil key, and the guide warns never to call lua_tostring on a key mid iteration, because it can mutate the key in place and break the loop.
Closures and Upvalues
A C closure is a C function that carries persistent private state. Upvalues are initialized once when you push the closure with lua_pushcclosure and persist for the closure's lifetime, accessible via lua_upvalueindex(n) and updatable with lua_replace. The guide builds a counter factory where each NewCounter() call returns an independent closure holding its own count, then a second example with two upvalues implements a running adder.
Per the guide this is the pattern for any C function that needs to remember state between calls without a global: the state lives with the closure, multiple instances stay independent, and the garbage collector owns the lifetime.
Metatables: The Full Metamethod Map
A metatable is a regular Lua table attached to userdata or a specific table that defines how Lua handles operations on it, and each key in it is a metamethod. The guide includes a complete reference table, reproduced here as it appears in the source:
| Metamethod key | Triggered by | Note from the guide |
|---|---|---|
| __index | t[k] when key absent | Can be table or function |
| __newindex | t[k] = v when key absent | Can be table or function |
| __call | t(args) | Makes value callable |
| __tostring | tostring(t) | Must return string |
| __len | #t | Return integer |
| __eq | t == u | Only called if both same type |
| __lt | t < u | |
| __le | t <= u | |
| __add | t + u | |
| __sub | t - u | |
| __mul | t * u | |
| __div | t / u | |
| __mod | t % u | |
| __pow | t ^ u | |
| __unm | -t | Unary minus |
| __concat | t .. u | Concatenation |
| __gc | Garbage collected | Userdata only in 5.1 |
The capstone of the section is a complete Vec2 type in C++: userdata, constructor, operator overloading for add, subtract, multiply, negation, equality and length, a custom __tostring, field reads and writes routed through __index and __newindex, methods table stashed inside the metatable, a __gc stub, and a Lua script exercising all of it with dot product and normalization calls.
The Registry: C Only Storage
The registry is a special table accessible only from C, never from Lua scripts, and per the guide it is the correct place for values that must persist beyond a single C function call: callbacks, shared state and C++ object pointers. luaL_ref stores any value and returns an integer reference, lua_rawgeti fetches it back, and luaL_unref frees it so the collector can reclaim the value.
The guide gives two storage recipes: keep a pointer to a C++ engine object under a named field so callbacks can reach it, and register a Lua callback by copying a function value into the registry, then firing it later from C through lua_pcall with error handling around the call.
Calling Lua from C
lua_pcall(lua_State*, nargs, nresults, errfunc_idx) is the safe call: push the function, push arguments, call, and check the return code with the error message on top of the stack on failure. Passing an error handler index gives you a traceback, which the guide implements as a small handler function that calls luaL_traceback; the handler is pushed before the function and args and removed afterward. Method calls replicate Lua's obj:Method(arg) by pushing the object, the method, the object again as self, then the argument. Loading runs through luaL_loadfile or luaL_loadstring followed by lua_pcall, with LUA_MULTRET for chunks that return multiple values.
Light vs Full Userdata
The guide's second data table distinction is the two userdata flavors, split as it presents them:
| Property | Light userdata | Full userdata |
|---|---|---|
| What it is | Raw void* wrapped as a value | GC managed memory block allocated by Lua |
| Memory owner | C side manages the lifetime | Lua allocated it and frees it when collected |
| Metatable | No per instance metatable | Can have a metatable |
| Garbage collection | No GC, no size | Has a size, triggers __gc when collected |
| Use case per the guide | Passing C pointers cheaply | C++ objects Lua should own |
The guide's summary sentence is worth repeating: with light userdata Lua holds a pointer to something you manage; with full userdata Lua allocated the memory and will free it when collected, after calling __gc if set.
Sandboxing, Coroutines, and Errors
Every function in Lua 5.1 has an environment, a table that acts as its global table, which is 5.1's sandboxing mechanism from C: load a chunk with luaL_loadstring, build a restricted environment table containing only what you allow (the guide's example allows print, math and string), attach it with lua_setfenv, and run. The sandboxed script can use what you granted and cannot reach io, os, loadfile or any global you did not explicitly allow.
Coroutines work first class from C: lua_newthread creates the coroutine, lua_resume drives it, and any values yielded surface on the coroutine's own stack, where the guide's loop reads them, pops them and resumes with new arguments every time the status comes back as LUA_YIELD.
Error handling follows three patterns. luaL_error throws from inside a registered function and never returns, because it longjmps to the nearest lua_pcall, so the guide cautions against calling it outside a C function context. lua_cpcall protects arbitrary C code without stack manipulation. And Lua errors can carry tables instead of strings: the guide throws a table with a code field and a message field and reads them back through pcall.
The Full Working Example and Quick Reference
The guide closes with a complete compilable C++ program: a Timer struct exposed to Lua through full userdata, with a constructor, fields routed through __index, Tick and Reset methods, custom __tostring and registration via a metatable plus a global namespace table. A Lua script attached to the example creates a timer at a 1.00 limit, ticks it five times at 0.25, fires exactly once at 1.00, and resets to 0.00, with the program's expected output shown line by line.
That string of outputs is more than a demo. It is the whole integration contract of an embedded object: allocation, construction, state mutation from the script side, a condition trigger, and text formatting, all in under 100 lines of C++ per the guide. A closing quick reference table maps roughly 44 operations to their API calls, from luaL_newstate() through lua_resume().
FAQ
Which Lua version does the guide cover?
Lua 5.1 only, per the guide's text. Its title, all 164 code blocks and its quick reference compile against Lua 5.1 headers, which it notes apply directly to any embedding of that version, including the WoW 3.3.5a client.
Why does Lua 5.1 still matter in 2026?
Because the game clients and server cores built around the Blizzard-era World of Warcraft 3.3.5a client still embed Lua 5.1 for scripting, and the guide's gamedev and reverse-engineering tags target exactly that audience. New Lua versions exist, but 5.1's API is what those environments expose.
What does the C side share with Lua?
One stack, per the guide. Arguments, returns, keys and metamethod results all cross through it, positive indices count from the bottom and negatives from the top, and index 0 is never valid.
How many metamethods does the guide document?
Seventeen, from __index through __gc, with __gc noted as userdata only in Lua 5.1 and __eq only firing when both operands share a type.
What is the registry used for?
Per the guide, it is the C only table for values that must outlive a single C function call, such as a stored Lua callback or a pointer to a C++ engine object, referenced by integer via luaL_ref.
Does the guide reproduce any code you need to copy?
Yes, though we do not republish it here. Everything demonstrates a working pattern C side, from a Sum variadic function and a counter closure factory to the full Timer program, and the page closes with an operations to API call quick reference table.
Bottom Line
Lua 5.1's C API is twenty years old and the WoW-custom-server scene still runs on it, and one July 2026 reference now maps every function, metamethod and stack trick it involves on a single page: 164 code blocks, a complete 17 entry metamethod table, registry patterns for callbacks and C++ pointers, C driven coroutines, 5.1 environment sandboxing, and a full working Timer example.
Our take: This is the guide we would hand to anyone touching a 3.3.5a codebase for the first time, and honestly to anyone embedding any Lua. Most stack explanations stop after lua_push and lua_pop; this one carries through to upvalue mutation, the registry, setfenv sandboxing and error objects, which is where real embedded code actually gets hard. The no code reproduced here constraint hurts it in prose form, and the 164 code blocks are the real product. Bookmark the source, not this summary.
Source: Lua 5.1 C API. Functions, Metatables, and the Stack, backend-development.net, July 24, 2026.