luaguides

Lua-C Interop: A Practical Guide

Overview

Lua is designed to be embedded. The official C API is not an afterthought or a bindings layer — it is the primary way Lua communicates with host applications. Every Lua interpreter, from the standalone lua command to game engines and network servers, is built on top of this same API. This Lua-C interop guide walks through both directions of the boundary so you can connect Lua and C code confidently.

There are two directions to this boundary. Extending Lua from C means writing C functions that Lua code can call as if they were native. Embedding Lua in C means loading and running Lua scripts from inside a C program, passing values in and out.

Both directions use the same mechanism: a value stack. Every value that crosses the language boundary — numbers, strings, tables, functions — passes through this stack. If you understand the stack, you understand the entire API.

The Stack

The Lua C API is a stack-based API. All communication between C and Lua happens through a virtual stack sitting between the two worlds.

The stack is indexed from 1 to the top (where 1 is the bottom). Negative indices count from the top: -1 is always the top, -2 is second from the top, and so on. This negative indexing is convenient when you do not know how many items are on the stack.

#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

int main(void) {
    lua_State *L = luaL_newstate();   // create Lua state
    luaL_openlibs(L);                  // open standard libraries

    lua_pushnumber(L, 42);              // push 42 onto stack
    lua_pushstring(L, "hello");         // push string

    int top = lua_gettop(L);            // stack has 2 items
    (void)top;

    lua_pop(L, 1);                      // remove one item
    // stack now has 1 item: the number 42

    lua_close(L);
    return 0;
}

Extending Lua from C

Write a C function that Lua can call, register it, then call it from Lua.

The stack you just saw is the same mechanism that powers function calls across the language boundary. When Lua invokes a C function, the arguments arrive on the stack in order, and the return values must be pushed back onto the stack before the function returns. Every C function that Lua can call follows the same pattern: read input from the stack, perform work, push output onto the stack.

Writing a C function

A C function callable from Lua has this signature:

static int my_add(lua_State *L) {
    double a = luaL_checknumber(L, 1);  // get first argument
    double b = luaL_checknumber(L, 2);  // get second argument
    lua_pushnumber(L, a + b);           // push result
    return 1;                           // one return value
}

luaL_checknumber raises a Lua error if the argument is not a number. return N tells Lua how many values are on the stack for the caller to receive.

Registering the function

Register the function into a global table so Lua can find it:

lua_pushcfunction(L, my_add);
lua_setglobal(L, "my_add");

Once lua_setglobal binds the C function pointer to a name in the Lua global table, Lua scripts can invoke it as if it were a built-in function. The arguments you pass from the Lua side correspond to stack positions 1, 2, 3 and so on inside the C function, exactly as the luaL_checknumber calls expect. This is what makes the stack model elegant — there is no special argument-passing mechanism. Everything is just values on the stack, pushed by the Lua runtime before it hands control to your C code.

Now from Lua:

print(my_add(3, 4))  --> 7

Exposing C functions as a module

Registering individual globals pollutes the global namespace. Better to put them in a module table.

A module table keeps related functions together under a single namespace, just like the standard Lua libraries (math, string, table). The caller gets a clean import with require instead of relying on globals that could collide with other libraries or user-defined variables. The luaL_Reg array is a zero-terminated list of name-and-function-pointer pairs that the Lua auxiliary library uses to populate your module in one call.

static const luaL_Reg my_module[] = {
    { "add",      my_add },
    { "multiply", my_multiply },
    { NULL, NULL }
};

int luaopen_my_module(lua_State *L) {
    luaL_newlib(L, my_module);   // create table from luaL_Reg array
    return 1;                     // return the table
}

The require function locates the compiled module, calls its luaopen_ entry point, and returns the table that the C function pushed onto the stack. From that point on, the module table behaves like any other Lua table — you can index it, pass it around, or assign it to a local variable to avoid repeated global lookups in performance-sensitive code.

In Lua, load and use the module the standard way:

local my = require("my_module")
print(my.add(3, 4))  --> 7

Pushing and retrieving values

Every Lua type has a corresponding push function:

Lua typeC push functionC get function
nillua_pushnil
numberlua_pushnumberlua_tonumber / luaL_checknumber
stringlua_pushstringlua_tostring / luaL_checkstring
booleanlua_pushbooleanlua_toboolean
tablelua_createtable / lua_newtablelua_gettable
function (C)lua_pushcfunctionlua_isfunction
userdatalua_newuserdatalua_touserdata

Working with tables from C

Create a table and set fields:

