luaguides

LuaJIT FFI: Calling C from Lua

LuaJIT FFI lets you call C code and work with C data structures directly from Lua. No C extension modules, no lua_CFunction boilerplate. If you have a C library lying around, the FFI lets you use it from Lua almost as if it were native code.

This only works in LuaJIT. Standard Lua 5.4 has no FFI. The rest of this guide assumes you’re running LuaJIT 2.1.

The three-step pattern

Every FFI interaction follows the same sequence:

  1. require("ffi") — load the FFI library
  2. ffi.cdef(...) — declare C types and function signatures
  3. ffi.load(...) — load the C library
  4. Call functions through the loaded namespace
local ffi = require("ffi")

ffi.cdef([[
    int printf(const char *fmt, ...);
    double sin(double x);
]])

local C = ffi.load("c")

C.printf("sin(0.5) = %f\n", C.sin(0.5))

By convention, the namespace for the standard C library is named C. For other libraries, use a descriptive name like mylib or zmq. The name you choose becomes a Lua table populated with every C function and variable declared in your cdef block, so pick something that reads naturally in the surrounding code.

Loading Libraries

ffi.load accepts a library name or an absolute path.

local C = ffi.load("c")                        -- standard C library
local mylib = ffi.load("/usr/local/lib/libmylib.so")  -- custom .so / .dll / .dylib

-- third argument = true: symbols resolved globally (like dlsym(RTLD_DEFAULT))
local C = ffi.load("c", true)

When you pass a bare name like "c", LuaJIT searches standard library paths automatically. On Linux that includes libc.so.6; on macOS it’s the dylib equivalent.

Declaring C types and functions

ffi.cdef takes a string containing C declarations. Everything you want to use must be declared before you call it.

ffi.cdef([[
    void *malloc(size_t size);
    void free(void *ptr);
    size_t strlen(const char *s);
    int strcmp(const char *s1, const char *s2);
]])

You can declare structs, enums, unions, and function pointers the same way, all within a single cdef call or spread across multiple calls that accumulate declarations. Struct and enum definitions follow standard C syntax — the FFI parser understands the full C declaration grammar, including nested structs and anonymous unions.

ffi.cdef([[
    typedef struct {
        int x;
        int y;
    } point_t;

    typedef enum {
        RED   = 0,
        GREEN = 1,
        BLUE  = 2
    } color_t;

    typedef int (*compare_fn)(const void *, const void *);
]])

A few things to keep in mind:

  • Always declare structs and function signatures before using them.
  • Enumerations are declared inline with typedef enum { ... } name_t;.
  • Function pointer types need (*name) — a plain typedef without the asterisk does not work for callbacks.

Allocating C Data with ffi.new

ffi.new allocates C data objects (cdata). These live on the LuaJIT-managed stack or heap depending on their size.

local i = ffi.new("int")           -- single int, zero-initialized
local i = ffi.new("int", 42)       -- with initial value

local p = ffi.new("point_t")       -- struct, zeroed out
p.x = 10
p.y = 20

local arr = ffi.new("int[10]")     -- C array of 10 ints
arr[0] = 1
arr[1] = 2

-- Initialize an array from a Lua table
local arr = ffi.new("int[3]", {1, 2, 3})

Stack-allocated cdata is automatically reclaimed when it goes out of scope. Heap-allocated cdata (larger objects) is garbage-collected normally by LuaJIT. One exception: memory you allocate manually with ffi.C.malloc is not automatically freed — you must pair every malloc with a corresponding free call, or attach a finalizer to handle cleanup.

Memory Management and ffi.gc

If you call ffi.C.malloc directly, you own that memory. LuaJIT will not free it for you.

ffi.cdef("void *malloc(size_t size); void free(void *ptr);")

local buf = ffi.C.malloc(256)
if ffi.errnull(buf) then error("malloc failed") end

-- attach a finalizer so it gets freed automatically
buf = ffi.gc(buf, ffi.C.free)

ffi.gc(addr, finalizer) attaches a destructor to a cdata pointer. When LuaJIT garbage-collects that object, it runs the finalizer. This pattern is much safer than manually tracking every malloc — you wrap the raw pointer once and let the GC handle the rest. The finalizer can be any function, but ffi.C.free is by far the most common choice.

You can also use ffi.gc with ffi.new for heap-allocated objects, though ffi.new already manages its own lifetime for stack-sized allocations:

local arr = ffi.gc(ffi.C.malloc(ffi.sizeof("int[100]")), ffi.C.free)

