luaguides

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.

FeatureFull UserdataLight Userdata
Memory managed by LuaYesNo
Individual metatableYesNo
Custom operationsYesNo

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

FeatureLua 5.1Lua 5.2+Lua 5.3+Lua 5.4+
__gcNoYesYesYes
__lenNoYesYesYes
__eqNoYesYesYes
Bitwise opsNoNoYesYes
Floor divisionNoNoYesYes
__closeNoNoNoYes

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.