lua_createtable(L, 0, 2);        // new table: 0 array slots, 2 hash slots
lua_pushstring(L, "name");
lua_pushstring(L, "Alice");
lua_settable(L, -3);             // set t["name"] = "Alice"  (key at -2, value at -1)

lua_pushstring(L, "age");
lua_pushnumber(L, 30);
lua_settable(L, -3);             // set t["age"] = 30

To retrieve values stored in a table, lua_getfield pushes the field value onto the stack by key name. The stack position argument tells Lua which table to index — negative indices like -1 refer to the top of the stack, where your table should be sitting. After reading the value and converting it to a C type with lua_tostring or lua_tonumber, you must pop it to keep the stack clean for subsequent operations. Leaving stale values on the stack is a common source of subtle bugs in Lua-C interop code.

Read a field from a table:

lua_getfield(L, -1, "name");     // push t["name"] onto stack
const char *name = lua_tostring(L, -1);
lua_pop(L, 1);                   // pop the string

Embedding Lua in C

Embedding is the reverse direction: instead of Lua calling into C, your C program hosts a Lua runtime and drives it. This is what makes Lua popular as a configuration and scripting language inside game engines, network appliances, and scientific tools. The C program creates one or more lua_State instances, loads Lua code from strings or files, and retrieves results through the same stack API used for extending. Each lua_State is an isolated Lua universe with its own global table and garbage collector.

Create a Lua state, run a script, and handle the results.

Creating the interpreter

#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

int main(void) {
    lua_State *L = luaL_newstate();   // create state
    luaL_openlibs(L);                 // load stdlib (io, os, string, etc.)

    // run a script from a string
    if (luaL_dostring(L, "print('hello from lua')") != LUA_OK) {
        fprintf(stderr, "Lua error: %s\n", lua_tostring(L, -1));
        lua_pop(L, 1);
    }

    lua_close(L);
    return 0;
}

Loading and calling a file

luaL_dofile is a convenience wrapper that opens a file, reads its contents, compiles the Lua source into bytecode, and executes it — all in one call. For production use, consider separating these steps with luaL_loadfile followed by lua_pcall so you can distinguish compilation errors from runtime errors and handle each case differently. Compilation errors typically indicate a syntax mistake in the script, while runtime errors mean the script ran but hit a problem during execution.

Use luaL_dofile; it combines loading and executing:

if (luaL_dofile(L, "script.lua") != LUA_OK) {
    fprintf(stderr, "Error: %s\n", lua_tostring(L, -1));
    lua_pop(L, 1);
}

Calling a Lua function from C

After loading a Lua file, any functions it defined sit in the global table waiting to be called. lua_getglobal pushes the function onto the stack by name, then you push arguments on top of it. When you call lua_pcall or lua_call, Lua pops the function and its arguments, invokes the function, and pushes the return values back. The function index argument to lua_pcall tells it how many stack positions below the top the function sits — use 0 when you have pushed the function last, just before the arguments.

If your Lua script defines functions, retrieve and call them:

lua_getglobal(L, "greet");        // push function onto stack
lua_pushstring(L, "Alice");       // argument

if (lua_pcall(L, 1, 1, 0) != LUA_OK) {   // call with 1 arg, 1 result
    fprintf(stderr, "Error: %s\n", lua_tostring(L, -1));
    lua_pop(L, 1);
}

const char *result = lua_tostring(L, -1);
printf("Lua returned: %s\n", result);
lua_pop(L, 1);

lua_pcall is the protected version of calling a function. If the Lua function errors, lua_pcall catches it and returns an error code instead of crashing. The fourth argument (0 here) is an error handler function index; 0 means use the default handler.

The error handler function, if provided, runs before the error message is returned to the caller. A common pattern is to push a debug traceback function as the error handler, which prepends a Lua stack trace to any error message — invaluable for debugging embedded scripts. You set the fourth argument to a non-zero stack index pointing to your handler function, typically placed just below the function being called.

Returning values to C

From a Lua function called by C, return values by pushing them onto the stack:

static int get_config(lua_State *L) {
    lua_createtable(L, 0, 2);
    lua_pushstring(L, "host");
    lua_pushstring(L, "localhost");
    lua_settable(L, -3);
    lua_pushstring(L, "port");
    lua_pushinteger(L, 8080);
    lua_settable(L, -3);
    return 1;  // one table on the stack, Lua receives it as the return value
}

Handling Errors

