Userdata, Light Userdata, and Metatables in Lua
Full Userdata — C Objects in Lua
Full userdata is a block of raw memory that Lua manages. You create it from C, not Lua.
#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
}
Lua treats full userdata as opaque objects. Two full userdata are equal only if they’re the same memory block — content doesn’t matter.
-- 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)
The garbage collector handles full userdata. When no references remain, Lua calls the __gc metamethod before freeing memory.
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);
}
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)
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 __.
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.
-- 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)
You can also use a table for faster lookups instead of a function.
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
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
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
Now you can call count() like a 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)
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;
}
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)
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.