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 type | C push function | C get function |
|---|---|---|
| nil | lua_pushnil | — |
| number | lua_pushnumber | lua_tonumber / luaL_checknumber |
| string | lua_pushstring | lua_tostring / luaL_checkstring |
| boolean | lua_pushboolean | lua_toboolean |
| table | lua_createtable / lua_newtable | lua_gettable |
| function (C) | lua_pushcfunction | lua_isfunction |
| userdata | lua_newuserdata | lua_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
- /tutorials/embedding-lua-in-c/ — a step-by-step walkthrough of embedding Lua in a C program
- /tutorials/extending-lua-from-c/ — writing and registering C functions that Lua calls
- /guides/lua-closures/ — closures and upvalues, which matter when Lua functions call back into C