Embedding Lua in a host program
The C API stack, passing values in both directions, registering host functions, and sandboxing untrusted scripts.
The C API in one breath
Lua is designed to be embedded: the interpreter is a library, and your program owns the lua_State that holds the stack, the globals and the garbage collector. Everything crosses the boundary through that stack.
#include <stdio.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
int main(void) {
lua_State *L = luaL_newstate(); /* one state per thread, never shared */
luaL_openlibs(L); /* string, table, math, io, os, ... */
if (luaL_dofile(L, "config.lua") != LUA_OK) {
fprintf(stderr, "%s\n", lua_tostring(L, -1));
lua_pop(L, 1); /* remove the error message */
lua_close(L);
return 1;
}
lua_getglobal(L, "width"); /* push the global onto the stack */
int width = (int)lua_tointeger(L, -1);
lua_pop(L, 1); /* pop it: keep the stack balanced */
printf("width = %d\n", width);
lua_close(L);
return 0;
}- The stack is indexed from 1 at the bottom; negative indices count from the top, so
-1is always the most recent push. - Any function returning
intreturns a count of values left on the stack for Lua to consume. luaL_dofileisluaL_loadfilepluslua_pcall; the pieces are separate when you want to compile once and run many times.- Compile with the C compiler, not C++, or put the headers inside
extern "C".
Host functions and calling Lua
#include <math.h>
/* Registered as log(msg, level) inside Lua. */
static int host_log(lua_State *L) {
const char *msg = luaL_checkstring(L, 1); /* type error if not a string */
int level = (int)luaL_optinteger(L, 2, 0); /* optional with a default */
fprintf(stderr, "[%d] %s\n", level, msg);
return 0; /* nothing to return */
}
/* Registered as distance(x1, y1, x2, y2) and returns one number. */
static int host_distance(lua_State *L) {
double dx = luaL_checknumber(L, 1) - luaL_checknumber(L, 3);
double dy = luaL_checknumber(L, 2) - luaL_checknumber(L, 4);
lua_pushnumber(L, sqrt(dx * dx + dy * dy));
return 1;
}
static void register_host(lua_State *L) {
lua_pushcfunction(L, host_log); lua_setglobal(L, "log");
lua_pushcfunction(L, host_distance); lua_setglobal(L, "distance");
}
/* Call a Lua function that the script defined. */
static int call_update(lua_State *L, int entity_id) {
lua_getglobal(L, "on_update"); /* 1: the function */
if (!lua_isfunction(L, -1)) { lua_pop(L, 1); return 0; }
lua_pushinteger(L, entity_id); /* 2: argument */
if (lua_pcall(L, 1, 1, 0) != LUA_OK) { /* 1 arg, 1 result, no handler */
fprintf(stderr, "on_update failed: %s\n", lua_tostring(L, -1));
lua_pop(L, 1);
return 0;
}
int ok = (int)lua_toboolean(L, -1);
lua_pop(L, 1);
return ok;
}| Concern | Rule |
|---|---|
| Stack layout | Index 1 is the bottom, -1 the top; the top is the only safe place to consume |
| Errors | Wrap calls in lua_pcall or use a luaL_ helper that does it for you |
| String lifetime | A pointer from lua_tolstring stays valid only while the value is on the stack |
| Garbage collection | Values anchored on the stack are reachable; popping makes them collectable |
| Multiple states | Each state has its own globals and GC; a state must not be used from two threads at once |
| C structs | Expose them as userdata with a shared metatable for methods |
Sandboxing untrusted scripts
A script loaded by luaL_dofile can open files and read the environment. For user-supplied configuration or plugins, load the chunk with a restricted environment table instead of the globals.
-- 5.2+ / 5.4: a chunk's globals live in its _ENV upvalue
local function sandbox(code, extra)
local env = {
math = math, string = string, table = table,
ipairs = ipairs, pairs = pairs, tostring = tostring, tonumber = tonumber,
print = print, error = error, type = type,
}
for k, v in pairs(extra or {}) do env[k] = v end
local chunk, err = load(code, "sandbox", "t", env)
if not chunk then return nil, err end
local ok, result = pcall(chunk)
if not ok then return nil, result end
return result
end
local value = sandbox("return sum(2, 3)", { sum = function(a, b) return a + b end })
print(value) -- 5
local _, err = sandbox("return os.exit()")
print(err) -- sandbox:1: attempt to index a nil value (global 'os')- Omit
io,os,package,require,loadanddofilefrom the environment; each opens a door back to the host. - Set a memory ceiling with
lua_setallocf, or count instructions with thelua_sethookdebug library to stop infinite loops. - Lua's own share of the stack is not the C stack: a deep recursion inside a chunk can still exhaust the C stack when it calls into your host functions.
⚠️
Calling
lua_error, lua_call or raising inside a metamethod when no protected call is active causes a panic, and the default panic handler aborts the process. Every entry point from host code into Lua should be wrapped in lua_pcall.FAQ
How do I share state between two scripts?
They share the globals table of one
lua_State, so a plain assignment is enough. Use two states only for true isolation, and remember that values cannot be moved between states without being re-created from their C representation.Should I embed Lua or LuaJIT?
Standard Lua 5.4 for portability and simple 64-bit integers, LuaJIT when raw execution speed on x86 dominates and you can accept its older 5.1 dialect and its own FFI. Pick one for the whole project: the C API differs.
Related
Tables, metatables and object orientation Syntax and the compiled toolchain
Last refreshed 2026-09-18.