luaguides

Writing C Extensions for Lua

Introduction

Lua’s C API lets you write extension modules in C that Lua loads through require(). These modules behave exactly like Lua libraries — they expose functions, types, and constants — but the implementation runs at native code speed. This is distinct from embedding Lua in a C host program, where the C program controls the Lua interpreter. This article covers only extending Lua with C.

Common reasons to write C extensions:

  • Performance: hot paths that Lua cannot optimise sufficiently
  • Binding: wrapping an existing C library for use from Lua
  • Capability: providing features that require OS or hardware access

This article assumes you are comfortable reading and writing C code and that you know Lua at an intermediate level. You do not need to know the Lua C API.

The lua_CFunction Signature

Every C function exposed to Lua must have this exact signature:

int my_function(lua_State *L) {
    /* read arguments from stack, push results, then: */
    return n;  /* n = number of values returned to Lua */
}

The function receives a single lua_State* pointer. It reads arguments from the stack (position 1 is the first argument), performs its work, pushes return values onto the stack, and returns the count of those values. Returning 0 is valid — the function then returns nil.

The return value is not the function’s exit code. It is a contract with Lua’s call mechanism: “I am returning n values on the stack.” Lua takes those n values from the top of the stack and uses them as the function’s return values.

The Lua Stack

All communication between C and Lua happens through a single stack. C code never has direct pointers into Lua’s memory; you push values onto the stack to return them, and you read from the stack to receive arguments.

Stack Indices

The stack is indexed from 1 at the bottom and from -1 at the top:

lua_pushnumber(L, 10);   /* stack: [10]          index 1  */
lua_pushnumber(L, 20);   /* stack: [10, 20]      index 2  = -1 */
lua_pushnumber(L, 30);   /* stack: [10, 20, 30]  index 3  = -1, 2 = -2 */

Positive index 1 always refers to the first argument. Negative index -1 always refers to the top (most recently pushed). Index 0 is invalid. You can convert between the two schemes: lua_gettop(L) returns the current stack height, so the element at index i is also at lua_gettop(L) + i - 1.

Reading Arguments

Never assume the stack contains the arguments you expect. Always check:

static int l_add(lua_State *L) {
    int a = luaL_checkinteger(L, 1);  /* error if arg 1 is not an integer */
    int b = luaL_checkinteger(L, 2);  /* error if arg 2 is not an integer */
    lua_pushinteger(L, a + b);
    return 1;
}

luaL_checkinteger raises a formatted error and never returns if the argument is missing or has the wrong type. luaL_optinteger returns a default value if the argument is nil or absent. Use lua_gettop(L) to determine how many arguments were actually passed:

static int l_hypot(lua_State *L) {
    int n = lua_gettop(L);
    if (n < 2) {
        return luaL_error(L, "expected 2 arguments, got %d", n);
    }
    double a = luaL_checknumber(L, 1);
    double b = luaL_checknumber(L, 2);
    lua_pushnumber(L, sqrt(a * a + b * b));
    return 1;
}

Pushing Return Values

Push exactly the values you want to return, in order, then return N where N is the count:

static int l_divmod(lua_State *L) {
    int a = luaL_checkinteger(L, 1);
    int b = luaL_checkinteger(L, 2);
    if (b == 0) {
        return luaL_error(L, "division by zero");
    }
    lua_pushinteger(L, a / b);   /* quotient  */
    lua_pushinteger(L, a % b);   /* remainder */
    return 2;                     /* two return values */
}

Module Registration with luaL_newlib

A C extension module is a shared object (.so on Linux, .dll on Windows) that Lua loads via require("modulename"). Lua calls a specially named C function — luaopen_<modulename> — to initialise the module. That function must return a table containing the module’s public API.

The standard modern pattern uses luaL_newlib, which creates a table and registers all functions in one call:

#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

static int l_add(lua_State *L) {
    int a = luaL_checkinteger(L, 1);
    int b = luaL_checkinteger(L, 2);
    lua_pushinteger(L, a + b);
    return 1;
}

static int l_sub(lua_State *L) {
    int a = luaL_checkinteger(L, 1);
    int b = luaL_checkinteger(L, 2);
    lua_pushinteger(L, a - b);
    return 1;
}

/* Array of module functions — MUST end with {NULL, NULL} */
static const luaL_Reg mylib[] = {
    {"add", l_add},
    {"sub", l_sub},
    {NULL, NULL}
};

/* Entry point: luaopen_<modulename> */
int luaopen_mylib(lua_State *L) {
    luaL_newlib(L, mylib);
    return 1;
}

