The Lua C API, lua_State, calling Lua from C, calling C from Lua, coroutines for cooperative multitasking, and sandboxing untrusted scripts.
Lua is designed to be embedded. The entire runtime is a single C library (liblua). You interact with it through a stack-based API: push values onto the Lua stack, call functions, and pop results. All communication between C and Lua flows through this stack.
brew install lua (macOS) or apt install liblua5.4-dev (Ubuntu). Compile with gcc main.c -llua -lm -o app.#include <stdio.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
int main(void) {
// 1. Create a Lua state (the interpreter)
lua_State *L = luaL_newstate();
if (!L) { fprintf(stderr, "out of memory\n"); return 1; }
// 2. Open standard libraries (print, math, string, etc.)
luaL_openlibs(L);
// 3. Execute a Lua file
int err = luaL_dofile(L, "config.lua");
if (err) {
// The error message is on top of the stack
fprintf(stderr, "Error: %s\n", lua_tostring(L, -1));
lua_pop(L, 1);
}
// 4. Execute a string of Lua code
luaL_dostring(L, "print('Hello from C!')");
// 5. Read a global variable from Lua
lua_getglobal(L, "my_value"); // push _G["my_value"]
if (lua_isnumber(L, -1)) {
double v = lua_tonumber(L, -1);
printf("my_value = %g\n", v);
}
lua_pop(L, 1); // pop the value
// 6. Cleanup
lua_close(L);
return 0;
}
// Lua script loaded earlier defines:
// function add(a, b) return a + b end
// Call add(10, 32) from C and get the result
lua_getglobal(L, "add"); // push function
lua_pushnumber(L, 10); // push arg 1
lua_pushnumber(L, 32); // push arg 2
// lua_pcall(L, nargs, nresults, error_handler_index)
// Returns LUA_OK (0) on success
if (lua_pcall(L, 2, 1, 0) != LUA_OK) {
fprintf(stderr, "call error: %s\n", lua_tostring(L, -1));
lua_pop(L, 1);
} else {
double result = lua_tonumber(L, -1);
printf("add(10, 32) = %g\n", result); // 42.0
lua_pop(L, 1);
}
// Always use lua_pcall (protected call) rather than lua_call
// lua_pcall catches Lua errors; lua_call panics on error
// A C function callable from Lua must have this signature:
// typedef int (*lua_CFunction)(lua_State *L);
// Arguments come from the stack; return values go onto the stack.
// Return value = number of results pushed.
static int c_sqrt(lua_State *L) {
// luaL_checknumber: get arg 1 as a number (errors if wrong type)
double n = luaL_checknumber(L, 1);
lua_pushnumber(L, sqrt(n));
return 1; // one result
}
static int c_greet(lua_State *L) {
const char *name = luaL_checkstring(L, 1);
lua_pushfstring(L, "Hello, %s!", name);
return 1;
}
// Register a library of C functions
static const luaL_Reg mylib[] = {
{"sqrt", c_sqrt},
{"greet", c_greet},
{NULL, NULL} // sentinel
};
// Call this once after luaL_newstate()
static void register_mylib(lua_State *L) {
luaL_newlib(L, mylib); // create table with functions
lua_setglobal(L, "mylib"); // _G["mylib"] = table
}
// Now in Lua:
// print(mylib.sqrt(16)) --> 4.0
// print(mylib.greet("world")) --> Hello, world!
Lua coroutines are cooperative threads — they run on a single OS thread, pausing with coroutine.yield() and resuming with coroutine.resume(). They're perfect for game AI, async-style I/O, and iterators.
-- Producer / consumer with coroutines
local function producer()
local items = {"apple", "banana", "cherry", "date"}
for _, item in ipairs(items) do
coroutine.yield(item) -- pause and send value to consumer
end
return "done" -- final return value
end
local co = coroutine.create(producer)
while true do
local ok, value = coroutine.resume(co)
if not ok then
print("error:", value)
break
end
if coroutine.status(co) == "dead" then
print("producer finished:", value)
break
end
print("received:", value)
end
-- received: apple
-- received: banana
-- received: cherry
-- received: date
-- producer finished: done
-- Coroutine as a generator (wrap makes it iterator-like)
local function range(from, to, step)
step = step or 1
return coroutine.wrap(function()
for i = from, to, step do
coroutine.yield(i)
end
end)
end
for n in range(1, 10, 2) do
io.write(n .. " ")
end
-- 1 3 5 7 9
-- Build a restricted environment for untrusted code
local function make_sandbox()
return {
-- Safe standard library subset
print = print,
pairs = pairs,
ipairs = ipairs,
next = next,
type = type,
tostring = tostring,
tonumber = tonumber,
math = {
abs=math.abs, floor=math.floor, ceil=math.ceil,
min=math.min, max=math.max, sqrt=math.sqrt,
random=math.random, pi=math.pi,
},
string = {
format=string.format, len=string.len,
sub=string.sub, upper=string.upper, lower=string.lower,
gmatch=string.gmatch, gsub=string.gsub,
},
table = {
insert=table.insert, remove=table.remove,
sort=table.sort, concat=table.concat,
},
-- NO: io, os, require, load, dofile, loadfile, package
}
end
local function run_sandboxed(code, sandbox)
local fn, err = load(code, "sandbox", "t", sandbox)
if not fn then return false, err end
local ok, result = pcall(fn)
return ok, result
end
local sandbox = make_sandbox()
local code = [[
local sum = 0
for i = 1, 10 do sum = sum + i end
return sum
]]
local ok, result = run_sandboxed(code, sandbox)
print(ok, result) --> true 55
-- Attempt to break out
local bad = "return io.open('/etc/passwd')"
local ok2, err2 = run_sandboxed(bad, sandbox)
print(ok2, err2) --> false [string "sandbox"]:1: attempt to index a nil value (global 'io')
lua_sethook in C with a count hook to inject a check every N instructions, and abort if a time limit is exceeded.config.lua file at startup. The config defines server settings (host, port, max_connections) as a Lua table. Parse them in C with the stack API.plugins/ directory. At startup, scan it with opendir/readdir and luaL_dofile each .lua file. Each plugin is expected to expose an on_request(path) function.on_request function, call it via lua_pcall, and print the return value.coroutine.wrap-based generator in Lua that the C host can call repeatedly to stream a large response in chunks.You went from syntax to tables, metatables, a playable game, and a C embedding. Lua is now in your toolkit — the scripting layer that makes every C/C++ application extensible.
Take the Live Bootcamp →Completing all five days means having a solid working knowledge of Lua. The skills here translate directly to real projects. The next step is practice — pick a project and build something with what was learned.
Before moving on, verify you can answer these without looking:
Live Bootcamp
Learn this in person — 2 days, 5 cities
Thu–Fri sessions in Denver, Los Angeles, New York, Chicago, and Dallas. $1,490 per seat. June–October 2026.
Reserve Your Seat →