LuaJIT FFI: Calling C from Lua
· 6 min read · Updated April 1, 2026 · intermediate
lua luajit ffi c-interop
LuaJIT ships with a Foreign Function Interface (FFI) library that 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:
require("ffi")— load the FFI libraryffi.cdef(...)— declare C types and function signaturesffi.load(...)— load the C library- 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))
```lua
By convention, the namespace for the standard C library is named `C`. For other libraries, use a descriptive name.
### Loading Libraries
`ffi.load` accepts a library name or an absolute path.
```lua
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)
```lua
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.
```lua
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);
]])
```lua
You can declare structs, enums, unions, and function pointers the same way.
```lua
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 *);
]])
```lua
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.
```lua
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})
```lua
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.
## Memory Management and ffi.gc
If you call `ffi.C.malloc` directly, you own that memory. LuaJIT will not free it for you.
```lua
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)
```lua
`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 can also use `ffi.gc` with `ffi.new` for heap-allocated objects:
```lua
local arr = ffi.gc(ffi.C.malloc(ffi.sizeof("int[100]")), ffi.C.free)
```lua
## Working with Strings
C char pointers are not Lua strings. Two functions bridge the gap.
`ffi.string` converts a C string (or fixed-length buffer) to a Lua string:
```lua
local c_str = ffi.string(c_char_ptr) -- null-terminated
local c_str = ffi.string(c_char_ptr, 10) -- exactly 10 bytes
```lua
`ffi.copy` copies into a C buffer. Useful for filling in struct fields:
```lua
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
```lua
Do not pass a Lua table where a C array is expected. Tables are not arrays in C memory.
```lua
-- 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)
```lua
## 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.
```lua
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
```lua
The key is `ffi.cast`, which converts the Lua function into a callable C function pointer with the matching signature.
## Accessing Global C Variables
Global variables declared in a header can be read and written through the library namespace:
```lua
ffi.cdef("extern int global_counter;")
print(C.global_counter) -- read
C.global_counter = 42 -- write
```lua
## Platform Detection
`ffi.os` and `ffi.arch` let you branch on the current platform:
```lua
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
```lua
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
- [/tutorials/lua-c-api-basics/](/tutorials/lua-c-api-basics/) — if you need to write C extension modules the traditional way
- [/guides/lua-binary-data/](/guides/lua-binary-data/) — working with binary formats and structured data