The luaL_Reg array maps Lua-facing names ("add", "sub") to C function pointers. The sentinel {NULL, NULL} marks the end of the array. Omitting it causes the linker to read garbage past the end of the array.

luaL_newlib (Lua 5.2+) combines lua_createtable(L, 0, n) with luaL_setfuncs(L, mylib, 0). In Lua 5.1, use luaL_register(L, "mylib", mylib), though luaL_register is deprecated in 5.2 and removed in 5.4.

Compile and use:

gcc -shared -fPIC -o mylib.so mylib.c $(pkg-config --cflags --libs lua5.4)
local mylib = require("mylib")
print(mylib.add(3, 4))   --> 7
print(mylib.sub(10, 3))  --> 7

The luaopen_ Convention

When you call require("mylib"), Lua’s loader looks for a file named mylib.so (or mylib.dll) in the directories listed in package.cpath. When found, it calls luaopen_mylib(L). The function name is derived directly from the filename — there is no configuration step, no registry, no metadata. If your file is vec3.so, the function must be named luaopen_vec3. This is the contract.

Lua then uses the table returned by luaopen_mylib as the value of the require expression:

local v = require("vec3")  -- calls luaopen_vec3(L), result stored in package.loaded.vec3

Stack Balance

The single most important rule in C extension writing: before your function returns, the stack must contain exactly the values you are returning.

Lua’s caller expects arguments at positions 1, 2, and so on. If your function leaves extra values on the stack, those become part of the return value unexpectedly. If it removes values that were there on entry, the caller will read garbage. Use lua_settop(L, idx) to reset the stack to a known state:

static int l_example(lua_State *L) {
    int n = lua_gettop(L);  /* save stack height */
    /* ... do work, push intermediate values ... */
    lua_settop(L, n);       /* discard everything above original top */
    lua_pushinteger(L, 42);
    return 1;                /* stack: [original args..., 42] */
}

Forgetting to return the correct count — or forgetting to return at all — is the most common source of stack leaks and incorrect return values. The C compiler does not warn you; the bug only appears at runtime from Lua’s perspective.

Custom Types with Userdata

You can expose C struct types to Lua using userdata. A userdata is a block of raw memory that Lua manages — it is garbage collected like any other Lua value, but its contents are entirely under your control.

Creating Userdata

#include <lua.h>
#include <lauxlib.h>
#include <stdlib.h>

typedef struct {
    double x, y, z;
} Vec3;

static int l_vec3_new(lua_State *L) {
    double x = luaL_optnumber(L, 1, 0.0);
    double y = luaL_optnumber(L, 2, 0.0);
    double z = luaL_optnumber(L, 3, 0.0);
    Vec3 *v = (Vec3 *)lua_newuserdata(L, sizeof(Vec3));
    v->x = x; v->y = y; v->z = z;
    return 1;  /* userdata is now on the stack as the return value */
}

Attaching a Metatable

Raw userdata has no behaviour. To make it behave like an object, attach a metatable:

static int l_vec3_dot(lua_State *L) {
    Vec3 *a = (Vec3 *)luaL_checkudata(L, 1, "Vec3");
    Vec3 *b = (Vec3 *)luaL_checkudata(L, 2, "Vec3");
    lua_pushnumber(L, a->x * b->x + a->y * b->y + a->z * b->z);
    return 1;
}

int luaopen_vec3(lua_State *L) {
    luaL_newlib(L, vec3_funcs);      /* register module functions */

    /* Create and fill the metatable for Vec3 userdata */
    luaL_newmetatable(L, "Vec3");    /* register "Vec3" metatable */
    lua_pushstring(L, "__index");
    lua_pushvalue(L, -2);            /* metatable points to itself for __index */
    lua_settable(L, -3);             /* metatable.__index = metatable */
    /* add method table as a field of the metatable so methods are accessible */
    lua_pushvalue(L, LUA_GLOBALSINDEX); /* not in 5.2+ — use upvalues instead */
    /* (simplified: expose methods directly via module table instead) */

    return 1;
}

A cleaner pattern for Lua 5.2+ is to put the methods in the module table and set __index on the metatable to point to the module table itself, so both v:dot(w) and vec3.dot(v, w) work.

Checking Userdata Type

Use luaL_checkudata to verify that a stack value is userdata of the expected type. If the value is not userdata or has the wrong metatable, it raises a formatted error:

Vec3 *v = (Vec3 *)luaL_checkudata(L, 1, "Vec3");

