Full Userdata and Light Userdata: Metatables in Lua
Prerequisites
You need a C compiler that links against Lua 5.2 or later and a working Lua installation. You should already be comfortable with the Lua C API basics: creating a lua_State, pushing values onto the stack, and calling Lua functions from C. If any of that sounds unfamiliar, start with the embedding Lua in C tutorial before tackling userdata and metatables. The examples in this tutorial use pseudocode in some places to focus on the conceptual patterns, but every pattern shown here works with a real C compiler and a proper Lua installation.
Full userdata — C objects in Lua
Lua offers two kinds of userdata for bridging C and Lua code, and understanding the difference between full userdata and light userdata is essential for writing safe C extensions. Full userdata is a block of raw memory that Lua manages. You create it from C, not Lua. This is the mechanism that lets your C code hand ownership of heap-allocated structures over to Lua’s garbage collector. Your C objects then behave like native Lua values with automatic memory management.
#include <lua.h>
#include <lauxlib.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
typedef struct {
char filename[256];
FILE *handle;
} FileData;
// Pseudocode: demonstrates the pattern, not a complete implementation
int l_createfile(lua_State *L) {
const char *filename = luaL_checkstring(L, 1);
// Allocate memory (Lua's GC will manage this)
FileData *f = (FileData *)lua_newuserdata(L, sizeof(FileData));
// Initialize the data
strncpy(f->filename, filename, sizeof(f->filename) - 1);
f->filename[sizeof(f->filename) - 1] = '\0';
f->handle = fopen(filename, "r");
// Set up metatable for operations
luaL_newmetatable(L, "FileMeta");
lua_setmetatable(L, -2);
return 1; // Return the userdata on the stack
}
When you call this C function from Lua, you get back a userdata value that wraps your FileData struct. To Lua script code, this value looks like an opaque handle. You cannot inspect its fields from Lua without writing accessor functions in C. The identity semantics are what make userdata useful as handles. You can pass the same object around and compare it against itself to confirm you are dealing with the same resource. Two separately created userdata values are never equal, even if their C structs contain identical data, because Lua compares them by their memory address, not by their content. The following Lua pseudocode illustrates this identity comparison.
-- Lua: Understanding userdata identity
-- In practice, you'd call this through a C module: require("file").open("test.txt")
-- The key concept: userdata compares by identity, not by content
-- Pseudocode showing the behavior:
-- local file1 = some_c_function("test.txt")
-- local file2 = some_c_function("test.txt")
-- file1 == file1 -- true (same object)
-- file2 == file2 -- true (same object)
-- file1 == file2 -- false (different objects)
This identity-based comparison is essential when userdata represents system resources like file handles or network sockets. You always know that comparing a userdata against itself returns true, which makes it safe to use as a dictionary key or to track which resources are already open. The garbage collector handles full userdata automatically. When no references remain, Lua calls the __gc metamethod before freeing memory. For long-running applications, this means you can trust Lua to release C resources without manual cleanup, provided you set up the finalizer correctly.
Light userdata — raw C pointers
Light userdata is just a pointer value (void*). No memory allocation happens.
#include <lua.h>
// Pseudocode: demonstrates pushing light userdata
void push_pointer(lua_State *L, void *somePointer) {
lua_pushlightuserdata(L, somePointer);
}
The function above is about as simple as the Lua C API gets. It takes a void* pointer from your C code and pushes it onto the Lua stack as a light userdata value. Unlike full userdata, there is no memory allocation happening inside Lua and no metatable is attached. The pointer you provide is stored as-is. This makes light userdata extremely cheap to create. It also means you are entirely responsible for the lifetime of whatever the pointer points to. If you free the memory on the C side while Lua still holds a light userdata referencing it, you get a dangling pointer and undefined behaviour.
Light userdata compares by pointer value:
-- Lua: Light userdata behavior
-- Light userdata can only be created from C — there's no pure-Lua constructor
-- When returned from C, they compare by pointer value:
-- Pseudocode showing the behavior:
-- local ptr1 = some_c_function_returning_lightuserdata(0x00400000)
-- local ptr2 = some_c_function_returning_lightuserdata(0x00400000)
-- local ptr3 = some_c_function_returning_lightuserdata(0x00401000)
-- ptr1 == ptr2 -- true (same address)
-- ptr1 == ptr3 -- false (different address)
Because light userdata holds raw addresses, two values are equal only when they point to the exact same location in memory, even if they point to separate blocks that contain identical data. This property makes light userdata convenient as unique identifiers or opaque tokens in event systems, where you want to distinguish one C-side object from another without exposing the full object to Lua. The trade-off is that you, the C programmer, must coordinate cleanup. If the C code frees a buffer while Lua still references its light userdata, the next use will dereference freed memory, which is a hard crash with no Lua-level error to catch.
Key difference: Lua doesn’t manage memory for light userdata. You handle cleanup yourself in C.
| Feature | Full Userdata | Light Userdata |
|---|---|---|
| Memory managed by Lua | Yes | No |
| Individual metatable | Yes | No |
| Custom operations | Yes | No |
Metatables — adding behavior to values
Every Lua value can have a metatable that defines what happens during operations. Use setmetatable to attach one.
-- Create a table and give it a metatable
local point = setmetatable(
{ x = 10, y = 20 },
{
__tostring = function(self)
return "(" .. self.x .. ", " .. self.y .. ")"
end
}
)
print(point) -- "(10, 20)"
The metatable is just a regular table with special keys (metamethods) starting with __. When Lua encounters an operation on a value, such as addition, comparison, or key access, it checks the value’s metatable for the corresponding metamethod. If found, Lua calls that function instead of performing the default operation. This is how a plain table suddenly knows how to represent itself as a string or how two tables can be added together. The metatable system is what makes object-oriented patterns possible in Lua without dedicated class syntax.
The __index metamethod
__index fires when you access a key that doesn’t exist in the table. It’s the foundation for OOP in Lua and the single most-used metamethod. Every time you write obj.method() in a Lua class system, __index is doing the work behind the scenes. It’s the foundation for OOP in Lua.
-- Default values pattern
local defaults = { greeting = "Hello", count = 0 }
local config = setmetatable(
{ name = "myapp" },
{ __index = defaults }
)
print(config.name) -- "myapp" (exists in config)
print(config.greeting) -- "Hello" (falls back to defaults)
print(config.count) -- 0 (falls back to defaults)
Setting __index to a table is the most common pattern because table lookups are faster than function calls and require no extra stack frames. The table is a direct fallback, checked by the Lua VM itself without any function dispatch overhead. Lua checks the fallback table directly, without invoking any function. This technique underpins class-based inheritance in Lua. You set the metatable’s __index to point to the parent class table, and any method or field not found in the instance automatically resolves through the parent. The same mechanism works for providing default configuration values, as shown above . Missing keys silently resolve to their defaults without extra code.
The __newindex metamethod
__newindex fires when you set a key that doesn’t exist. Use it to create read-only tables or log changes.
-- Read-only table
local function readonly(t)
local proxy = {}
setmetatable(proxy, {
__index = t,
__newindex = function(_, key, _)
error("Cannot write to read-only table: " .. tostring(key), 2)
end
})
return proxy
end
local config = readonly({ host = "localhost", port = 8080 })
print(config.host) -- "localhost"
config.port = 9000 -- Error: Cannot write to read-only table: port
The read-only proxy pattern works because __newindex intercepts every write to a key that does not already exist in the proxy table. Since the proxy starts empty, all writes trigger the error. This is a clean way to enforce immutability without copying data. The original table sits untouched as the __index fallback, and the proxy simply refuses writes. You can also log writes conditionally or validate values before accepting them, all from within the __newindex handler.
Inside __newindex, use rawset to avoid infinite recursion:
local observed = setmetatable({}, {
__newindex = function(t, key, value)
print("Setting " .. key .. " = " .. tostring(value))
rawset(t, key, value) -- Must use rawset!
end
})
observed.name = "test" -- Prints: Setting name = test
Calling rawset(t, key, value) bypasses the metatable entirely and writes directly into the table t. Without it, the assignment inside __newindex would trigger __newindex again, creating infinite recursion until Lua runs out of stack space. This is a common pitfall when writing metamethods that modify the object they are attached to. Any write that could trigger the same metamethod must go through rawset, and any read that could trigger __index must go through rawget.
Making objects callable with __call
local Counter = {}
Counter.__index = Counter
function Counter.new(initial)
return setmetatable({ value = initial or 0 }, Counter)
end
function Counter:__call()
self.value = self.value + 1
return self.value
end
local count = Counter.new(10)
print(count()) -- 11
print(count()) -- 12
print(count()) -- 13
The __call metamethod transforms any table into a callable object. When Lua sees count(), it looks up the metatable, finds __call, and invokes it with the table as the first argument. This pattern is remarkably useful for creating function objects that carry state. A counter that increments on each call is a simple example, but the same pattern works for building middleware pipelines, lazy iterators, or mock objects in tests. The callable interface keeps the syntax familiar while letting you attach persistent data to what looks like an ordinary function.
Operator overloading
Metatables let you define behavior for operators.
local Vector = {}
Vector.__index = Vector
function Vector.new(x, y)
return setmetatable({ x = x, y = y }, Vector)
end
function Vector.__add(a, b)
return Vector.new(a.x + b.x, a.y + b.y)
end
function Vector.__mul(v, n)
return Vector.new(v.x * n, v.y * n)
end
function Vector:__tostring()
return string.format("(%g, %g)", self.x, self.y)
end
function Vector:__eq(a, b)
-- Note: Lua's identity check runs first
-- v1 == v1 returns true without calling __eq
-- __eq only fires for v1 == v2 when they're different objects
if a == b then return true end -- Identity check
return a.x == b.x and a.y == b.y
end
-- Usage
local v1 = Vector.new(1, 2)
local v2 = Vector.new(3, 4)
local v3 = v1 + v2
print(v3) -- "(4, 6)"
print(v1 * 2) -- "(2, 4)"
print(v1 == v2) -- false
print(v1 == v1) -- true (identity check, __eq not called)
A subtle detail worth noting is that Lua always performs an identity check before calling __eq. When you write v1 == v1, Lua sees that both operands are the exact same table and returns true without ever invoking the metamethod. This prevents unnecessary function calls and avoids edge cases where a buggy __eq implementation could claim an object is not equal to itself. The __eq metamethod only fires when the two operands share the same metatable and are different objects, which keeps the comparison system predictable.
Garbage collection with __gc (Lua 5.2+)
The __gc metamethod runs when Lua reclaims a userdata. Use it to clean up C resources.
#include <lua.h>
#include <lauxlib.h>
#include <stdlib.h>
#include <stdio.h>
typedef struct {
void *data;
size_t size;
} Buffer;
static int buffer_finalizer(lua_State *L) {
Buffer *buf = (Buffer *)lua_touserdata(L, 1);
if (buf && buf->data) {
free(buf->data);
printf("Buffer freed\n");
}
return 0;
}
// Pseudocode: demonstrates the pattern
int l_createbuffer(lua_State *L) {
Buffer *buf = (Buffer *)lua_newuserdata(L, sizeof(Buffer));
buf->data = malloc(1024);
if (!buf->data) {
return luaL_error(L, "Failed to allocate buffer");
}
buf->size = 1024;
// Create metatable with __gc
luaL_newmetatable(L, "BufferMeta");
lua_pushstring(L, "__gc");
lua_pushcfunction(L, buffer_finalizer);
lua_settable(L, -3);
lua_setmetatable(L, -2);
return 1;
}
The __gc metamethod gives you a reliable way to pair Lua’s garbage collection with C resource management. It is worth understanding that __gc is only available on userdata (not tables), and it is called exactly once per userdata object when the collector determines the object is unreachable. When the last reference to a userdata is removed and the garbage collector runs, Lua calls your C finalizer before actually freeing the memory block. This is how you close file handles, free malloc’d buffers, or disconnect network sockets without requiring the Lua programmer to remember to call a cleanup function. The finalizer receives the userdata as its only argument, so you can cast it back to your C struct and release whatever resources it holds.
In Lua 5.4, use __close for scope-based cleanup:
-- Lua 5.4: __close for automatic cleanup
-- Pseudocode: demonstrates the __close metamethod pattern
-- In practice, you'd use io.open or a C function that sets __close
-- Example of the pattern (won't run without a proper C implementation):
-- local function with_file(filename)
-- local file = some_c_function_that_returns_closable(filename)
-- return file
-- end
--
-- local f = with_file("data.txt")
-- When 'f' goes out of scope, __close is called automatically
-- (This is pseudocode demonstrating the concept)
The __close metamethod works differently from __gc. It triggers when a variable goes out of scope, not when the garbage collector runs. This makes it deterministic and predictable, which is exactly what you want for resources like file handles or locks. A value with __close set can be used with a <close> attribute on a local variable declaration, and Lua guarantees the metamethod runs when that variable’s scope ends, even if an error is thrown. This is Lua 5.4’s answer to Python’s with statement or C#‘s using block. It provides scope-based resource management without relying on GC timing.
Version differences
| Feature | Lua 5.1 | Lua 5.2+ | Lua 5.3+ | Lua 5.4+ |
|---|---|---|---|---|
__gc | No | Yes | Yes | Yes |
__len | No | Yes | Yes | Yes |
__eq | No | Yes | Yes | Yes |
| Bitwise ops | No | No | Yes | Yes |
| Floor division | No | No | Yes | Yes |
__close | No | No | No | Yes |
Lua 5.2 added debug.setmetatable to change metatables on any type. Earlier versions only allowed tables and full userdata.
Quick reference
-- Get metatable
getmetatable(obj)
-- Set metatable (only tables and userdata)
setmetatable(t, mt)
-- Common metamethods
__add, __sub, __mul, __div -- arithmetic
__eq, __lt, __le -- comparison
__index, __newindex -- access
__call -- callable objects
__tostring -- tostring() output
__len -- length operator
__gc -- garbage collection (5.2+)
__close -- scope cleanup (5.4+)
Metatables are how you build object-oriented patterns in Lua. Userdata lets C code participate in that system. Together, they let you bridge Lua with native libraries while giving your objects familiar syntax.
Next steps
Now that you understand how userdata and metatables work together, you are ready to put them into practice. Try writing a small C module that exposes a custom data structure, like a ring buffer or a simple hash map, as a userdata with arithmetic and indexing metamethods. The Lua C API reference lists every lua_* function you will need. If you are building a game or GUI application, look at how LÖVE2D uses userdata to wrap C++ graphics objects behind clean Lua interfaces.
See also
- Embedding Lua in a C application : the full workflow for creating a Lua state, running scripts, and registering C functions
- Metatables and metamethods : a deeper exploration of every available metamethod and the patterns they enable
- Lua garbage collection in depth : how the incremental collector works and how to write GC-friendly code