luaguides

The Lua C API: Stack and State

Prerequisites

  • A C compiler (GCC or Clang) with C99 support
  • Lua 5.4 development headers installed (liblua5.4-dev on Debian/Ubuntu, lua on Homebrew)
  • Familiarity with C pointers, structs, and function signatures
  • Basic knowledge of Lua syntax (variables, functions, tables)

This tutorial targets Lua 5.4, the current stable release. Most concepts apply to 5.3 as well, but some API signatures differ in earlier versions.

What you will learn

The Lua C API gives you direct access to Lua’s internals from C: you create states, push values onto a virtual stack, and call Lua functions from your C program. This tutorial covers the foundational concepts every embedder needs:

  • What lua_State is 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

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 _G table
  • Thread safe by design: one lua_State per thread
graph LR
  PM["Process memory"]
  PM --> T1["Thread 1"] --> LA["lua_State A
_G, stack, gc"] PM --> T2["Thread 2"] --> LB["lua_State B
_G, stack, gc"] PM --> T3["Thread 3"] --> LC["lua_State C
_G, stack, gc"]

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 the appropriate flags for your platform. The -lm flag links the math library, which Lua depends on internally. On some systems (particularly macOS with Homebrew) the library name is just -llua rather than -llua5.4. If you get linker errors about undefined references to luaL_newstate or lua_close, verify the library name with pkg-config --libs lua5.4 or check your distribution’s package documentation.

# 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 program above does no real work yet; it only proves that the Lua runtime initializes and shuts down correctly on your system. If the output prints both messages without error, your compiler, linker, and Lua headers are all set up properly, and you are ready to move on to exchanging actual data between C and Lua.


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. For a broader overview of how this fits into embedding patterns, see the Lua Interop Guide.

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:

  1. 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.”

  2. 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
graph LR
  CV["C variable
(int x)"] LG["Lua globals"] LS["Lua stack
10, 'hi', ..."] CV -->|push| LS LG -->|push| LS LS -->|read| CV LS -->|read| LG

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 code
  • lua_upvalueindex(i) accesses upvalue i of 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.

FunctionPushes
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 slot idx, shifting the element currently at idx and all above it upward by one position.
  • lua_replace(L, idx) pops the top and writes it into slot idx without shifting any other elements. The old value at idx is 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

Once values are on the stack, you need to know how many there are and whether you have enough room to add more. The two functions covered in this section, lua_gettop and lua_checkstack, answer those questions and are the first calls you should make before reading or writing stack values in any function that does not fully control its own stack state.

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);

Whenever you query the stack size, keep in mind that lua_gettop returns the count of values currently living on the stack. That count changes with every push, pop, or lua_settop call. If you need to store a reference to a stack position that stays valid across stack mutations, convert the index to an absolute value with lua_absindex first.

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. The convenience macros like lua_isstring are designed to answer the question “can this value be used as a string?” rather than “is this value a string?” The distinction matters whenever your C code branches on type ; using the wrong check can send a number down a string-handling path and produce a subtle bug that only surfaces at runtime.

/* 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. This distinction is important when your C code needs to distinguish between integer-indexed arrays (the array part of a table) and floating-point keys ; using lua_isinteger instead of lua_isnumber prevents accidental coercion of fractional values into table indices.

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.

FunctionReturns
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. Lua strings are not null-terminated ; they carry an explicit length internally. This means a single Lua string can hold binary data like image buffers, serialised structs, or UTF-8 text that happens to contain zero bytes. When you call lua_tolstring with a non-NULL len pointer, you get both the raw bytes and the true byte count, so you never accidentally truncate at the first embedded null.

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. The techniques covered here mirror the table operations available in Lua itself — see the Table Library reference for the Lua-side equivalents.

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 up t[k] where t is at idx, and pushes the result.
  • lua_settable(L, idx) pops both a key and a value from the stack, then sets t[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. For string-keyed table access, which covers the majority of real-world use, these shortcuts make the C code noticeably shorter and less error-prone, since you avoid the separate key-push step that can leave stray values on the stack if the subsequent get operation fails.

/* 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. This is essential when you are writing low-level table utilities in C that must operate on the table’s own keys without triggering user-defined metamethod hooks ; for example, a serialisation function that enumerates every key in a table should use raw access to avoid calling a custom __index that might return unexpected values.

/* 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

The following example ties together the table operations covered so far: creating a table with pre-allocated slots, setting named fields with lua_setfield, and reading them back with lua_getfield. Notice how the table remains on the stack throughout ; every lua_setfield pops only the value, not the table, because it receives the table’s index as its second argument. This is a common pattern: push a table once, perform multiple field operations on it, then pop it when you are finished.

/* 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. For a deeper dive into userdata lifecycle and metatables, see the Userdata and Metatables tutorial.

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. For a complete walkthrough of metatable-driven object systems in C, read the Extending Lua from C tutorial.

  • lua_newuserdata(L, size) allocates a block of size bytes and pushes it onto the stack. It returns a void* pointer to the allocated memory.
  • lua_newuserdatauv(L, size, nuvalue) (Lua 5.4+) does the same but allows you to associate nuvalue userdata values with the block.
  • lua_touserdata(L, idx) returns the void* 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 pointer p.
  • 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. Because luaL_error does a longjmp, any C resources you allocated before the error call (file handles, malloc’d memory, mutex locks) will leak unless you clean them up before calling luaL_error ; or unless you use Lua’s own allocator, which the GC can reclaim.

/* 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. After the call finishes, you are responsible for reading and popping every returned value ; Lua does not track which stack slots were pushed by the function call and which were already there. Using LUA_MULTRET is standard practice when you do not know in advance how many values a Lua function will return, which is common with iterators and table unpacking.

/* 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. You can also open libraries selectively with individual calls like luaopen_base(L) or luaopen_math(L), but luaL_openlibs is the standard approach for embedding ; it gives Lua code access to the full standard library without extra boilerplate on the C side.

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). The Lua API does not bounds-check stack accesses ; it assumes you have kept track of what is on the stack. A mismatched push/pop pair or an off-by-one index are among the most frequent crashes in C embedding code, and they often manifest far from the actual bug because the stack corruption only becomes visible several Lua operations later.

/* 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. A beginner might expect lua_pop to behave like the pop operation in a stack data structure that returns the removed element ; but the Lua API intentionally separates the “read” step from the “remove” step. This design gives you full control: you can inspect a value, decide whether to keep it, and only then pop it.

/* 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. This is by design ; Lua’s automatic coercion means that any number can be converted to a string representation on demand. If you need to distinguish between strings and numbers, use lua_type(L, idx) == LUA_TSTRING. The coercion behaviour is most surprising when you are writing type-dispatching code that branches on the value kind: a quick lua_isstring check will route both 42 and "42" into the same branch.

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. Use luaL_newstate().
  • luaopen_base(), luaopen_table(), etc.: replaced by the single luaL_openlibs() call.
  • lua_strlen(): replaced by lua_rawlen() in Lua 5.1.
  • lua_tostring() used for length: in Lua 5.4, use lua_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. The program walks through every concept introduced so far in a single source file ; compile it, run it, and step through the comments to see how each API call connects to the next. Use this as a template when starting your own Lua embedding project.

The program opens with the standard includes and a helper function, l_double, registered as a callable C function. After creating the state and opening libraries, it demonstrates stack push and pop operations:

/*
** 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);

With the state ready and the double function registered, the program now runs Lua code from C, calls Lua functions, exercises the error-handling path, creates a table, and allocates userdata — all operating within a single state lifecycle. Each step below builds on the setup performed above, so keep the preceding code in place when compiling.

    /* 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 the following. The exact userdata pointer address will vary between runs and systems, but the computed values, function results, and error messages should match exactly. If a line is missing, check that your Lua version is 5.4 and that you linked against the correct library (-llua5.4 on Linux, -llua on macOS).

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 with luaL_newstate(), destroy with lua_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(), and lua_copy() manipulate stack slots in place.
  • Always type-check before converting with lua_to*. lua_isstring is misleading for numbers. String pointers from lua_tolstring() are invalidated when the value is popped.
  • Table operations use lua_createtable to create, lua_gettable/lua_settable for stack-based key access, and lua_getfield/lua_setfield for 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. Use lua_pcall() for protected calls. Use lua_call() only when errors are guaranteed not to occur. Pass LUA_MULTRET for variable return counts. Load and run Lua strings with luaL_dostring(). For a complete embedding workflow that puts these error-handling patterns into practice, see the Extending Lua from C tutorial.

Next steps

Now that you understand the Lua C API fundamentals—state management, the virtual stack, type checking, table manipulation, and error handling—you are ready to move on to the next tutorial in this series:

  • Extending Lua from C — learn how to write C functions that Lua can call, register them as modules, and structure larger embedding projects with proper error boundaries and metatable-based type systems.

See also

  • Extending Lua from C — the next tutorial in the embedding series, covering C function registration and module creation
  • Userdata and Metatables — goes deeper into full userdata, __gc finalisers, and metatable-based object systems
  • Lua Interop Guide — a broader reference covering the full spectrum of C-Lua interop patterns beyond the basics