Embed Lua 5.4 in a C Host Application
Run Lua scripts from C, expose host functions, and shut down the Lua state safely.
24 Oct 2025, 16:40 UTC

Problem and takeaway
You need a C or C++ host application to run Lua scripts without moving business logic into C. The useful takeaway is that Lua 5.4 exposes a small C API: create a lua_State, open standard libraries, load and run a file, push C functions onto the Lua stack, and close the state only after coroutines finish.
Prerequisites
- Lua 5.4 development headers (
lua.h,lauxlib.h,lualib.h) and the shared or static library (liblua5.4.so,lua54.dll, or equivalent). - A C compiler such as gcc or clang on Linux/macOS, or MSVC on Windows.
- A terminal where you can compile and run the host program.
lua_State is the opaque structure that holds a complete Lua execution environment, including the stack, globals, and garbage collector. The Lua stack is where C code passes arguments to and receives results from Lua functions.
1. Create and initialize the Lua state
Call luaL_newstate() to allocate a state, then luaL_openlibs(L) to load the standard library modules: base, package, string, table, math, io, os, debug, coroutine, and utf8.
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
#include <stdio.h>
int main(void) {
lua_State *L = luaL_newstate();
if (L == NULL) {
fprintf(stderr, "failed to create Lua state\n");
return 1;
}
luaL_openlibs(L);
/* load scripts and expose functions here */
lua_close(L);
return 0;
}
Run this in a normal user shell. No special permissions are required, but the compiler must find the Lua headers and library. If compilation fails with lua.h: No such file or directory, install the Lua 5.4 development package or add the header path with -I.
2. Expose a C function to Lua
A C function exposed to Lua must have the signature int function_name(lua_State *L). It reads arguments from the stack, pushes return values, and returns the number of results. Push the function with lua_pushcfunction and assign it a global name with lua_setglobal.
static int host_log(lua_State *L) {
int n = lua_gettop(L);
for (int i = 1; i <= n; i++) {
const char *text = lua_tostring(L, i);
if (text != NULL) {
fputs(text, stdout);
if (i < n) putchar(' ');
}
}
putchar('\n');
return 0;
}
/* in main, after luaL_openlibs(L): */
lua_pushcfunction(L, host_log);
lua_setglobal(L, "host_log");
lua_gettop(L) returns the stack depth. Index 1 is the first argument; negative indices count from the top, so -1 is the most recent value. Always balance pushes and pops; leaving extra values on the stack can corrupt later calls.
3. Load and run a Lua script
Use luaL_dofile(L, "script.lua"), which combines loading and protected execution. It returns 0 on success and a non-zero error code on failure. On failure, the error message is a string at the top of the stack.
if (luaL_dofile(L, "script.lua") != 0) {
fprintf(stderr, "script error: %s\n", lua_tostring(L, -1));
lua_pop(L, 1); /* remove the error message from the stack */
/* decide whether to continue or shut down */
}
Create script.lua in the working directory:
host_log("hello", "from", "Lua")
x = 1 + 2
host_log("result:", x)
Compile and run from the shell:
cc -Wall -Wextra -o host host.c -llua5.4 -lm -ldl
./host
Expected output:
hello from Lua
result: 3.0
This output is illustrative, not a captured test run. Lua 5.4 prints numbers as doubles by default, so 3 may appear as 3.0 depending on formatting.
4. Handle errors safely
Wrap calls that execute Lua code in protected mode. luaL_dofile already uses lua_pcall internally. If you call a Lua function directly from C, use lua_pcall so a Lua error does not long-jump past your C frames. Validate arguments with luaL_checkinteger, luaL_optstring, and related functions; they raise a Lua error with a clear message when types do not match.
Do not call lua_close(L) while a coroutine is still active. Finish or close all coroutines first, then close the state.
5. Shut down and collect memory
Lua uses garbage collection. Before shutdown, you can force a full collection:
lua_gc(L, LUA_GCCOLLECT, 0);
lua_close(L);
If your C code allocates memory inside userdata, give that userdata a __gc metamethod so Lua frees the C memory when the userdata is collected. Release registry references created with luaL_ref using luaL_unref when they are no longer needed.
Expected checks
- The host exits with code
0when the script runs successfully. - A syntax error or runtime error in
script.luaproduces a non-zero return fromluaL_dofileand prints the error message from stack top. - Calling
host_logfrom Lua prints to the host's stdout. lua_version(L)returns a pointer to the Lua 5.4 version number, confirming header/library compatibility.
Limitations
- Lua 5.4 uses generational garbage collection by default. Long-running hosts sensitive to latency may switch to incremental mode with
lua_gc(L, LUA_GCGEN, 0, 0). - Each operating system thread needs its own
lua_State. Do not share Lua objects directly across states; serialize data or use light userdata carefully. - The debug library can expose internals. Omit it in production or sandbox scripts that should not inspect the host.
- Lua numbers are doubles by default. If your Lua build was compiled with integer floats, C API conversion macros and precision differ.
Recovery options
- If
luaL_newstatereturns NULL, abort initialization and report an out-of-memory or configuration error. - If
luaL_dofilefails, pop the error message withlua_pop(L, 1)before reusing the state, otherwise the stack grows unbounded. - If a C callback crashes, check stack balance with
lua_gettopin a debug build and run under AddressSanitizer or valgrind to detect userdata leaks.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.