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.

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.

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

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:

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
}

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

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

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

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

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.

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. 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 is C memory that Lua manages through its garbage collector. 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. 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

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