The Lua C API: Stack and State
What you will learn
- What
lua_Stateis and why every Lua execution context has its own state - How the virtual stack bridges C and Lua data exchange
- The difference between absolute, negative, and pseudo stack indices
- How to push values onto, query, and pop values from the Lua stack
- How to create tables and manipulate their fields from C
- How to allocate and use userdata to pass C data into Lua
- How to check types and extract values safely from C
- How to handle errors when calling Lua from C
Prerequisites: Familiarity with C (pointers, structs, function signatures) and basic Lua syntax.
Lua version: 5.4 (current stable)
Creating and destroying a Lua state
Why lua_state exists
Lua stores all execution state in an opaque pointer called lua_State. Every state has its own global environment (_G), garbage collector, registry, and call stack. Unlike many scripting languages, Lua has no global variables — every API function receives the state pointer as its first argument.
Think of lua_State as a completely isolated sandbox. Inside each sandbox lives its own set of Lua globals, its own memory manager, and its own execution context. You can have multiple sandboxes running in the same program at the same time, and nothing leaks between them.
This design means each thread can operate on its own lua_State without mutexes or locks. The independence is built into the API.
- Lua defines no global state — everything is in
lua_State* - Each state is independently garbage-collected and has its own
_Gtable - Thread safe by design: one
lua_Stateper thread
Process memory
├── Thread 1 ──► lua_State A ──► _G table A, stack A, gc A
├── Thread 2 ──► lua_State B ──► _G table B, stack B, gc B
└── Thread 3 ──► lua_State C ──► _G table C, stack C, gc C
Each lua_State pointer references a separate Lua world. Data never crosses between them unless you explicitly pass it through your own C code.
Lual_newstate and lua_close
To work with Lua from C, the first step is creating a state. The function luaL_newstate() does this with a single call, using the standard C allocator (malloc/free) internally. When you are done, call lua_close(L) to release all resources and run any pending __gc finalizers.
Always pair every luaL_newstate() with a corresponding lua_close(). Skipping the close call causes a memory leak because Lua allocates internal structures that are only freed when you explicitly close the state.
#include <stdio.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
int main(void) {
/* Create a new Lua state */
lua_State *L = luaL_newstate();
if (L == NULL) {
fprintf(stderr, "Failed to create Lua state\n");
return 1;
}
/* Open all standard libraries (base, table, string, math, etc.) */
luaL_openlibs(L);
printf("Lua state created successfully\n");
/* Clean up — always do this when finished */
lua_close(L);
printf("Lua state closed\n");
return 0;
}
Compile this with:
# Linux (Debian/Ubuntu) — Lua 5.4
gcc -o example example.c -llua5.4 -lm
# macOS (Homebrew)
gcc -o example example.c -llua -lm
The output confirms that the state was created and then cleanly destroyed.
The virtual stack
Why a stack?
When C code needs to exchange data with Lua, it does so through a virtual stack. This is not the actual C call stack — it is a separate data structure that Lua manages. Values go onto the stack from C, and Lua pulls them off when it runs code.
You might wonder why Lua does not just use a direct value type. A language like Python has a universal PyObject* that you can pass around directly. Lua deliberately avoids this. Instead of a lua_Value union, the API uses a stack. There are two key reasons:
-
No combinatorial explosion. If every value had to be a C type, you would need hundreds of function variants to handle every combination of types. The stack sidesteps this — operations always act on “whatever is on top.”
-
Garbage collector awareness. Because Lua values live on the stack (which Lua manages), the garbage collector always knows which values C code is currently holding a reference to. With direct value types, it would be easy to accidentally keep a reference that the GC does not know about.
Lua code only ever interacts with the top of the stack. C code can inspect or move any slot on the stack, not just the top.
- The stack is a virtual mechanism — not the actual C call stack
- It is the sole channel for passing values between C and Lua
- Lua’s GC tracks stack values automatically
C code Lua code
┌─────────────┐ ┌─────────────┐
│ C variable │ │ Lua globals │
│ int x │ │ │
└──────┬───────┘ └──────┬──────┘
│ ┌─────────────┐ │
└────────►│ Lua stack │◄─────────┘
│ [10]["hi"] │
└─────────────┘
Stack indexing
Absolute and negative indices
Every slot on the Lua stack has an index — a number you use in API calls to refer to that slot. Lua gives you two ways to index the same stack.
Absolute (positive) indices start at 1 at the bottom of the stack and grow upward. If the stack has 5 elements, they are at indices 1, 2, 3, 4, and 5. Index lua_gettop(L) always points to the top (the last element).
Negative indices count backward from the top. -1 is the top element (most recently pushed), -2 is the one below it, and so on. Negative indices are convenient when you want to say “give me the thing on top” or “give me the thing just below the top” regardless of how deep the stack is.
Stack contents (top at right):
index: 1 2 3 4 5
values: 10 "hi" true nil 3.14
neg: -5 -4 -3 -2 -1
lua_gettop(L) == 5
stack[-1] == stack[5] == 3.14
stack[-2] == nil
stack[1] == 10
Absolute indices are stable unless you insert or remove elements. Negative indices are relative — if you push a new element, the negative indices of existing elements shift.
Pseudo-indices
Pseudo-indices look like negative numbers but they do not refer to stack slots. Instead they point to special memory locations:
LUA_REGISTRYINDEX— accesses the registry table, a global table that C code can use to store its own data independently of Lua codelua_upvalueindex(i)— accesses upvalueiof the current C function
Pseudo-indices are read-only in the sense that you cannot push or pop them. They always refer to the same location.
Lua_absindex
Some API functions only accept absolute indices. If you have a negative index and need to pass it to one of those functions, convert it first with lua_absindex(L, idx):
/* Convert negative index -1 (top) to its absolute position */
/* If the stack has 3 elements, -1 becomes 3 */
int absolute_top = lua_absindex(L, -1);
printf("Absolute index of top: %d\n", absolute_top);
/* Negative index -2 becomes 2 */
int absolute_second = lua_absindex(L, -2);
printf("Absolute index of second-from-top: %d\n", absolute_second);
Use lua_absindex() whenever you store an index and need it to remain valid after push or pop operations.
Pushing values onto the stack
The API provides one push function per Lua type. All push functions return void — the stack grows automatically. Lua guarantees a minimum of 20 free slots at all times. If you need more space than that, call lua_checkstack() first.
| Function | Pushes |
|---|---|
lua_pushnil(L) | nil |
lua_pushnumber(L, double) | floating-point number |
lua_pushinteger(L, int) | integer (Lua 5.1+) |
lua_pushstring(L, char*) | zero-terminated string |
lua_pushlstring(L, char*, size_t) | string with explicit length (supports embedded \0) |
lua_pushboolean(L, int) | boolean (any non-zero becomes true) |
lua_pushvalue(L, index) | copy of the value at index |
lua_pushlstring() is the canonical function for pushing strings. Use it when the string might contain embedded null bytes or when you need explicit length control. lua_pushstring() is a convenience wrapper for zero-terminated strings only.
lua_State *L = luaL_newstate();
luaL_openlibs(L);
/* Push nil */
lua_pushnil(L); /* stack: [nil] */
/* Push numbers */
lua_pushnumber(L, 3.14); /* stack: [nil, 3.14] */
lua_pushinteger(L, 42); /* stack: [nil, 3.14, 42] */
/* Push a string (zero-terminated) */
lua_pushstring(L, "hello"); /* stack: [nil, 3.14, 42, "hello"] */
/* Push a string with embedded null bytes (binary data) */
const char binary[] = "hi\0there";
lua_pushlstring(L, binary, 8); /* stack: [nil, 3.14, 42, "hello", "hi\0there"] */
/* Push a boolean — any non-zero becomes true */
lua_pushboolean(L, 1); /* stack: [..., true] */
lua_pushboolean(L, 0); /* stack: [..., true, false] */
/* Push a copy of the value currently at stack index 1 (nil) */
lua_pushvalue(L, 1); /* stack: [..., true, false, nil] */
printf("Stack size: %d\n", lua_gettop(L)); /* 7 */
lua_close(L);
Each push grows the stack by one slot. The stack shrinks when you pop values.
Removing and moving stack elements
Lua_settop, lua_pop, lua_remove
The primary stack-truncation function is lua_settop(L, index). It sets the stack top to the given index. Setting the top to 0 clears the entire stack.
lua_pop(L, n) is a macro that expands to lua_settop(L, -(n)-1). It discards n elements from the top. Note that lua_pop() does not return the popped value — it simply removes them.
To remove an element at an arbitrary position, lua_remove(L, idx) shifts all elements above it down to fill the gap.
/* Build a stack: [10, "hi", true, nil, 3.14] */
lua_pushnumber(L, 10);
lua_pushstring(L, "hi");
lua_pushboolean(L, 1);
lua_pushnil(L);
lua_pushnumber(L, 3.14);
/* lua_settop(L, 0) clears the entire stack */
lua_settop(L, 0); /* stack: [] */
/* Build again: [10, "hi", true] */
lua_pushnumber(L, 10);
lua_pushstring(L, "hi");
lua_pushboolean(L, 1);
/* lua_pop(L, 2) removes 2 elements from top */
lua_pop(L, 2); /* stack: [10] */
/* Add more: [10, "hi", true, nil, 3.14] */
lua_pushstring(L, "hi");
lua_pushboolean(L, 1);
lua_pushnil(L);
lua_pushnumber(L, 3.14);
/* lua_remove(L, 3) removes the element at index 3 (true) */
/* Elements above it shift down: [10, "hi", nil, 3.14] */
lua_remove(L, 3); /* stack: [10, "hi", nil, 3.14] */
Lua_insert, lua_replace, lua_copy
lua_insert(L, idx)moves the top element into slotidx, shifting the element currently atidxand all above it upward by one position.lua_replace(L, idx)pops the top and writes it into slotidxwithout shifting any other elements. The old value atidxis discarded.lua_copy(L, from, to)copies a value from one slot to another without changing the stack size.
All three are implemented as macros built on top of lua_rotate() in Lua 5.4+.
/* Build: [10, 20, 30] */
lua_pushnumber(L, 10);
lua_pushnumber(L, 20);
lua_pushnumber(L, 30); /* stack: [10, 20, 30] */
/* lua_insert(L, 1) moves top (30) into slot 1, shifting others up */
/* Result: [30, 10, 20] */
lua_insert(L, 1);
/* lua_replace(L, 2) pops top (20) and writes it into slot 2 */
/* Result: [30, 20] — slot 2's old value (10) is discarded */
lua_replace(L, 2);
/* lua_copy(L, 1, 3) copies slot 1 (30) into slot 3 (currently empty) */
/* Lua pushes nil to fill the gap if needed */
/* Result: [30, 20, 30] */
lua_copy(L, 1, 3);
printf("Stack size: %d\n", lua_gettop(L)); /* 3 */
lua_close(L);
Querying the stack
Lua_gettop
lua_gettop(L) returns the number of elements currently on the stack. If the stack is empty, it returns 0. The returned value is also the index of the top element.
lua_State *L = luaL_newstate();
luaL_openlibs(L);
printf("Empty stack: %d elements\n", lua_gettop(L)); /* 0 */
lua_pushnumber(L, 42);
lua_pushstring(L, "answer");
printf("After pushing two values: %d elements\n", lua_gettop(L)); /* 2 */
lua_close(L);
Lua_checkstack
Before pushing many values, call lua_checkstack(L, n) to ensure at least n extra slots are available. It returns 1 on success or 0 if the request would exceed the maximum stack size.
/* Ensure there are at least 50 free slots before pushing a large batch */
if (!lua_checkstack(L, 50)) {
fprintf(stderr, "Cannot grow stack to 50 slots\n");
lua_close(L);
return 1;
}
/* Now it is safe to push up to 50 values */
for (int i = 0; i < 50; i++) {
lua_pushnumber(L, i);
}
printf("Stack has %d elements\n", lua_gettop(L)); /* 50 */
lua_close(L);
Lua grows the stack automatically up to its maximum (approximately INT_MAX/2 slots). lua_checkstack() lets you handle the rare case where that limit is a concern.
Type checking
Lua_type and lua_typename
lua_type(L, idx) returns a type code for the value at a given index. The code is an integer constant like LUA_TNIL, LUA_TNUMBER, or LUA_TSTRING. If the index refers to an empty slot beyond the top, it returns LUA_TNONE.
lua_typename(L, code) converts a type code back to a human-readable string.
lua_pushnil(L); /* index 1 */
lua_pushnumber(L, 3.14); /* index 2 */
lua_pushstring(L, "hi"); /* index 3 */
lua_pushboolean(L, 1); /* index 4 */
printf("Type at index 1: %s\n", lua_typename(L, lua_type(L, 1))); /* nil */
printf("Type at index 2: %s\n", lua_typename(L, lua_type(L, 2))); /* number */
printf("Type at index 3: %s\n", lua_typename(L, lua_type(L, 3))); /* string */
printf("Type at index 4: %s\n", lua_typename(L, lua_type(L, 4))); /* boolean */
printf("Type at index 5: %s\n", lua_typename(L, lua_type(L, 5))); /* no value */
lua_close(L);
Lua_is* functions
Convenience macros return 1 (true) if the value matches the checked type, 0 otherwise:
lua_isnil,lua_isnumber,lua_isinteger,lua_isstring,lua_isfunction,lua_istable,lua_isboolean,lua_iscfunction
Important gotcha: lua_isstring(L, idx) returns 1 for numbers too, because Lua can automatically convert numbers to strings. This is a common source of bugs.
/* Push a number at index 1, a numeric string at index 2 */
lua_pushnumber(L, 42); /* index 1: a number */
lua_pushstring(L, "42"); /* index 2: a string — but Lua coerces it to a number! */
printf("lua_isnumber(1): %d\n", lua_isnumber(L, 1)); /* 1 */
printf("lua_isstring(1): %d\n", lua_isstring(L, 1)); /* 1 — because 42 coerces to "42"! */
printf("lua_isstring(2): %d\n", lua_isstring(L, 2)); /* 1 — actual string */
lua_pop(L, 2);
lua_close(L);
For strict type checking, always compare lua_type(L, idx) directly against the LUA_T* constant:
/* Strict check: is this actually a string and not a number? */
if (lua_type(L, idx) == LUA_TSTRING) {
/* This branch only fires for real strings */
}
lua_isinteger(L, idx) (Lua 5.3+) is stricter than lua_isnumber. It returns true only if the value is an actual integer representation. A float like 3.14 passes lua_isnumber but fails lua_isinteger.
lua_pushnumber(L, 3.14); /* index 1 */
lua_pushinteger(L, 42); /* index 2 */
printf("lua_isnumber(1): %d\n", lua_isnumber(L, 1)); /* 1 */
printf("lua_isinteger(1): %d\n", lua_isinteger(L, 1)); /* 0 — 3.14 is not an integer */
printf("lua_isnumber(2): %d\n", lua_isnumber(L, 2)); /* 1 */
printf("lua_isinteger(2): %d\n", lua_isinteger(L, 2)); /* 1 */
lua_close(L);
Reading values from the stack
Lua_to* functions
Each lua_to* function extracts a C value from a stack slot. If the value cannot be converted to the requested C type, they return a default value: 0 for numbers, NULL for strings, and 0 for booleans. Always pair lua_to* with a type check unless you are completely certain of the type.
| Function | Returns |
|---|---|
lua_toboolean(L, idx) | int — 1 for true, 0 for false |
lua_tonumberx(L, idx, &isnum) | lua_Number (default double) |
lua_tointegerx(L, idx, &isnum) | lua_Integer (default 0) |
lua_tolstring(L, idx, &len) | const char* — pointer to internal copy |
The lua_tonumberx() and lua_tointegerx() variants accept an optional isnum pointer. If you pass a non-NULL pointer, the function sets it to 1 on success or 0 on failure. This lets you distinguish between a genuine zero and a conversion failure.
lua_pushnumber(L, 3.14);
lua_pushinteger(L, 42);
lua_pushboolean(L, 1);
/* Read the number */
lua_Number n = lua_tonumber(L, 1);
printf("Number at 1: %.2f\n", n); /* 3.14 */
/* Read the integer — check conversion success first */
int isnum;
lua_Integer i = lua_tointegerx(L, 2, &isnum);
if (isnum) {
printf("Integer at 2: %lld\n", (long long)i); /* 42 */
}
/* Read boolean — nil also returns 0, so check the type first */
if (lua_isboolean(L, 3)) {
int b = lua_toboolean(L, 3);
printf("Boolean at 3: %d\n", b); /* 1 */
}
lua_close(L);
String pointer lifetime
lua_tolstring() returns a pointer to an internal copy of the string that is valid only while the value remains on the stack. As soon as you pop the value, the pointer dangles. If you need the string after popping, push a copy first.
/* WRONG — pointer dangles after pop */
const char *s = lua_tolstring(L, -1, NULL);
lua_pop(L, 1); /* value popped, s is now dangling! */
printf("%s\n", s); /* undefined behavior — crash likely */
/* RIGHT — copy before popping */
lua_pushvalue(L, -1); /* duplicate the top so we own a copy */
const char *s = lua_tolstring(L, -2, NULL); /* read from the original slot */
lua_pop(L, 1); /* pop the original */
printf("String: %s\n", s); /* safe — s still points to the copy on stack */
lua_close(L);
The length output parameter is useful when strings contain embedded null bytes:
const char binary[] = "hello\0world";
lua_pushlstring(L, binary, 11); /* push 11 bytes including the null */
size_t len;
const char *s = lua_tolstring(L, -1, &len);
/* s points to "hello\0world" (11 bytes), s[11] == '\0' */
printf("Length (via len): %zu\n", len); /* 11 */
/* strlen(s) would return 5 because it stops at the first null */
lua_close(L);
Working with tables
Tables are the primary data structure in Lua, and the C API provides a dedicated family of functions for creating tables, reading their fields, and writing to them.
Creating tables
lua_createtable(L, narr, nrec) creates a new empty table and pushes it onto the stack. The narr parameter pre-allocates space for the array part (integer keys), and nrec pre-allocates space for the record part (string keys). If you do not know the sizes ahead of time, pass 0 for both.
lua_newtable(L) is a convenience macro that calls lua_createtable(L, 0, 0).
/* Create a table with pre-allocated space for 10 array slots and 5 record slots */
lua_createtable(L, 10, 5); /* stack: [{ empty table }] */
/* Create a table with no pre-allocation */
lua_newtable(L); /* stack: [{ empty table }] */
Getting and setting table fields
The two fundamental table operations are lua_gettable and lua_settable. Both use the stack-based key convention: you place the table somewhere on the stack, then use API calls that operate on it at a given index.
lua_gettable(L, idx)pops a key from the stack, looks upt[k]wheretis atidx, and pushes the result.lua_settable(L, idx)pops both a key and a value from the stack, then setst[k] = v.
/* Push a table: stack = [{ empty table }] */
lua_newtable(L);
/* Set t.name = "Alice" using lua_settable */
/* Key */
lua_pushstring(L, "name"); /* stack: [{ }, "name"] */
lua_pushstring(L, "Alice"); /* stack: [{ }, "name", "Alice"] */
lua_settable(L, -3); /* table at -3, pop key+value → stack: [{ }] */
/* Set t.age = 30 using lua_settable */
lua_pushstring(L, "age"); /* stack: [{ }, "age"] */
lua_pushinteger(L, 30); /* stack: [{ }, "age", 30] */
lua_settable(L, -3); /* → stack: [{ }] */
/* Get t.name — push the key, call lua_gettable */
lua_pushstring(L, "name"); /* stack: [{ }, "name"] */
lua_gettable(L, -2); /* look up, push result → stack: [{ }, "Alice"] */
/* The result is now on top — read it */
const char *name = lua_tostring(L, -1);
printf("name = %s\n", name); /* name = Alice */
/* Pop the result */
lua_pop(L, 1); /* stack: [{ }] */
Named field access
lua_getfield(L, idx, k) and lua_setfield(L, idx, k) are convenience functions that perform named field access without requiring you to push the key separately. They are equivalent to the lua_pushstring(L, k) + lua_gettable(L, idx) sequence.
/* Using lua_getfield / lua_setfield — cleaner for named fields */
/* Push a table */
lua_newtable(L); /* stack: [{ }] */
/* t.score = 95 */
lua_pushinteger(L, 95);
lua_setfield(L, -2, "score"); /* pops value, sets t.score → stack: [{ }] */
/* t.active = true */
lua_pushboolean(L, 1);
lua_setfield(L, -2, "active"); /* → stack: [{ }] */
/* Read t.score */
lua_getfield(L, -1, "score"); /* pushes t.score → stack: [{ }, 95] */
lua_Integer score = lua_tointeger(L, -1);
printf("score = %lld\n", (long long)score); /* score = 95 */
lua_pop(L, 1); /* stack: [{ }] */
Raw access (without metatables)
lua_rawget(L, idx) and lua_rawset(L, idx) behave like lua_gettable and lua_settable but they bypass the metatable lookup. Use raw access when you want to read or write directly to the table without invoking __index or __newindex metamethods.
/* Raw get — same key-on-stack convention as lua_gettable */
lua_newtable(L); /* stack: [{ }] */
lua_pushstring(L, "key");
lua_pushnumber(L, 99);
lua_rawset(L, -3); /* t.key = 99, no metatable involved → stack: [{ }] */
lua_pushstring(L, "key"); /* stack: [{ }, "key"] */
lua_rawget(L, -2); /* look up t.key directly → stack: [{ }, 99] */
printf("t.key = %.0f\n", lua_tonumber(L, -1)); /* t.key = 99 */
lua_pop(L, 1); /* stack: [{ }] */
Complete table example
/* Build a Lua table representing a Point { x = 10, y = 20 } */
lua_createtable(L, 0, 2); /* pre-allocate 2 record fields */
lua_pushnumber(L, 10);
lua_setfield(L, -2, "x"); /* t.x = 10 */
lua_pushnumber(L, 20);
lua_setfield(L, -2, "y"); /* t.y = 20 */
/* Now read both fields back */
lua_getfield(L, -1, "x");
lua_getfield(L, -2, "y"); /* -2 refers to the table, but this is just for demo */
printf("Point: x=%.0f, y=%.0f\n",
lua_tonumber(L, -2), lua_tonumber(L, -1));
lua_pop(L, 2); /* pop the two number results */
printf("Stack size: %d\n", lua_gettop(L)); /* 1 (table still on stack) */
lua_close(L);
Userdata and light userdata
Userdata is how C programs pass arbitrary C data — file handles, buffers, custom structs — into Lua. Lua distinguishes between two kinds of userdata.
Full userdata
Full userdata is a block of raw memory allocated by Lua’s allocator. It lives on the Lua heap, is managed by the garbage collector, and can have a metatable associated with it.
lua_newuserdata(L, size)allocates a block ofsizebytes and pushes it onto the stack. It returns avoid*pointer to the allocated memory.lua_newuserdatauv(L, size, nuvalue)(Lua 5.4+) does the same but allows you to associatenuvalueuserdata values with the block.lua_touserdata(L, idx)returns thevoid*pointer from a userdata on the stack.
/* Allocate a full userdata block of 64 bytes */
size_t size = 64;
void *udata = lua_newuserdata(L, size);
printf("Userdata pointer: %p\n", udata);
/* The pointer is valid until the userdata is garbage-collected
or popped from the stack */
Light userdata
Light userdata is not a Lua heap allocation — it is simply a void* pointer value pushed onto the stack. It has no metatable, no garbage collection, and no managed lifetime. It is just a raw pointer value that Lua stores on the stack like any other value.
lua_pushlightuserdata(L, p)pushes a light userdata holding pointerp.lua_touserdata(L, idx)also works for light userdata.
int my_data = 42;
/* Light userdata — just stores the pointer value, no allocation */
lua_pushlightuserdata(L, &my_data); /* stack: [<lightuserdata>] */
/* Read it back */
void *p = lua_touserdata(L, -1);
printf("Light userdata points to: %p (value: %d)\n", p, *(int*)p);
lua_pop(L, 1);
Use light userdata when you need to pass a C pointer into Lua without the GC overhead of full userdata. The C code is responsible for ensuring the pointed-to memory remains valid.
Setting a metatable on userdata
Full userdata can have a metatable, which lets you define custom behavior for operations like indexing, arithmetic, or finalization:
/* Create a userdata and set a metatable on it */
lua_newuserdata(L, sizeof(double)); /* allocate */
lua_createtable(L, 0, 1); /* create metatable */
/* Set a __name field in the metatable */
lua_pushstring(L, "__name");
lua_pushstring(L, "MyType");
lua_settable(L, -3); /* mt.__name = "MyType" */
/* Set the metatable on the userdata (at stack top) */
/* lua_setmetatable pops the metatable and attaches it to the userdata */
lua_setmetatable(L, -2); /* userdata now has its metatable */
printf("Userdata with metatable created\n");
lua_close(L);
Error handling
Raising errors from C
When a C function called from Lua detects an error, it calls luaL_error(L, "format %s", arg). This formats a message, pushes it onto the stack, and performs a longjmp back to the nearest protected caller. It never returns.
/* A C function that expects a number as its first argument */
static int l_double(lua_State *L) {
if (!lua_isnumber(L, 1)) {
/* Raise a descriptive error and never return */
return luaL_error(L, "argument must be a number, got %s",
lua_typename(L, lua_type(L, 1)));
}
lua_Number n = lua_tonumber(L, 1);
lua_pushnumber(L, n * 2);
return 1; /* one return value */
}
Protected calls with lua_pcall
lua_pcall(L, nargs, nresults, errfunc) calls a Lua function in protected mode. If the function raises an error, the error handler at errfunc receives the error message (pass 0 for no handler). The return code is LUA_OK (0) on success, or one of LUA_ERRRUN, LUA_ERRSYNTAX, LUA_ERRMEM.
lua_getglobal(L, "add"); /* push the Lua function "add" onto the stack */
lua_pushnumber(L, 3); /* first argument */
lua_pushnumber(L, 4); /* second argument */
/* Call the function with 2 args, expecting 1 result, no error handler (0) */
int status = lua_pcall(L, 2, 1, 0);
if (status != LUA_OK) {
/* An error occurred — the error message is on the stack */
fprintf(stderr, "Error: %s\n", lua_tostring(L, -1));
lua_pop(L, 1); /* pop the error message */
} else {
/* Success — read the result */
lua_Number result = lua_tonumber(L, -1);
printf("add(3, 4) = %.1f\n", result); /* 7.0 */
lua_pop(L, 1); /* pop the result */
}
Unprotected calls with lua_call
lua_call(L, nargs, nresults) is the unprotected variant. If the called function raises an error, the error propagates directly up the C call stack — there is no protective wrapper. Only use lua_call when you are certain the Lua code cannot error, for example when calling a pure-math C function you wrote yourself.
/* Call a Lua function with lua_call — no error protection */
lua_getglobal(L, "double"); /* push function */
lua_pushnumber(L, 5); /* argument */
lua_call(L, 1, 1); /* 1 arg, 1 result, no protection */
printf("Result: %.0f\n", lua_tonumber(L, -1));
lua_pop(L, 1);
Handling multiple return values
Pass LUA_MULTRET (which equals -1) as the nresults parameter to receive all return values the function produces. The stack grows to accommodate them.
/* Define a function that returns multiple values */
luaL_dostring(L, "function f() return 1, 2, 3 end");
lua_getglobal(L, "f");
lua_call(L, 0, LUA_MULTRET); /* call with 0 args, accept all results */
printf("Stack size after call: %d\n", lua_gettop(L)); /* 3 */
printf("Values: %.0f, %.0f, %.0f\n",
lua_tonumber(L, -3), lua_tonumber(L, -2), lua_tonumber(L, -1));
lua_pop(L, 3); /* pop all returned values */
Loading and running Lua code
luaL_loadstring(L, code) compiles a Lua string and pushes the resulting function onto the stack. Returns LUA_OK on success or an error code.
luaL_dostring(L, code) is a convenience macro that loads and executes a string in one step. It returns 0 on success and non-zero on error.
/* Load and execute a Lua snippet */
int status = luaL_dostring(L, "print('hello from Lua')");
if (status != LUA_OK) {
fprintf(stderr, "Error: %s\n", lua_tostring(L, -1));
lua_pop(L, 1);
}
Opening standard libraries
Call luaL_openlibs(L) right after luaL_newstate() to make Lua’s standard libraries (base, string, table, math, io, os, utf8, debug) available. Without this, basic functions like print, pairs, and ipairs will not exist.
int main(void) {
lua_State *L = luaL_newstate();
/* Must be called before any Lua code that uses standard library functions */
luaL_openlibs(L);
/* Now print() is available */
luaL_dostring(L, "print('Lua is ready!')");
lua_close(L);
return 0;
}
Common pitfalls
Stack underflow
Accessing or popping from an empty stack causes undefined behavior. Always check lua_gettop(L) first, or use the lua_isnone(L, idx) macro (returns true if the index is not valid).
/* WRONG — crashes if the stack is empty */
lua_Integer x = lua_tointeger(L, 1);
/* RIGHT — check first */
if (lua_gettop(L) >= 1 && lua_isnumber(L, 1)) {
lua_Integer x = lua_tointeger(L, 1);
}
Lua_pop does not return values
lua_pop(L, n) simply discards n elements. If you need the popped value, copy it first:
/* WRONG — the value is gone after pop */
lua_pop(L, 1);
/* RIGHT — copy the top before popping */
lua_pushvalue(L, -1); /* duplicate the top */
lua_Integer x = lua_tointeger(L, -2); /* read from the original */
lua_pop(L, 1); /* pop the original, leaving the copy */
Lua_isstring returns true for numbers
Because Lua coerces numbers to strings in many contexts, lua_isstring(L, idx) returns true for numeric values. If you need to distinguish between strings and numbers, use lua_type(L, idx) == LUA_TSTRING.
lua_pushnumber(L, 42);
/* This fires even though the value is a number, not a string */
if (lua_isstring(L, 1)) {
printf("This fires even though the value is a number!\n");
}
/* Correct check for actual strings only */
if (lua_type(L, 1) == LUA_TSTRING) {
/* This branch does NOT fire for the number 42 */
}
Deprecated APIs
Several old functions have been removed or replaced in modern Lua. Avoid them:
lua_open()— removed in Lua 5.1. UseluaL_newstate().luaopen_base(),luaopen_table(), etc. — replaced by the singleluaL_openlibs()call.lua_strlen()— replaced bylua_rawlen()in Lua 5.1.lua_tostring()used for length — in Lua 5.4, uselua_tolstring(L, idx, &len)to get both the pointer and the length.
Putting it all together
Here is a complete C program that creates a Lua state, opens the standard libraries, pushes values, defines a Lua function from C, calls it from Lua code loaded via luaL_dostring(), reads the result, creates a table, allocates userdata, and cleans up safely.
/*
** Complete minimal example demonstrating the Lua C API basics.
**
** Compile: gcc -o example example.c -llua5.4 -lm (Linux)
** gcc -o example example.c -llua -lm (macOS)
** Run: ./example
**
** Expected output:
** Stack size after pushes: 3
** add(3, 4) = 7.0
** double(21) = 42
** double('hi') error: argument must be a number, got string
*/
#include <stdio.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
/* A C function callable from Lua: doubles a number */
static int l_double(lua_State *L) {
/* Check that the first argument is a number */
if (!lua_isnumber(L, 1)) {
/* Raise a Lua error — luaL_error never returns */
return luaL_error(L, "argument must be a number, got %s",
lua_typename(L, lua_type(L, 1)));
}
/* Read the number from the stack at index 1 */
lua_Number n = lua_tonumber(L, 1);
/* Push the result back onto the stack */
lua_pushnumber(L, n * 2);
/* Return 1 — our function produces one return value */
return 1;
}
int main(void) {
lua_State *L;
/* 1. Create a new Lua state */
L = luaL_newstate();
if (L == NULL) {
fprintf(stderr, "Failed to create Lua state\n");
return 1;
}
/* 2. Open all standard libraries */
luaL_openlibs(L);
/* 3. Register our C function as a global Lua function called "double" */
lua_register(L, "double", l_double);
/* lua_register(L, name, f) is shorthand for:
lua_pushcfunction(L, f);
lua_setglobal(L, name);
*/
/* 4. Push a few values to show stack operations */
lua_pushnumber(L, 42);
lua_pushstring(L, "hello");
lua_pushboolean(L, 1);
printf("Stack size after pushes: %d\n", lua_gettop(L)); /* 3 */
/* Pop the three values we just pushed (clean up before running Lua code) */
lua_pop(L, 3);
/* 5. Define a Lua function and call it from C */
const char *code =
"function add(a, b)\n"
" return a + b\n"
"end\n";
if (luaL_dostring(L, code) != LUA_OK) {
fprintf(stderr, "Load error: %s\n", lua_tostring(L, -1));
lua_close(L);
return 1;
}
/* 6. Call the Lua function add(3, 4) */
lua_getglobal(L, "add"); /* push function onto stack */
lua_pushnumber(L, 3); /* first argument */
lua_pushnumber(L, 4); /* second argument */
/* Protected call: 2 args, 1 result, no error handler (0) */
if (lua_pcall(L, 2, 1, 0) != LUA_OK) {
fprintf(stderr, "Runtime error: %s\n", lua_tostring(L, -1));
lua_pop(L, 1);
} else {
/* Read the result */
lua_Number result = lua_tonumber(L, -1);
printf("add(3, 4) = %.1f\n", result); /* 7.0 */
lua_pop(L, 1); /* pop the result */
}
/* 7. Test our C function "double" called from Lua */
if (luaL_dostring(L, "print('double(21) =', double(21))") != LUA_OK) {
fprintf(stderr, "Error: %s\n", lua_tostring(L, -1));
lua_pop(L, 1);
}
/* 8. Test the error case: pass a string instead of a number */
if (luaL_dostring(L, "print(double('hi'))") != LUA_OK) {
fprintf(stderr, "double('hi') error: %s\n", lua_tostring(L, -1));
lua_pop(L, 1); /* pop the error message */
}
/* 9. Create a table and set fields */
lua_createtable(L, 0, 2); /* pre-allocate 2 fields */
lua_pushnumber(L, 10);
lua_setfield(L, -2, "x"); /* t.x = 10 */
lua_pushnumber(L, 20);
lua_setfield(L, -2, "y"); /* t.y = 20 */
lua_getfield(L, -1, "x");
printf("Table field t.x = %.0f\n", lua_tonumber(L, -1));
lua_pop(L, 1);
lua_pop(L, 1); /* pop the table */
/* 10. Create and use userdata */
void *udata = lua_newuserdata(L, sizeof(double));
*(double *)udata = 3.14159;
printf("Userdata allocated: %p (value: %.5f)\n", udata, *(double *)udata);
lua_pop(L, 1); /* pop the userdata */
/* 11. Clean up */
lua_close(L);
return 0;
}
Running this program produces output like:
Stack size after pushes: 3
add(3, 4) = 7.0
double(21) = 42
double('hi') error: argument must be a number, got string
Table field t.x = 10
Userdata allocated: 0x55a3b4a8e260 (value: 3.14159)
The program demonstrates every key concept from this article: creating and destroying a state, pushing and popping values, registering a C function, loading and calling Lua code, creating tables and setting fields, allocating userdata, and handling errors gracefully.
Summary
- Every Lua execution context is represented by a
lua_State*. Create withluaL_newstate(), destroy withlua_close(). - The virtual stack is the sole channel for exchanging values between C and Lua. It is LIFO, managed by Lua, and GC-aware.
- Positive indices count from the bottom (1 upward). Negative indices count from the top (-1 is top). Pseudo-indices access the registry and upvalues.
- Push functions cover every Lua type.
lua_pop()discards elements without returning them.lua_insert(),lua_replace(), andlua_copy()manipulate stack slots in place. - Always type-check before converting with
lua_to*.lua_isstringis misleading for numbers. String pointers fromlua_tolstring()are invalidated when the value is popped. - Table operations use
lua_createtableto create,lua_gettable/lua_settablefor stack-based key access, andlua_getfield/lua_setfieldfor named field access. Raw access bypasses metatables. - Full userdata (
lua_newuserdata) is heap-allocated and GC-managed with optional metatable support. Light userdata (lua_pushlightuserdata) is just a pointer value with no allocation or GC involvement. - Use
luaL_error()to raise errors from C. Uselua_pcall()for protected calls. Uselua_call()only when errors are guaranteed not to occur. PassLUA_MULTRETfor variable return counts. Load and run Lua strings withluaL_dostring().