Working with Strings

C char pointers are not Lua strings. Two functions bridge the gap, and getting them confused with Lua’s native string handling is one of the most common FFI mistakes. A const char * in C maps to a cdata pointer in Lua — passing it to print() will show an address, not the text.

ffi.string converts a C string (or fixed-length buffer) to a Lua string:

local c_str = ffi.string(c_char_ptr)       -- null-terminated
local c_str = ffi.string(c_char_ptr, 10)   -- exactly 10 bytes

ffi.copy copies into a C buffer, which is especially useful for filling in struct fields that expect fixed-length character arrays. Unlike ffi.string, which reads out of C memory, ffi.copy writes into it. A common pattern in FFI-heavy code is to declare a struct with a char array field, use ffi.copy to populate it from a Lua string, then pass the struct to a C function that operates on it.

ffi.cdef("typedef struct { char name[32]; } person_t;")

local p = ffi.new("person_t")
ffi.copy(p.name, "Alice", #"Alice" + 1)  -- +1 for null terminator
print(ffi.string(p.name))  --> Alice

Do not pass a Lua table where a C array is expected. Tables are not arrays in C memory. Lua and C have fundamentally different memory layouts — a Lua table is a hash map stored on the Lua heap with no contiguous element storage, while a C array is a flat block of bytes at a fixed address. The FFI cannot bridge this gap automatically, so you must explicitly allocate cdata arrays whenever a C function signature calls for one.

-- WRONG
local t = {1, 2, 3}
some_c_function(t)  -- passes a table, not an int[3]

-- CORRECT
local arr = ffi.new("int[3]", t)
some_c_function(arr)

Callbacks: Lua functions called from C

When a C library needs a callback — for example, qsort needs a comparison function — you must cast a Lua function to a C function pointer. Plain Lua functions cannot be passed directly. The FFI needs to know the exact calling convention, argument types, and return type to generate the trampoline that bridges Lua’s stack-based calling model to C’s register-and-stack convention.

ffi.cdef([[
    void qsort(void *base, size_t nmemb, size_t size,
               int (*cmp)(const void *, const void *));
]])

local C = ffi.load("c")

local function lua_compare(a, b)
    local x = ffi.cast("int *", a)[0]
    local y = ffi.cast("int *", b)[0]
    if x < y then return -1
    elseif x > y then return 1
    else return 0 end
end

local cmp_fn = ffi.cast("int (*)(const void *, const void *)", lua_compare)

local arr = ffi.new("int[5]", {5, 2, 8, 1, 9})
C.qsort(arr, 5, ffi.sizeof("int"), cmp_fn)
for i = 0, 4 do print(arr[i]) end

The key is ffi.cast, which converts the Lua function into a callable C function pointer with the matching signature. The cast does not create a wrapper object that you need to manage — the resulting cdata behaves like a native function pointer and can be passed to any C function that expects that callback type. Note that the Lua function must remain reachable for the lifetime of any pending callbacks; if it gets garbage-collected, a dangling function pointer results in undefined behavior.

Accessing global C variables

Global variables declared in a header can be read and written through the library namespace:

ffi.cdef("extern int global_counter;")
print(C.global_counter)  -- read
C.global_counter = 42     -- write

Platform Detection

ffi.os and ffi.arch let you branch on the current platform. These are read-only strings that LuaJIT sets at startup, reflecting the operating system and CPU architecture of the machine running the code. This is essential when you need to load different shared libraries on Linux versus macOS, or choose between 32-bit and 64-bit struct layouts at runtime:

print("OS: " .. ffi.os)    -- linux, osx, windows, bsd
print("Arch: " .. ffi.arch) -- x64, arm64, arm, mips

if ffi.abi("le") then
    print("Little-endian")
end

This is useful when loading different libraries per platform or choosing between struct layouts.

Common Gotchas

NYI errors when passing large structs by value. LuaJIT’s FFI has unimplemented cases for structs passed by value to functions. If you hit a NYI error, pass a pointer instead: struct_t *.

Type truncation silently. Passing a Lua number to a C function expecting a smaller integer type truncates without warning. Use ffi.cast for explicit conversions.

void * loses type information. A void * pointer cannot have metamethods. Any type-erased pointer won’t support __tostring or custom arithmetic.

NULL checks. Use ffi.errnull(ptr) rather than comparing to nil. cdata pointers compare with nil but ffi.errnull is unambiguous for pointer types.

Varargs are partially supported. Basic ... in C functions works, but complex varargs with structs are unreliable.

See Also