Embedding Lua in a C Application
Prerequisites
You need a C compiler (GCC or Clang) and the Lua 5.4 development headers installed on your system. On Debian or Ubuntu, run apt install liblua5.4-dev. On macOS with Homebrew, use brew install lua. Basic familiarity with C programming and compiling from the command line is assumed. You do not need to know Lua beforehand — this tutorial starts from zero on the Lua side.
Introduction
Embedding Lua into a C application gives you a powerful, lightweight scripting layer that your users can use to configure behaviour, automate tasks, or extend functionality without recompiling anything. The Lua C API is a thin, well-designed bridge. You get a Lua state, push and pull values through a virtual stack, and call Lua code from C with almost no overhead.
This tutorial walks you through the complete embedding workflow: creating and destroying the Lua state, loading and running Lua code, exchanging data via the stack, and exposing your own C functions to Lua scripts. By the end you’ll have a working skeleton you can build on.
Setting up the Lua state
Every Lua operation starts with a lua_State, which is a pointer to an isolated Lua interpreter instance. You create it with luaL_newstate(), optionally load the standard library with luaL_openlibs(), and close it with lua_close() when you’re done.
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
int main(void) {
lua_State *L = luaL_newstate(); // Create a new Lua state
luaL_openlibs(L); // Load the standard library (print, io, etc.)
/* Run Lua code here */
lua_close(L); // Clean up
return 0;
}
This is the absolute minimum skeleton for any Lua-embedding program. Calling luaL_newstate() allocates and initialises the interpreter, luaL_openlibs() loads the standard libraries so your scripts can use print, table, string, and the rest, and lua_close() frees everything when you are done. Without luaL_openlibs(), the Lua interpreter starts with almost nothing: no print, no io, not even pairs. This can be useful if you want to sandbox scripts.
Compile this with:
gcc -o myapp myapp.c $(pkg-config --cflags --libs lua5.4)
If pkg-config is not available, use -I/usr/include/lua5.4 -llua5.4 directly. The exact library name depends on your installation. On some systems it is lua5.4, on others simply lua.
Running Lua code from C
There are two convenient helpers for executing Lua code: luaL_dofile() runs a .lua file and luaL_dostring() executes a string. Both return 0 on success and a non-zero error code on failure.
int ret = luaL_dofile(L, "script.lua");
if (ret != 0) {
fprintf(stderr, "Lua error: %s\n", lua_tostring(L, -1));
lua_pop(L, 1); // Remove the error message from the stack
}
Running an external Lua file with luaL_dofile() is the most common pattern for applications that ship Lua scripts alongside the binary. Think game engines loading level scripts or editors running user plugins. The return value of 0 means success, and any non-zero value indicates an error, which you can read from the top of the stack as a string. Always pop the error message after reading it so the stack stays balanced.
luaL_dostring() works the same way for inline code:
ret = luaL_dostring(L, "print('Hello from embedded Lua!')");
if (ret != 0) {
fprintf(stderr, "Error: %s\n", lua_tostring(L, -1));
lua_pop(L, 1);
}
For production code you will usually want to use lua_pcall() instead, because it lets you set a custom error handler and gives you more control over error recovery. The _pcall variant takes four arguments: the Lua state, the number of arguments on the stack, the number of return values you expect, and an optional error handler index (0 means no handler).
lua_getglobal(L, "print"); // Push the print function
lua_pushstring(L, "via pcall"); // Push an argument
int ret = lua_pcall(L, 1, 0, 0); // 1 arg, 0 returns, no error handler
if (ret != 0) {
fprintf(stderr, "pcall failed: %s\n", lua_tostring(L, -1));
lua_pop(L, 1);
}
lua_call() does the same job without any error handling. If the called code raises an error, it propagates directly to the caller. Use it only when you are certain the code cannot fail, or when you are already inside a protected call.
Exchanging data through the stack
The Lua stack is the central mechanism for all data exchange between C and Lua. C code pushes Lua values onto the stack to pass them to Lua, and pops values from the stack to read results back. The stack also works in reverse: you can push values from C and then read them back from Lua.
Pushing values onto the stack
The API provides one lua_push* function per type:
lua_pushnil(L); // Push nil
lua_pushboolean(L, 1); // Push a boolean (int, 0 or non-zero)
lua_pushinteger(L, 42); // Push an integer
lua_pushnumber(L, 3.14); // Push a double
lua_pushstring(L, "hello"); // Push a string (Lua copies it)
Lua manages the memory for these values. Strings in particular are copied into Lua’s internal buffer, so you do not need to keep your original C string alive after pushing it.
Reading values from the stack
Use the lua_to* family to read stack values back into C types:
int b = lua_toboolean(L, -1); // Returns 0 or 1
lua_Integer i = lua_tointeger(L, -1);
double n = lua_tonumber(L, -1);
const char *s = lua_tostring(L, -1); // Returns NULL if not a string
All of these functions are safe to call even if the value at the given index is not of the expected type, they return a sensible default (0, NULL, or false).
Stack indexes
Indexes into the stack can be positive or negative. Positive indexes count from the bottom of the stack (where index 1 is the first element). Negative indexes count from the top, so -1 is the top element, -2 is the one below it, and so on.
// Assuming the stack contains: {1, 2, 3} (3 on top)
// lua_gettop(L) returns 3
lua_pushinteger(L, 100);
// Stack: {1, 2, 3, 100} -- top is 100
int top = lua_gettop(L); // Returns 4
lua_pop(L, 1); // Remove top element
// Stack: {1, 2, 3} -- top is back to 3
For most operations you will use relative indexes like -1 to refer to the top, and 1 to refer to the bottom when iterating over all stack elements.
Exposing C functions to Lua
One of the most useful things you can do when embedding Lua is to register your own C functions so Lua scripts can call them. This is how you give scripts access to your application’s functionality.
A C function callable from Lua has this signature:
static int my_add(lua_State *L) {
int a = (int)luaL_checkinteger(L, 1); // Get 1st argument
int b = (int)luaL_checkinteger(L, 2); // Get 2nd argument
lua_pushinteger(L, a + b); // Push the result
return 1; // Number of return values
}
luaL_checkinteger validates that the argument at the given position is an integer and returns it. If it is not, it raises a Lua error with a formatted message. There is also luaL_checkstring for strings.
The function returns the number of values it pushed onto the stack, Lua uses this to know how many return values to collect.
Register the function with lua_register, which is a convenience macro that combines lua_pushcfunction and lua_setglobal:
lua_register(L, "my_add", my_add);
After calling lua_register, the C function my_add is available as a global named "my_add" in the Lua state — any Lua code running in that state can call it. The function follows the standard Lua C API contract: it receives its arguments from the stack (indexes 1, 2, …) and pushes its return values back onto the stack, then returns an integer count telling Lua how many return values to collect. This is the same calling convention used by every built-in Lua function.
Here is a complete example that registers a function and calls it from Lua:
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
#include <stdio.h>
static int say_hello(lua_State *L) {
const char *name = luaL_checkstring(L, 1);
printf("Hello, %s!\n", name);
return 0; // No return values
}
int main(void) {
lua_State *L = luaL_newstate();
luaL_openlibs(L);
lua_register(L, "say_hello", say_hello);
/* Call the function we just registered */
lua_getglobal(L, "say_hello");
lua_pushstring(L, "World");
lua_pcall(L, 1, 0, 0);
lua_close(L);
return 0;
}
When you run this program it prints Hello, World!.
Raising errors from C
When something goes wrong inside a C function that you have exposed to Lua, use luaL_error() to raise a Lua error. It formats a message using printf-style arguments and throws it, just as if Lua itself had raised an error.
static int safe_div(lua_State *L) {
int a = (int)luaL_checkinteger(L, 1);
int b = (int)luaL_checkinteger(L, 2);
if (b == 0) {
return luaL_error(L, "division by zero");
}
lua_pushinteger(L, a / b);
return 1;
}
The error propagates up through lua_pcall and lands on the stack as a string, where your C code can read it and handle it.
A complete worked example
Putting the pieces together, here is a minimal but complete C program that creates a Lua state, registers a C function, evaluates a Lua expression from a string, and prints the result:
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
#include <stdio.h>
static int square(lua_State *L) {
int n = (int)luaL_checkinteger(L, 1);
lua_pushinteger(L, n * n);
return 1;
}
int main(void) {
lua_State *L = luaL_newstate();
luaL_openlibs(L);
lua_register(L, "square", square);
const char *code = "local x = 5; local y = square(x); print(y)";
int ret = luaL_dostring(L, code);
if (ret != 0) {
fprintf(stderr, "Error running Lua: %s\n", lua_tostring(L, -1));
lua_pop(L, 1);
}
lua_close(L);
return 0;
}
This program ties together everything covered so far: creating and closing a Lua state, registering a C function, running Lua code from a string, and handling errors. The square function follows the exact same pattern as my_add earlier — check arguments with luaL_checkinteger, push a result, and return 1. The Lua code local x = 5; local y = square(x); print(y) calls our C function just as if it were a native Lua function, which is the whole point of embedding.
Compile and run:
gcc -o square square.c $(pkg-config --cflags --libs lua5.4)
./square
The output is 25, because square(5) returns 25 from our C function.
Common pitfalls
A few mistakes come up repeatedly when you are first embedding Lua:
Stack overflow. Lua has a stack size limit (typically a few thousand entries). If you push too many values without popping, Lua will abort with a stack overflow. Use lua_gettop(L) to check the current stack size and lua_checkstack(L, n) to ensure there is room before a large push.
String lifetime. When you push a string with lua_pushstring(), Lua copies the string into its own memory. However, if you push a luaL_Buffer (a temporary string buffer API), the underlying buffer must not be freed before the buffer is completed, the Lua API documentation calls this out explicitly.
Garbage collection for userdata. If you allocate memory from C and want Lua’s garbage collector to manage it, create a userdata object with a custom metatable that has a __gc metamethod. This is an advanced topic but important for long-running applications that create many such objects.
Thread safety. Each lua_State is single-threaded. If you need to run Lua from multiple OS threads, each thread must have its own lua_State. Lua coroutines (threads within a single lua_State) are fine, but sharing a state across OS threads requires explicit synchronisation and is generally not safe.
Summary
Embedding Lua comes down to a small, consistent set of operations:
- Create the state with
luaL_newstate(), load libraries withluaL_openlibs(), and close withlua_close(). - Run code with
luaL_dofile(),luaL_dostring(), orlua_pcall()for protected execution. - Push values to send data to Lua and
lua_to*functions to read values back. - Register C functions with
lua_register()so Lua scripts can call your code. - Handle errors with
lua_pcall()on the Lua side andluaL_error()on the C side.
The Lua 5.4 API is stable and well documented. Once you are comfortable with the stack model, you will find that adding a Lua scripting layer to a C project takes only a few dozen lines of code, and gives your users a great deal of flexibility in return.
Next steps
Now that you can run Lua code from C and expose your own functions, try building a small application that reads configuration from a Lua script or uses Lua to define game logic. The Lua C API reference lists every lua_* function available. For a deeper understanding of the virtual stack and how Lua manages values internally, work through the Lua C API: Stack and State tutorial.
See also
- The Lua C API: Stack and State — a deep dive into how the virtual stack works, index conventions, and the core push/pull API
- Metatables and Metamethods — how Lua’s metatable system lets you overload operators and build object-oriented patterns
- Modules and
require— how Lua loads and organises code, which matters when embedding scripts in C