# cat ./posts/lua-51-c-api-functions-metatables-and-the-stack.md
Lua 5.1 C API — Functions, Metatables, and the Stack
Disclaimer
This document is provided for educational and research purposes only. The author is not responsible for any misuse of the information contained herein. Any actions taken by individuals using this information are solely their own responsibility. Use of this information for malicious purposes, unauthorized access, or any illegal activities is strictly prohibited and may violate local, state, federal, or international laws.
A Complete Guide to the Lua 5.1 C API
This guide covers everything you need to embed Lua 5.1 into a C++ application — from first principles through advanced patterns like closures, upvalues, coroutines, and the registry. All examples compile against the Lua 5.1 headers and are directly applicable to any environment embedding that version, including the WoW 3.3.5a client.
1. The Lua State
Everything in the Lua C API revolves around lua_State. It holds the entire runtime: the stack, all globals, loaded modules, garbage collector state, and the call stack. You can have multiple independent states in the same process — they share no data unless you explicitly pass it between them.
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
lua_State *L = luaL_newstate(); // allocate a new state
luaL_openlibs(L); // load standard libraries (io, math, string, etc.)
// ... use L ...
lua_close(L); // destroy state and free all memory
luaL_newstate uses the default malloc/realloc/free allocator. If you need a custom allocator (e.g. a pool allocator or a sandbox that limits memory usage) use lua_newstate instead:
static void *my_alloc(void *ud, void *ptr, size_t osize, size_t nsize)
{
(void)ud; (void)osize;
if (nsize == 0) { free(ptr); return NULL; }
return realloc(ptr, nsize);
}
lua_State *L = lua_newstate(my_alloc, NULL);
2. The Stack — In Depth
The stack is the only communication channel between C and Lua. Every value that crosses the boundary — arguments, return values, table keys, metamethod results — passes through it. Understanding it completely is the single most important thing in this guide.
Stack Layout
Index Value
1 first pushed value (lua_toXxx(L, 1))
2 second pushed value (lua_toXxx(L, 2))
3 third pushed value (lua_toXxx(L, 3))
-3 same as 3 (from top)
-2 same as 2 (from top)
-1 most recently pushed (lua_toXxx(L, -1))
Positive indices count from the bottom of the current call frame. Negative indices count from the top. Index 0 is never valid.
Stack Size
The default guaranteed stack space is LUA_MINSTACK (20 slots). Before pushing many values call lua_checkstack:
if (!lua_checkstack(L, 50))
luaL_error(L, "stack overflow"); // grows the stack or returns false
Inspecting the Stack
int top = lua_gettop(L); // number of values currently on the stack
// Print the full stack for debugging
static void dump_stack(lua_State *L)
{
int n = lua_gettop(L);
printf("--- stack (%d) ---\n", n);
for (int i = 1; i <= n; i++) {
int t = lua_type(L, i);
switch (t) {
case LUA_TNUMBER: printf("[%d] number %g\n", i, lua_tonumber(L, i)); break;
case LUA_TSTRING: printf("[%d] string '%s'\n",i, lua_tostring(L, i)); break;
case LUA_TBOOLEAN: printf("[%d] bool %s\n", i, lua_toboolean(L, i) ? "true" : "false"); break;
case LUA_TNIL: printf("[%d] nil\n", i); break;
default: printf("[%d] %s\n", i, lua_typename(L, t)); break;
}
}
printf("------------------\n");
}
Manipulating the Stack
lua_settop(L, 3); // set stack size to 3, truncating or padding with nil
lua_settop(L, 0); // clear the entire stack
lua_pop(L, n); // remove n values from the top (macro: lua_settop(L, -(n)-1))
lua_pushvalue(L, idx); // push a copy of the value at idx
lua_remove(L, idx); // remove value at idx, shift above values down
lua_insert(L, idx); // move top value to idx, shift above values up
lua_replace(L, idx); // move top value to idx without shifting, pop top
// Example: rotate top 3 values
// Before: [a][b][c]
lua_insert(L, -3);
// After: [c][a][b]
3. Types and Type Checking
Lua has eight basic types. Every stack slot has a type you can query.
lua_type(L, idx); // returns LUA_TNIL, LUA_TNUMBER, LUA_TBOOLEAN,
// LUA_TSTRING, LUA_TTABLE, LUA_TFUNCTION,
// LUA_TUSERDATA, LUA_TTHREAD, LUA_TLIGHTUSERDATA
lua_typename(L, type); // human-readable type name
lua_isnumber(L, idx); // 1 if number or string coercible to number
lua_isstring(L, idx); // 1 if string or number
lua_istable(L, idx);
lua_isfunction(L, idx);
lua_isuserdata(L, idx); // true for both full and light userdata
lua_isnil(L, idx);
lua_isnone(L, idx); // true if idx is out of range (no value at all)
lua_isnoneornil(L, idx); // true if nil or out of range
Safe Reads (luaL_ variants)
Always prefer the luaL_check* family inside registered functions. They throw a Lua error with a useful message if the type is wrong, instead of returning garbage.
// These throw on type mismatch — use inside lua_CFunction
double n = luaL_checknumber(L, 1);
int i = (int)luaL_checkinteger(L, 1);
const char *s = luaL_checkstring(L, 1);
int b = lua_toboolean(L, 1); // no check variant; any value is valid for bool
// Optional arguments with defaults
double opt = luaL_optnumber(L, 2, 0.0); // default 0.0 if arg 2 is absent
const char *str = luaL_optstring(L, 3, "default");
// Type assertion with custom error message
luaL_argcheck(L, n > 0, 1, "expected positive number");
// Check a specific type and get pointer (for userdata)
void *ptr = luaL_checkudata(L, 1, "MyType"); // errors if not MyType userdata
4. Pushing Values
lua_pushnil(L);
lua_pushboolean(L, 1); // push true
lua_pushnumber(L, 3.14);
lua_pushinteger(L, 42); // pushes as number in Lua 5.1
lua_pushstring(L, "hello"); // copies the string into Lua's heap
lua_pushlstring(L, buf, len); // push string with explicit length (can contain nulls)
lua_pushfstring(L, "val=%d", n); // printf-style, returns interned char*
lua_pushvalue(L, idx); // push a copy of an existing stack value
lua_pushlightuserdata(L, ptr); // push a raw void* (no GC, no metatable by default)
lua_pushcfunction(L, fn); // push a C function
lua_pushcclosure(L, fn, n); // push a C closure capturing n upvalues (see §8)
5. Reading Values
double n = lua_tonumber(L, idx);
lua_Integer i = lua_tointeger(L, idx);
int b = lua_toboolean(L, idx); // always succeeds; 0/nil = false, else true
const char *s = lua_tostring(L, idx); // returns NULL for non-strings; may coerce number
size_t l;
const char *s = lua_tolstring(L, idx, &l); // also returns length
void *p = lua_touserdata(L, idx);
lua_State *t = lua_tothread(L, idx);
void *p = lua_topointer(L, idx); // any value → unique void* (for identity)
lua_tostring interns the result inside Lua — the pointer is valid as long as the value stays on the stack. Copy it if you need to keep it after popping.
6. Registering Functions
The C Function Signature
Every function exposed to Lua must have this signature:
typedef int (*lua_CFunction)(lua_State *L);
Arguments arrive on the stack starting at index 1. The return value is the number of results you push before returning. You may push zero, one, or multiple return values.
static int l_greet(lua_State *L)
{
const char *name = luaL_checkstring(L, 1);
lua_pushfstring(L, "Hello, %s!", name);
return 1; // one return value
}
Registering as a Global
lua_register(L, "Greet", l_greet);
// equivalent to:
lua_pushcfunction(L, l_greet);
lua_setglobal(L, "Greet");
print(Greet("Jay")) -- Hello, Jay!
Registering a Library Table
Group related functions into a namespace using luaL_register:
static int l_vec_add(lua_State *L) { /* ... */ return 1; }
static int l_vec_dot(lua_State *L) { /* ... */ return 1; }
static int l_vec_len(lua_State *L) { /* ... */ return 1; }
static const luaL_Reg veclib[] = {
{ "Add", l_vec_add },
{ "Dot", l_vec_dot },
{ "Len", l_vec_len },
{ NULL, NULL } // sentinel — required
};
// Creates _G.Vec = { Add=..., Dot=..., Len=... }
luaL_register(L, "Vec", veclib);
lua_pop(L, 1); // luaL_register leaves the table on the stack
local v = Vec.Len(3, 4) -- 5
Registering Into an Existing Table
If the table already exists on the stack at index idx, pass NULL as the name:
lua_getglobal(L, "Vec"); // push existing Vec table
luaL_register(L, NULL, extra_funcs); // add to it
lua_pop(L, 1);
Multiple Return Values
static int l_minmax(lua_State *L)
{
double a = luaL_checknumber(L, 1);
double b = luaL_checknumber(L, 2);
lua_pushnumber(L, a < b ? a : b); // min
lua_pushnumber(L, a > b ? a : b); // max
return 2; // two return values
}
local lo, hi = MinMax(7, 3) -- lo=3, hi=7
Variadic Functions
static int l_sum(lua_State *L)
{
int n = lua_gettop(L); // number of arguments
double total = 0;
for (int i = 1; i <= n; i++)
total += luaL_checknumber(L, i);
lua_pushnumber(L, total);
return 1;
}
print(Sum(1, 2, 3, 4, 5)) -- 15
7. Variables — Globals, Locals, and Tables
Globals
// Write
lua_pushstring(L, "backend-development.net");
lua_setglobal(L, "Domain");
// Read
lua_getglobal(L, "Domain");
const char *d = lua_tostring(L, -1);
lua_pop(L, 1);
// Delete (set to nil)
lua_pushnil(L);
lua_setglobal(L, "Domain");
Executing a Lua Chunk That Sets Globals
luaL_dostring(L, "Version = '1.0.0'");
lua_getglobal(L, "Version");
printf("%s\n", lua_tostring(L, -1)); // 1.0.0
lua_pop(L, 1);
Table Fields
// Create a table
lua_newtable(L); // stack: [{}]
lua_pushstring(L, "Jay");
lua_setfield(L, -2, "author"); // t.author = "Jay"
lua_pushnumber(L, 2026);
lua_setfield(L, -2, "year"); // t.year = 2026
lua_pushboolean(L, 1);
lua_setfield(L, -2, "published"); // t.published = true
lua_setglobal(L, "Config"); // _G.Config = t
// Read fields back
lua_getglobal(L, "Config"); // stack: [t]
lua_getfield(L, -1, "author"); // stack: [t, "Jay"]
printf("author: %s\n", lua_tostring(L, -1));
lua_pop(L, 2);
Integer / Array Keys
lua_newtable(L);
// Lua arrays are 1-indexed by convention
const char *items[] = { "dagger", "staff", "bow" };
for (int i = 0; i < 3; i++) {
lua_pushstring(L, items[i]);
lua_rawseti(L, -2, i + 1); // t[i+1] = items[i]
}
lua_setglobal(L, "Weapons");
lua_rawseti/lua_rawgeti bypass metamethods — faster and safer for plain arrays.
Arbitrary Keys (rawset/rawget)
lua_newtable(L); // table at -1
lua_pushstring(L, "key"); // push key
lua_pushinteger(L, 100); // push value
lua_rawset(L, -3); // t["key"] = 100, pops key+value
lua_pushstring(L, "key");
lua_rawget(L, -2); // push t["key"], stack: [t, 100]
printf("%d\n", (int)lua_tointeger(L, -1));
lua_pop(L, 2);
Nested Tables
lua_newtable(L); // outer = {}
lua_newtable(L); // inner = {}
lua_pushstring(L, "127.0.0.1");
lua_setfield(L, -2, "host"); // inner.host = "127.0.0.1"
lua_pushinteger(L, 8000);
lua_setfield(L, -2, "port"); // inner.port = 8000
lua_setfield(L, -2, "server"); // outer.server = inner
lua_setglobal(L, "App");
// Read: App.server.host
lua_getglobal(L, "App");
lua_getfield(L, -1, "server");
lua_getfield(L, -1, "host");
printf("%s\n", lua_tostring(L, -1)); // 127.0.0.1
lua_pop(L, 3);
Iterating a Table
lua_getglobal(L, "Config"); // push table
lua_pushnil(L); // first key (nil starts the iteration)
while (lua_next(L, -2)) // pops key, pushes key+value
{
// stack: [table, key, value]
const char *k = lua_tostring(L, -2);
const char *v = lua_tostring(L, -1); // may be NULL if not string
printf("%s = %s\n", k ? k : "?", v ? v : "?");
lua_pop(L, 1); // pop value, leave key for next iteration
}
// stack: [table]
lua_pop(L, 1);
Never call lua_tostring on the key during iteration if the key might be a number — it will mutate the key in place and break lua_next. Use lua_type first and handle accordingly.
8. Closures and Upvalues
A closure is a C function that carries persistent private state — upvalues. Upvalues are initialised once when you push the closure and persist for the closure's lifetime.
// A counter that remembers its state between calls
static int l_counter(lua_State *L)
{
// Upvalue 1 is the current count
int count = (int)lua_tointeger(L, lua_upvalueindex(1));
count++;
lua_pushinteger(L, count);
lua_replace(L, lua_upvalueindex(1)); // update upvalue
lua_pushinteger(L, count);
return 1;
}
// Factory: creates a new independent counter
static int l_new_counter(lua_State *L)
{
lua_pushinteger(L, 0); // initial upvalue: count = 0
lua_pushcclosure(L, l_counter, 1); // 1 upvalue
return 1;
}
lua_register(L, "NewCounter", l_new_counter);
local c1 = NewCounter()
local c2 = NewCounter()
print(c1()) -- 1
print(c1()) -- 2
print(c2()) -- 1 (independent)
print(c1()) -- 3
Multiple Upvalues
static int l_adder(lua_State *L)
{
double base = lua_tonumber(L, lua_upvalueindex(1));
double step = lua_tonumber(L, lua_upvalueindex(2));
base += step;
lua_pushnumber(L, base);
lua_replace(L, lua_upvalueindex(1));
lua_pushnumber(L, base);
return 1;
}
static int l_make_adder(lua_State *L)
{
double start = luaL_checknumber(L, 1);
double step = luaL_checknumber(L, 2);
lua_pushnumber(L, start); // upvalue 1
lua_pushnumber(L, step); // upvalue 2
lua_pushcclosure(L, l_adder, 2);
return 1;
}
local f = MakeAdder(0, 5)
print(f()) -- 5
print(f()) -- 10
print(f()) -- 15
9. Metatables — All Metamethods
A metatable is a regular Lua table attached to another value (any userdata, or any specific table) that defines how Lua handles operations on it. Each key in the metatable is a metamethod name.
Complete Metamethod Reference
| Key | Triggered by | Notes |
|---|---|---|
__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 |
|
__gc |
Garbage collected | Userdata only in 5.1 |
Defining a Full Metatable
Let's build a Vec2 type with arithmetic, comparison, tostring, and methods:
struct Vec2 { float x, y; };
// Helper: get Vec2 pointer from stack with type check
static Vec2 *check_vec2(lua_State *L, int idx)
{
return (Vec2 *)luaL_checkudata(L, idx, "Vec2");
}
// Helper: push a new Vec2 onto the stack
static Vec2 *push_vec2(lua_State *L, float x, float y)
{
Vec2 *v = (Vec2 *)lua_newuserdata(L, sizeof(Vec2));
v->x = x; v->y = y;
luaL_getmetatable(L, "Vec2");
lua_setmetatable(L, -2);
return v;
}
// Constructor: Vec2.New(x, y)
static int Vec2_new(lua_State *L)
{
float x = (float)luaL_checknumber(L, 1);
float y = (float)luaL_checknumber(L, 2);
push_vec2(L, x, y);
return 1;
}
// __tostring
static int Vec2_tostring(lua_State *L)
{
Vec2 *v = check_vec2(L, 1);
lua_pushfstring(L, "Vec2(%.4f, %.4f)", v->x, v->y);
return 1;
}
// __add: v1 + v2
static int Vec2_add(lua_State *L)
{
Vec2 *a = check_vec2(L, 1);
Vec2 *b = check_vec2(L, 2);
push_vec2(L, a->x + b->x, a->y + b->y);
return 1;
}
// __sub: v1 - v2
static int Vec2_sub(lua_State *L)
{
Vec2 *a = check_vec2(L, 1);
Vec2 *b = check_vec2(L, 2);
push_vec2(L, a->x - b->x, a->y - b->y);
return 1;
}
// __mul: v * scalar or scalar * v
static int Vec2_mul(lua_State *L)
{
if (lua_isuserdata(L, 1)) {
Vec2 *v = check_vec2(L, 1);
float s = (float)luaL_checknumber(L, 2);
push_vec2(L, v->x * s, v->y * s);
} else {
float s = (float)luaL_checknumber(L, 1);
Vec2 *v = check_vec2(L, 2);
push_vec2(L, v->x * s, v->y * s);
}
return 1;
}
// __unm: -v
static int Vec2_unm(lua_State *L)
{
Vec2 *v = check_vec2(L, 1);
push_vec2(L, -v->x, -v->y);
return 1;
}
// __eq: v1 == v2
static int Vec2_eq(lua_State *L)
{
Vec2 *a = check_vec2(L, 1);
Vec2 *b = check_vec2(L, 2);
lua_pushboolean(L, a->x == b->x && a->y == b->y);
return 1;
}
// __len: #v (returns magnitude)
static int Vec2_len(lua_State *L)
{
Vec2 *v = check_vec2(L, 1);
lua_pushnumber(L, sqrtf(v->x * v->x + v->y * v->y));
return 1;
}
// __index for field access: v.x, v.y
static int Vec2_index(lua_State *L)
{
Vec2 *v = check_vec2(L, 1);
const char *k = luaL_checkstring(L, 2);
if (strcmp(k, "x") == 0) { lua_pushnumber(L, v->x); return 1; }
if (strcmp(k, "y") == 0) { lua_pushnumber(L, v->y); return 1; }
// Fall through to methods table
luaL_getmetatable(L, "Vec2");
lua_getfield(L, -1, "__methods");
lua_getfield(L, -1, k);
return 1;
}
// __newindex for field write: v.x = n
static int Vec2_newindex(lua_State *L)
{
Vec2 *v = check_vec2(L, 1);
const char *k = luaL_checkstring(L, 2);
float val = (float)luaL_checknumber(L, 3);
if (strcmp(k, "x") == 0) { v->x = val; return 0; }
if (strcmp(k, "y") == 0) { v->y = val; return 0; }
luaL_error(L, "Vec2: unknown field '%s'", k);
return 0;
}
// __gc
static int Vec2_gc(lua_State *L)
{
// Vec2 has no heap members — nothing to free
// If you had malloc'd internals, free them here
return 0;
}
// Methods: v:Dot(u), v:Normalize(), v:Clone()
static int Vec2_Dot(lua_State *L)
{
Vec2 *a = check_vec2(L, 1);
Vec2 *b = check_vec2(L, 2);
lua_pushnumber(L, a->x * b->x + a->y * b->y);
return 1;
}
static int Vec2_Normalize(lua_State *L)
{
Vec2 *v = check_vec2(L, 1);
float mag = sqrtf(v->x * v->x + v->y * v->y);
if (mag == 0) luaL_error(L, "Vec2:Normalize — zero length vector");
push_vec2(L, v->x / mag, v->y / mag);
return 1;
}
static int Vec2_Clone(lua_State *L)
{
Vec2 *v = check_vec2(L, 1);
push_vec2(L, v->x, v->y);
return 1;
}
static const luaL_Reg Vec2_meta[] = {
{ "__tostring", Vec2_tostring },
{ "__add", Vec2_add },
{ "__sub", Vec2_sub },
{ "__mul", Vec2_mul },
{ "__unm", Vec2_unm },
{ "__eq", Vec2_eq },
{ "__len", Vec2_len },
{ "__index", Vec2_index },
{ "__newindex", Vec2_newindex },
{ "__gc", Vec2_gc },
{ NULL, NULL }
};
static const luaL_Reg Vec2_methods[] = {
{ "Dot", Vec2_Dot },
{ "Normalize", Vec2_Normalize },
{ "Clone", Vec2_Clone },
{ NULL, NULL }
};
void register_Vec2(lua_State *L)
{
// Create "Vec2" metatable in the registry
luaL_newmetatable(L, "Vec2");
luaL_register(L, NULL, Vec2_meta);
// Build methods table and stash it inside the metatable
lua_newtable(L);
luaL_register(L, NULL, Vec2_methods);
lua_setfield(L, -2, "__methods");
lua_pop(L, 1); // pop metatable
// Create Vec2 namespace table with constructor
lua_newtable(L);
lua_pushcfunction(L, Vec2_new);
lua_setfield(L, -2, "New");
lua_setglobal(L, "Vec2");
}
local a = Vec2.New(3, 4)
local b = Vec2.New(1, 2)
print(a) -- Vec2(3.0000, 4.0000)
print(#a) -- 5 (magnitude)
print(a + b) -- Vec2(4.0000, 6.0000)
print(a - b) -- Vec2(2.0000, 2.0000)
print(a * 2) -- Vec2(6.0000, 8.0000)
print(-a) -- Vec2(-3.0000, -4.0000)
print(a == b) -- false
print(a:Dot(b)) -- 11
print(a:Normalize()) -- Vec2(0.6000, 0.8000)
a.x = 10
print(a.x) -- 10
10. The Registry
The registry is a special table accessible only from C, not from Lua scripts. It is the correct place to store values that need to persist beyond a single C function call — callbacks, shared state, C++ object pointers, etc.
// Store a value
lua_pushstring(L, "my value");
int ref = luaL_ref(L, LUA_REGISTRYINDEX); // pops value, returns integer key
// Retrieve it later
lua_rawgeti(L, LUA_REGISTRYINDEX, ref);
printf("%s\n", lua_tostring(L, -1));
lua_pop(L, 1);
// Free the reference (allows GC of the value)
luaL_unref(L, LUA_REGISTRYINDEX, ref);
Storing a C++ Pointer in the Registry
// Store a pointer to a C++ object so Lua callbacks can reach it
MyEngine *engine = new MyEngine();
lua_pushlightuserdata(L, (void *)engine);
lua_setfield(L, LUA_REGISTRYINDEX, "MyEngine");
// Retrieve it inside a callback
lua_getfield(L, LUA_REGISTRYINDEX, "MyEngine");
MyEngine *e = (MyEngine *)lua_touserdata(L, -1);
lua_pop(L, 1);
Storing a Lua Callback
// Let Lua register a callback
static int l_set_callback(lua_State *L)
{
luaL_checktype(L, 1, LUA_TFUNCTION);
lua_pushvalue(L, 1); // copy function to top
lua_setfield(L, LUA_REGISTRYINDEX, "OnEvent"); // store in registry
return 0;
}
// Fire the callback from C
void fire_event(lua_State *L, const char *event)
{
lua_getfield(L, LUA_REGISTRYINDEX, "OnEvent");
if (lua_isfunction(L, -1)) {
lua_pushstring(L, event);
if (lua_pcall(L, 1, 0, 0) != 0) {
fprintf(stderr, "callback error: %s\n", lua_tostring(L, -1));
lua_pop(L, 1);
}
} else {
lua_pop(L, 1); // pop nil
}
}
SetCallback(function(event)
print("Got event:", event)
end)
11. Calling Lua from C
lua_pcall — Safe Call
// Call a global function
lua_getglobal(L, "MyFunction"); // push function
lua_pushnumber(L, 10); // push arg 1
lua_pushstring(L, "hello"); // push arg 2
// lua_pcall(L, nargs, nresults, errfunc_idx)
// errfunc_idx = 0 means no error handler
if (lua_pcall(L, 2, 1, 0) != 0) {
fprintf(stderr, "error: %s\n", lua_tostring(L, -1));
lua_pop(L, 1);
} else {
double result = lua_tonumber(L, -1);
lua_pop(L, 1);
}
Error Handler (Traceback)
static int traceback_handler(lua_State *L)
{
const char *msg = lua_tostring(L, 1);
luaL_traceback(L, L, msg, 1); // push message + traceback
return 1;
}
// Push error handler before function and args
lua_pushcfunction(L, traceback_handler); // index = base
int base = lua_gettop(L);
lua_getglobal(L, "MyFunction");
lua_pushnumber(L, 42);
if (lua_pcall(L, 1, 1, base) != 0) {
fprintf(stderr, "%s\n", lua_tostring(L, -1));
lua_pop(L, 1);
}
lua_remove(L, base); // remove error handler
Calling a Method on an Object
// Equivalent of Lua: obj:Method(arg)
lua_getglobal(L, "obj"); // push obj
lua_getfield(L, -1, "Method"); // push obj.Method
lua_pushvalue(L, -2); // push obj again (self)
lua_pushnumber(L, 99); // push arg
lua_pcall(L, 2, 1, 0); // call Method(self, arg)
lua_remove(L, -2); // remove obj from stack
Loading and Running a File
if (luaL_loadfile(L, "script.lua") != 0 ||
lua_pcall(L, 0, LUA_MULTRET, 0) != 0)
{
fprintf(stderr, "%s\n", lua_tostring(L, -1));
lua_pop(L, 1);
}
Loading a String
const char *code = "return 1 + 2";
luaL_loadstring(L, code);
lua_pcall(L, 0, 1, 0);
printf("%d\n", (int)lua_tointeger(L, -1)); // 3
lua_pop(L, 1);
12. Light Userdata vs Full Userdata
Light Userdata
A raw void* pointer wrapped as a Lua value. No GC, no size, no per-instance metatable. Use it to pass C pointers cheaply when lifetime is managed entirely on the C side.
lua_pushlightuserdata(L, (void *)my_ptr);
lua_setglobal(L, "NativeHandle");
lua_getglobal(L, "NativeHandle");
void *p = lua_touserdata(L, -1);
lua_pop(L, 1);
Full Userdata
A GC-managed block of memory allocated by Lua. Has a size, can have a metatable, triggers __gc when collected. Use this for C++ objects you want Lua to own.
// Allocate 64 bytes of Lua-managed memory
void *buf = lua_newuserdata(L, 64);
memset(buf, 0, 64);
// Attach a metatable to control its behaviour
luaL_getmetatable(L, "MyType");
lua_setmetatable(L, -2);
The key difference: 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).
13. Environments and Sandboxing
Every function in Lua 5.1 has an environment — a table that acts as its global table. You can sandbox scripts by giving them a restricted environment.
static void run_sandboxed(lua_State *L, const char *code)
{
// Load the chunk
if (luaL_loadstring(L, code) != 0) {
fprintf(stderr, "load error: %s\n", lua_tostring(L, -1));
lua_pop(L, 1);
return;
}
// chunk is at -1
// Build a restricted environment
lua_newtable(L); // sandbox env
lua_getglobal(L, "print"); // allow print
lua_setfield(L, -2, "print");
lua_getglobal(L, "math"); // allow math
lua_setfield(L, -2, "math");
lua_getglobal(L, "string"); // allow string
lua_setfield(L, -2, "string");
// Attach env to chunk using setfenv
lua_setfenv(L, -2); // chunk's env = sandbox table
// Execute
if (lua_pcall(L, 0, 0, 0) != 0) {
fprintf(stderr, "runtime error: %s\n", lua_tostring(L, -1));
lua_pop(L, 1);
}
}
The sandboxed script can call print and access math/string, but cannot reach io, os, loadfile, or any globals you didn't explicitly allow.
14. Coroutines
Lua coroutines are cooperative threads within a single OS thread. From C you can create, resume, and yield them.
// Create a coroutine from a Lua function
lua_getglobal(L, "MyCoroutine");
lua_State *co = lua_newthread(L); // new coroutine thread
lua_pushvalue(L, -2); // copy function to new thread's stack
// Resume with arguments (first resume passes args to the function body)
lua_pushnumber(co, 10);
int status = lua_resume(co, 1); // 1 argument
while (status == LUA_YIELD) {
// Collect yielded values from co's stack
int n = lua_gettop(co);
for (int i = 1; i <= n; i++)
printf("yielded: %g\n", lua_tonumber(co, i));
lua_pop(co, n);
// Resume again (values passed here become returns of coroutine.yield())
lua_pushnumber(co, 99);
status = lua_resume(co, 1);
}
if (status != 0)
fprintf(stderr, "coroutine error: %s\n", lua_tostring(co, -1));
function MyCoroutine(start)
local x = start
while true do
local input = coroutine.yield(x) -- yield x, receive input
x = x + input
end
end
15. Error Handling Patterns
Inside a Registered Function
static int l_safe_sqrt(lua_State *L)
{
double n = luaL_checknumber(L, 1);
if (n < 0)
luaL_error(L, "sqrt of negative number (%f)", n);
lua_pushnumber(L, sqrt(n));
return 1;
}
luaL_error never returns — it longjmps out of the function back to the nearest lua_pcall. Never call it outside a lua_CFunction unless you're inside a lua_pcall context.
lua_cpcall — Protected C Call Without Stack Manipulation
static int protected_init(lua_State *L)
{
// Everything here runs inside a pcall context
MyEngine *e = (MyEngine *)lua_touserdata(L, 1);
e->init();
return 0;
}
lua_cpcall(L, protected_init, my_engine);
Custom Error Objects
static int l_throw_table(lua_State *L)
{
lua_newtable(L);
lua_pushinteger(L, 404);
lua_setfield(L, -2, "code");
lua_pushstring(L, "not found");
lua_setfield(L, -2, "message");
lua_error(L); // throw the table on top of the stack
return 0; // unreachable
}
local ok, err = pcall(ThrowTable)
if not ok then
print(err.code, err.message) -- 404 not found
end
16. Full Working Example
Putting it all together: a minimal C++ object exposed to Lua with a constructor, fields, methods, arithmetic, and GC.
#include <stdio.h>
#include <string.h>
#include <math.h>
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
struct Timer {
double elapsed;
double limit;
int fired;
};
static Timer *check_timer(lua_State *L) {
return (Timer *)luaL_checkudata(L, 1, "Timer");
}
static int Timer_new(lua_State *L) {
double limit = luaL_checknumber(L, 1);
Timer *t = (Timer *)lua_newuserdata(L, sizeof(Timer));
t->elapsed = 0; t->limit = limit; t->fired = 0;
luaL_getmetatable(L, "Timer");
lua_setmetatable(L, -2);
return 1;
}
static int Timer_tick(lua_State *L) {
Timer *t = check_timer(L);
double dt = luaL_checknumber(L, 2);
t->elapsed += dt;
if (!t->fired && t->elapsed >= t->limit) {
t->fired = 1;
lua_pushboolean(L, 1);
} else {
lua_pushboolean(L, 0);
}
return 1;
}
static int Timer_reset(lua_State *L) {
Timer *t = check_timer(L); t->elapsed = 0; t->fired = 0;
return 0;
}
static int Timer_index(lua_State *L) {
Timer *t = check_timer(L);
const char *k = luaL_checkstring(L, 2);
if (!strcmp(k, "elapsed")) { lua_pushnumber(L, t->elapsed); return 1; }
if (!strcmp(k, "limit")) { lua_pushnumber(L, t->limit); return 1; }
if (!strcmp(k, "fired")) { lua_pushboolean(L, t->fired); return 1; }
luaL_getmetatable(L, "Timer");
lua_getfield(L, -1, k);
return 1;
}
static int Timer_tostring(lua_State *L) {
Timer *t = check_timer(L);
lua_pushfstring(L, "Timer(%.2f/%.2f)", t->elapsed, t->limit);
return 1;
}
static const luaL_Reg Timer_reg[] = {
{ "__index", Timer_index },
{ "__tostring", Timer_tostring },
{ "Tick", Timer_tick },
{ "Reset", Timer_reset },
{ NULL, NULL }
};
int main(void)
{
lua_State *L = luaL_newstate();
luaL_openlibs(L);
luaL_newmetatable(L, "Timer");
luaL_register(L, NULL, Timer_reg);
lua_pop(L, 1);
lua_newtable(L);
lua_pushcfunction(L, Timer_new);
lua_setfield(L, -2, "New");
lua_setglobal(L, "Timer");
const char *script =
"local t = Timer.New(1.0)\n"
"for i = 1, 5 do\n"
" local fired = t:Tick(0.25)\n"
" print(t, 'fired=' .. tostring(fired))\n"
"end\n"
"t:Reset()\n"
"print('after reset:', t)\n";
if (luaL_dostring(L, script) != 0)
fprintf(stderr, "%s\n", lua_tostring(L, -1));
lua_close(L);
return 0;
}
Output:
Timer(0.25/1.00) fired=false
Timer(0.50/1.00) fired=false
Timer(0.75/1.00) fired=false
Timer(1.00/1.00) fired=true
Timer(1.25/1.00) fired=false
after reset: Timer(0.00/1.00)
Quick Reference
| Operation | API call |
|---|---|
| New state | luaL_newstate() |
| Close state | lua_close(L) |
| Stack size | lua_gettop(L) |
| Clear stack | lua_settop(L, 0) |
| Pop n | lua_pop(L, n) |
| Push nil | lua_pushnil(L) |
| Push bool | lua_pushboolean(L, b) |
| Push number | lua_pushnumber(L, n) |
| Push string | lua_pushstring(L, s) |
| Push C function | lua_pushcfunction(L, f) |
| Push closure | lua_pushcclosure(L, f, n) |
| Read number | lua_tonumber(L, idx) |
| Read string | lua_tostring(L, idx) |
| Read bool | lua_toboolean(L, idx) |
| Check number | luaL_checknumber(L, idx) |
| Check string | luaL_checkstring(L, idx) |
| Check userdata | luaL_checkudata(L, idx, name) |
| Get type | lua_type(L, idx) |
| Set global | lua_setglobal(L, name) |
| Get global | lua_getglobal(L, name) |
| New table | lua_newtable(L) |
| Set field | lua_setfield(L, idx, key) |
| Get field | lua_getfield(L, idx, key) |
| Raw set | lua_rawset(L, idx) |
| Raw get | lua_rawget(L, idx) |
| Array set | lua_rawseti(L, idx, n) |
| Array get | lua_rawgeti(L, idx, n) |
| Iterate table | lua_next(L, idx) |
| Register function | lua_register(L, name, fn) |
| Register library | luaL_register(L, libname, regs) |
| New metatable | luaL_newmetatable(L, name) |
| Get metatable | luaL_getmetatable(L, name) |
| New userdata | lua_newuserdata(L, size) |
| Store in registry | luaL_ref(L, LUA_REGISTRYINDEX) |
| Load from registry | lua_rawgeti(L, LUA_REGISTRYINDEX, ref) |
| Free registry ref | luaL_unref(L, LUA_REGISTRYINDEX, ref) |
| Safe call | lua_pcall(L, nargs, nret, errfn) |
| Load string | luaL_loadstring(L, code) |
| Load file | luaL_loadfile(L, path) |
| Throw error | luaL_error(L, fmt, ...) |
| Upvalue index | lua_upvalueindex(n) |
| New thread | lua_newthread(L) |
| Resume coroutine | lua_resume(co, nargs) |