Skip to content
The C API Mental Model
step 1/4

Reading — step 1 of 4

Learn

~2 min readGC, JIT, and the C Bridge

Lua is embeddable. Its C API is famously small (~250 functions), stack-based, and the reason Lua is everywhere from Redis to ROS to game consoles.

The Lua stack:

The API exposes Lua values through a virtual stack indexed by integers. Positive indices count from the bottom (1, 2, 3...); negative indices from the top (-1, -2, -3...).

// C code calling into Lua:
lua_State *L = luaL_newstate();      // create VM
luaL_openlibs(L);                     // load standard library

luaL_dostring(L, "return 1 + 2");    // run code, leaves result on stack
int result = lua_tointeger(L, -1);    // top of stack
printf("%d\n", result);

lua_pop(L, 1);                        // remove from stack
lua_close(L);

Calling Lua functions from C:

lua_getglobal(L, "math");      // push math table
lua_getfield(L, -1, "sqrt");   // push math.sqrt onto stack
lua_pushnumber(L, 16.0);       // push argument
lua_call(L, 1, 1);             // 1 arg, 1 return
double r = lua_tonumber(L, -1);

Exposing C functions to Lua:

static int myAdd(lua_State *L) {
    double a = luaL_checknumber(L, 1);
    double b = luaL_checknumber(L, 2);
    lua_pushnumber(L, a + b);
    return 1;     // number of return values
}

// Register:
lua_pushcfunction(L, myAdd);
lua_setglobal(L, "myAdd");

In Lua: print(myAdd(3, 4))7.

Mental model:

  • Lua side: cooperative, no preemption.
  • C side: full control. Long C functions block Lua entirely.
  • Communication is data on the stack — push args, call, read results.
  • C errors must use luaL_error (which longjmps); never return -1 or set errno.

Userdata for opaque C state:

// Allocate a struct on Lua's GC heap:
MyStruct *p = (MyStruct *)lua_newuserdata(L, sizeof(MyStruct));
p->field = 42;

// Attach a metatable for method dispatch / __gc finalizer.
luaL_setmetatable(L, "MyStructMT");

Real embedding examples:

  • Redis: lua_State per connection, sandboxed env, restricted libs
  • Roblox/Luau: forked Lua, custom typed extension
  • Neovim: Lua VM bound to editor APIs
  • Wireshark dissectors, nginx/OpenResty modules, Premake build scripts

LuaJIT FFI (Lua-side, no C needed):

local ffi = require("ffi")
ffi.cdef[[
    int printf(const char *fmt, ...);
]]
ffi.C.printf("hello from FFI\n")

LuaJIT's FFI lets you declare C signatures in Lua and call them directly — way faster than wrapping each in a C binding.

We can't run C in this sandbox; this lesson is conceptual. Practice the mental model: stack discipline, push/pop, return-count.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…