luaL_newudata is an alias for lua_newuserdata. Both allocate a userdata block associated with the current state.

Memory Ownership

Do Not Free Lua-Owned Memory

Never call free() or realloc() on a pointer that Lua returned to you. lua_tostring(), lua_touserdata(), and lua_newuserdata() all return pointers into memory that Lua owns. The first two return pointers to internal buffers — these are invalidated as soon as the value is removed from the stack. lua_newuserdata returns a pointer to the userdata block itself; you may write into the block but you must not free the block.

If you need a persistent copy of a Lua string:

const char *s = luaL_checkstring(L, 1);
char *copy = strdup(s);    /* you now own this memory */
 /* ... use copy ... */
free(copy);                /* free it when done */

lua_close and Memory Cleanup

When you call lua_close(L), Lua’s garbage collector runs all pending finalizers (__gc metamethods) and then frees all memory associated with that state, including all userdata and all internal allocations. If you allocate memory in C that is not tracked by Lua (e.g., via malloc for a buffer that outlasts the Lua state), free it before calling lua_close:

/* C code that allocates a buffer */
char *buffer = malloc(BUFSIZ);

/* ... use buffer from Lua via a userdata wrapping a pointer ... */

/* cleanup */
free(buffer);      /* free C memory first */
lua_close(L);      /* then close Lua state */

Building the Extension

pkg-config

The standard build tool for Lua C extensions is pkg-config. List available Lua versions:

pkg-config --list-all | grep lua

Compile a single file into a shared object:

gcc -shared -fPIC -o mylib.so mylib.c $(pkg-config --cflags --libs lua5.4)

The -shared flag tells the compiler to produce a shared object suitable for dynamic loading. -fPIC generates position-independent code, which is required on most Unix systems for shared libraries. On macOS, use -dynamiclib instead of -shared.

For LuaJIT on Debian/Ubuntu (which installs as lua5.1):

gcc -shared -fPIC -o mylib.so mylib.c $(pkg-config --cflags --libs lua5.1)

If pkg-config does not find Lua, set the paths manually:

export LUA_CFLAGS="-I/usr/include/lua5.4"
export LUA_LIBS="-L/usr/local/lib -llua5.4"
gcc -shared -fPIC -o mylib.so mylib.c $LUA_CFLAGS $LUA_LIBS

CMake for Larger Projects

For projects with multiple source files, CMake automates discovery of the Lua installation:

cmake_minimum_required(VERSION 3.10)
project(myluaext LANGUAGES C)

find_package(PkgConfig REQUIRED)
pkg_check_modules(LUA REQUIRED lua5.4)

add_library(mylib MODULE mylib.c)
target_include_directories(mylib PRIVATE ${LUA_INCLUDE_DIRS})
target_link_libraries(mylib PRIVATE ${LUA_LIBRARIES})
set_target_properties(mylib PROPERTIES PREFIX "")   # so output is mylib.so, not libmylib.so

Build:

mkdir -p build && cd build
cmake ..
make

Where Lua Finds Your Module

Lua searches for C modules in the directories listed in package.cpath. Check the default value from Lua:

print(package.cpath)

Place your compiled .so in one of those directories, or add your own directory:

package.cpath = "./?.so;" .. package.cpath
local mylib = require("mylib")

The ? is replaced with the module name. On Linux, the default typically includes ./?.so, /usr/local/lib/lua/5.4/?.so, and /usr/lib/lua/5.4/?.so.

Header Files

Include these three headers in every C extension source file, in this order:

#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
  • lua.h — state management, stack operations, VM core
  • lauxlib.h — helper functions (luaL_checkinteger, luaL_newlib, luaL_newmetatable, etc.)
  • lualib.h — declarations for the luaopen_* functions of the standard libraries

Version Differences

The Lua C API has changed between major versions:

VersionKey Change
Lua 5.1luaL_register(L, name, funcs) is the module registration function. luaL_Reg arrays include the module name.
Lua 5.2luaL_register removed. Use luaL_newlib + luaL_setfuncs. luaL_Reg no longer includes the module name. lua_newstate takes an allocator; state starts with no libraries.
Lua 5.3lua_Integer is now int64_t by default (configurable at build time). lua_pushinteger pushes a lua_Integer, not a long.
Lua 5.4State creation unchanged from 5.3. luaL_checkstack has a new flags argument (pass 0). The auxiliary library is largely stable.

Use the LUA_VERSION_NUM macro for compile-time version checks:

