Embedding Lua in a C Application
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 — 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;
}
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
}
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. 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 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 this call, a Lua script can call my_add(3, 4) and receive 7.
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;
}
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.
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