Lua uses long jumps for error handling internally. The longjmp mechanism lets Lua unwind the C call stack when an error occurs, jumping directly to the nearest active lua_pcall or error handler. This is efficient but has consequences for C code that allocates resources — any malloc calls between the lua_pcall and the point of failure will leak unless you use proper cleanup with lua_newuserdata and __gc metamethods. Always structure your C callbacks so that resource allocation happens before the protected call, or use Lua-managed memory mechanisms.

C code running inside lua_pcall can call luaL_error to raise a Lua error that lua_pcall catches:

static int safe_div(lua_State *L) {
    double a = luaL_checknumber(L, 1);
    double b = luaL_checknumber(L, 2);
    if (b == 0) {
        return luaL_error(L, "division by zero");
    }
    lua_pushnumber(L, a / b);
    return 1;
}

When luaL_error is called inside lua_pcall, the stack contains the error message and lua_pcall returns LUA_ERRRUN.

Without lua_pcall (for example, during state creation), a Lua error in C code calls longjmp and the behavior is undefined. Always wrap Lua code execution in lua_pcall when it comes from C.

Memory Management

Lua allocates its own memory through an allocator function. When you call lua_close(L), Lua frees everything: all states, all allocated objects.

Userdata represents an opaque block of C memory that Lua can hold and pass around without understanding its contents. When Lua’s garbage collector determines that no references to the userdata remain, it invokes the __gc metamethod on the associated metatable. This gives your C code a hook to free any heap-allocated resources — buffers, file handles, network connections — that the userdata wraps. Without a __gc handler, userdata memory itself is reclaimed but any resources it points to leak permanently.

To mark userdata for garbage collection, use the metatable mechanism:

static int my_gc_callback(lua_State *L) {
    MyData *data = (MyData *)lua_touserdata(L, -1);
    free(data->buffer);
    free(data);
    return 0;
}

// When creating userdata:
MyData *data = (MyData *)lua_newuserdata(L, sizeof(MyData));
data->buffer = malloc(1024);
luaL_setmetatable(L, "MyData");

// Register the gc method somewhere in your module setup:
luaL_newmetatable(L, "MyData");
lua_pushcfunction(L, my_gc_callback);
lua_setfield(L, -2, "__gc");  // metamethod called when GC reclaims the object

Continuations and Yielding

When C functions are called from a coroutine that yields, the C function must support this via a continuation.

Yielding pauses a coroutine mid-execution and returns control to the caller, preserving the coroutine’s state so it can be resumed later. From C’s perspective, yielding means the C function must be prepared to have its execution suspended and later restarted — potentially on a different C stack frame. Continuation-based functions provide this by registering a callback that Lua invokes when the coroutine is resumed, allowing the C function to pick up where it left off without relying on the original C stack being intact. Use lua_callk or lua_pcallk instead of lua_call / lua_pcall, and use the lua_K continuation functions:

static int resumable_func(lua_State *L) {
    int status = lua_pcallk(L, 0, 1, 0, 0, NULL);
    if (status == LUA_YIELD) {
        return lua_yieldk(L, 1, 0, NULL);
    }
    return 1;
}

This is an advanced topic; most code does not need this unless writing coroutine-aware libraries.

Common Gotchas

Stack overflow. Lua has a stack that grows automatically, but C recursion is not tracked. If your C code recurses deeply (especially via lua_pcall), you can overflow the C stack before Lua’s protected execution catches it. Use iterative approaches when possible.

Forgetting to pop. Every lua_get* that retrieves a value leaves it on the stack. Forgetting to pop it before the next operation corrupts the data:

lua_getfield(L, -1, "name");
// must lua_pop(L, 1) before doing anything else with the stack

A single forgotten lua_pop can shift all subsequent stack indices by one, causing your code to read the wrong values without any obvious error. The Lua interpreter will not warn you because the stack is simply misaligned — your program keeps running but operates on garbage. For complex C functions that push and pop many values, consider wrapping operations in helper functions that clean up after themselves, or use lua_settop to reset the stack to a known height at the end of each function.

Integer truncation. lua_tonumber returns a double. If you store a large 64-bit integer in Lua and read it back through lua_tonumber, precision is lost:

// Lua: x = 9007199254740993  (2^53 + 1)
// C:   double d = lua_tonumber(L, -1);  // d may lose precision

Use lua_Integer with lua_tointeger for full 64-bit range on supported platforms.

Passing tables by reference, not copy. Tables are passed by reference in Lua. If you push a table from C to Lua and then modify it in Lua, the C pointer you stored is still valid. But if you close the Lua state, that pointer becomes dangling memory.

See Also