#if LUA_VERSION_NUM == 503
    /* Lua 5.3-specific code */
#elif LUA_VERSION_NUM == 504
    /* Lua 5.4-specific code */
#endif

Common Pitfalls

Forgetting the return count. If you push two values and return 1, Lua receives only the first. If you return 0 by mistake, the function returns nil.

Leaving the stack dirty on error. If your function calls luaL_error (which never returns), any values you pushed before the error remain on the stack. This is usually harmless because the error unwinds the call stack, but if you allocate resources during the function, clean them up before raising an error.

Dangling pointers from lua_tostring. The pointer returned by lua_tostring points into Lua’s internal string buffer. It is valid only until the string is garbage collected, which typically happens when it is popped from the stack. Store a copy with strdup if you need it to outlive the stack operation.

Mismatched metatable names. When you register a metatable with luaL_newmetatable(L, "Vec3"), you must use exactly the same string when checking with luaL_checkudata(L, idx, "Vec3"). Typos produce silent type check failures.

Calling convention on Windows. The lua_CFunction type uses the C calling convention. If you declare a function as __stdcall by accident, Lua will crash when it tries to call it. Ensure your extension module is compiled with a compatible calling convention.

Module file name vs function name. If your source file is vec3.c and you compile to vec3.so, the entry point must be luaopen_vec3. If you rename the compiled file to math3.so, Lua looks for luaopen_math3 and will fail to load the module.

A Complete Worked Example

File vec3.c:

#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
#include <math.h>
#include <stdlib.h>

typedef struct {
    double x, y, z;
} Vec3;

static int l_vec3_new(lua_State *L) {
    double x = luaL_optnumber(L, 1, 0.0);
    double y = luaL_optnumber(L, 2, 0.0);
    double z = luaL_optnumber(L, 3, 0.0);
    Vec3 *v = (Vec3 *)lua_newuserdata(L, sizeof(Vec3));
    v->x = x; v->y = y; v->z = z;
    luaL_getmetatable(L, "Vec3");
    lua_setmetatable(L, -2);
    return 1;
}

static int l_vec3_dot(lua_State *L) {
    Vec3 *a = (Vec3 *)luaL_checkudata(L, 1, "Vec3");
    Vec3 *b = (Vec3 *)luaL_checkudata(L, 2, "Vec3");
    lua_pushnumber(L, a->x * b->x + a->y * b->y + a->z * b->z);
    return 1;
}

static int l_vec3_cross(lua_State *L) {
    Vec3 *a = (Vec3 *)luaL_checkudata(L, 1, "Vec3");
    Vec3 *b = (Vec3 *)luaL_checkudata(L, 2, "Vec3");
    Vec3 *r = (Vec3 *)lua_newuserdata(L, sizeof(Vec3));
    r->x = a->y * b->z - a->z * b->y;
    r->y = a->z * b->x - a->x * b->z;
    r->z = a->x * b->y - a->y * b->x;
    luaL_getmetatable(L, "Vec3");
    lua_setmetatable(L, -2);
    return 1;
}

static const luaL_Reg vec3_funcs[] = {
    {"new",   l_vec3_new},
    {"dot",   l_vec3_dot},
    {"cross", l_vec3_cross},
    {NULL, NULL}
};

int luaopen_vec3(lua_State *L) {
    luaL_newlib(L, vec3_funcs);

    /* Create metatable for Vec3 userdata objects */
    luaL_newmetatable(L, "Vec3");
    lua_pushstring(L, "__index");
    lua_pushvalue(L, -2);
    lua_settable(L, -3);     /* metatable.__index = metatable */

    return 1;
}

Build:

gcc -shared -fPIC -o vec3.so vec3.c -lm $(pkg-config --cflags --libs lua5.4)

Use from Lua:

local vec3 = require("vec3")

local a = vec3.new(1, 0, 0)
local b = vec3.new(0, 1, 0)

print(vec3.dot(a, b))            --> 0

local c = vec3.cross(a, b)
print(string.format("%.1f, %.1f, %.1f", c.x, c.y, c.z))

Further Reading

This article covered the mechanics of writing and loading C extensions. The topics not addressed here — which go beyond a single tutorial — include: calling Lua functions from C (C as a Lua host), creating C closures with upvalues via lua_pushcclosure, the full userdata metatable API (__gc, __len, __pairs, etc.), managing multiple Lua states simultaneously, and coroutine integration with the C stack.

Consult the Lua 5.4 reference manual for the complete API specification, and the PiL book (particularly its C API chapters) for broader context on embedding and extending.