← Volver al listado de tecnologías

Apéndice C: Lua en el Mundo Real

Por: Artiko
luaopenrestyredisneovimlove2ddefoldproduction

Apéndice C: Lua en el Mundo Real

“La prueba del pudín está en comerlo.” — Proverbio inglés

Lua no es solo un lenguaje de juguete para aprender programación. Es usado en producción por empresas como Cloudflare, Shopify, GitHub, Adobe, y muchas más. Veamos dónde y cómo.

OpenResty: Lua + Nginx

OpenResty es un servidor web basado en Nginx que embebe LuaJIT. Es la aplicación más popular de Lua en producción.

¿Qué es OpenResty?

Instalación

# macOS
brew install openresty

# Ubuntu
sudo apt-get install software-properties-common
sudo add-apt-repository -y "deb http://openresty.org/package/ubuntu $(lsb_release -sc) main"
sudo apt-get update
sudo apt-get install openresty

Ejemplo: Hello World

# nginx.conf
worker_processes 1;
error_log logs/error.log;

events {
    worker_connections 1024;
}

http {
    server {
        listen 8080;

        location / {
            content_by_lua_block {
                ngx.say("Hello, OpenResty!")
            }
        }
    }
}

Ejecutar:

openresty -p `pwd` -c nginx.conf
curl http://localhost:8080
# Hello, OpenResty!

Ejemplo: API REST

http {
    server {
        listen 8080;

        # GET /users/:id
        location ~ ^/users/(\d+)$ {
            content_by_lua_block {
                local id = ngx.var[1]

                -- Simular DB lookup
                local users = {
                    ["1"] = {name = "Alice", age = 30},
                    ["2"] = {name = "Bob", age = 25}
                }

                local user = users[id]
                if user then
                    local cjson = require("cjson")
                    ngx.header["Content-Type"] = "application/json"
                    ngx.say(cjson.encode(user))
                else
                    ngx.status = 404
                    ngx.say("User not found")
                end
            }
        }

        # POST /users
        location = /users {
            content_by_lua_block {
                ngx.req.read_body()
                local body = ngx.req.get_body_data()

                local cjson = require("cjson")
                local user = cjson.decode(body)

                -- Aquí guardarías en DB

                ngx.status = 201
                ngx.header["Content-Type"] = "application/json"
                ngx.say(cjson.encode({
                    status = "created",
                    user = user
                }))
            }
        }
    }
}

Fases de Request

OpenResty expone diferentes fases del request de Nginx:

-- init_by_lua: Al inicio del proceso
init_by_lua_block {
    require("my_module").init()
}

-- init_worker_by_lua: Por cada worker
init_worker_by_lua_block {
    local timer = require("ngx.timer")
    timer.every(60, function()
        -- Cleanup cada minuto
    end)
}

-- ssl_certificate_by_lua: Durante SSL handshake
ssl_certificate_by_lua_block {
    -- Cargar certificado dinámicamente
}

-- rewrite_by_lua: Antes de encontrar location
rewrite_by_lua_block {
    if ngx.var.uri == "/old" then
        ngx.exec("/new")
    end
}

-- access_by_lua: Control de acceso
access_by_lua_block {
    local token = ngx.var.http_authorization
    if not verify_token(token) then
        ngx.exit(401)
    end
}

-- content_by_lua: Generar respuesta
content_by_lua_block {
    ngx.say("Hello!")
}

-- header_filter_by_lua: Modificar headers de respuesta
header_filter_by_lua_block {
    ngx.header["X-Powered-By"] = "OpenResty"
}

-- body_filter_by_lua: Modificar body de respuesta
body_filter_by_lua_block {
    local chunk = ngx.arg[1]
    ngx.arg[1] = chunk:gsub("foo", "bar")
}

-- log_by_lua: Logging
log_by_lua_block {
    local latency = ngx.now() - ngx.req.start_time()
    ngx.log(ngx.INFO, "Request took " .. latency .. "s")
}

Caso Real: Rate Limiting

-- lib/rate_limiter.lua
local redis = require("resty.redis")

local _M = {}

function _M.is_allowed(key, limit, window)
    local red = redis:new()
    red:connect("127.0.0.1", 6379)

    -- Usar ventana deslizante
    local now = ngx.now()
    local window_start = now - window

    -- Remover requests antiguos
    red:zremrangebyscore(key, 0, window_start)

    -- Contar requests actuales
    local current = red:zcard(key)

    if current < limit then
        red:zadd(key, now, now)
        red:expire(key, window)
        return true
    end

    return false
end

return _M

Uso en nginx:

location /api {
    access_by_lua_block {
        local limiter = require("rate_limiter")
        local ip = ngx.var.remote_addr
        local key = "rate:" .. ip

        if not limiter.is_allowed(key, 100, 60) then
            ngx.status = 429
            ngx.say("Too Many Requests")
            ngx.exit(429)
        end
    }

    proxy_pass http://backend;
}

Redis Scripting

Redis permite ejecutar scripts Lua atómicamente en el servidor.

Ventajas

  1. Atómico: El script completo se ejecuta sin interrupciones
  2. Menos round-trips: Una sola llamada en lugar de múltiples comandos
  3. Lógica compleja: Condicionales, loops, etc.

Ejemplo Básico

-- Script Lua en Redis
local current = redis.call('GET', KEYS[1])
if current == false then
    current = 0
end

current = current + ARGV[1]
redis.call('SET', KEYS[1], current)
return current

Ejecutar desde cliente:

redis-cli EVAL "$(cat increment.lua)" 1 counter 5

Desde código Lua (con lua-resty-redis):

local redis = require("resty.redis")
local red = redis:new()
red:connect("127.0.0.1", 6379)

local script = [[
    local current = redis.call('GET', KEYS[1])
    if current == false then
        current = 0
    end
    current = tonumber(current) + tonumber(ARGV[1])
    redis.call('SET', KEYS[1], current)
    return current
]]

local result = red:eval(script, 1, "counter", 5)
print(result)

Caso Real: Distributed Lock

-- acquire_lock.lua
local key = KEYS[1]
local token = ARGV[1]
local ttl = ARGV[2]

-- SET key token NX PX ttl
local result = redis.call('SET', key, token, 'NX', 'PX', ttl)
return result
-- release_lock.lua
local key = KEYS[1]
local token = ARGV[1]

-- Solo liberar si el token coincide
if redis.call('GET', key) == token then
    return redis.call('DEL', key)
end
return 0

Uso:

local redis = require("resty.redis")
local red = redis:new()
red:connect("127.0.0.1", 6379)

-- Adquirir lock
local token = ngx.now() .. "-" .. ngx.worker.pid()
local acquired = red:evalsha(lock_sha, 1, "mylock", token, 5000)

if acquired then
    -- Hacer trabajo crítico

    -- Liberar lock
    red:evalsha(unlock_sha, 1, "mylock", token)
end

Neovim: Editor Extensible con Lua

Neovim reemplazó VimScript con Lua como lenguaje de configuración y plugins.

Configuración Básica

-- ~/.config/nvim/init.lua
vim.opt.number = true
vim.opt.relativenumber = true
vim.opt.tabstop = 4
vim.opt.shiftwidth = 4
vim.opt.expandtab = true

-- Keymaps
vim.keymap.set('n', '<leader>w', ':w<CR>')
vim.keymap.set('n', '<leader>q', ':q<CR>')

-- Autocmds
vim.api.nvim_create_autocmd("BufWritePre", {
    pattern = "*.lua",
    callback = function()
        vim.lsp.buf.format()
    end
})

Plugin Simple

-- ~/.config/nvim/lua/my-plugin.lua
local M = {}

function M.hello()
    print("Hello from Lua!")
end

function M.insert_date()
    local date = os.date("%Y-%m-%d")
    vim.api.nvim_put({date}, 'c', true, true)
end

-- Comando
vim.api.nvim_create_user_command('InsertDate', M.insert_date, {})

return M

Usar en init.lua:

local my_plugin = require('my-plugin')
vim.keymap.set('n', '<leader>d', my_plugin.insert_date)

Plugin Manager: lazy.nvim

-- ~/.config/nvim/lua/plugins.lua
return {
    {
        'nvim-telescope/telescope.nvim',
        dependencies = { 'nvim-lua/plenary.nvim' },
        config = function()
            require('telescope').setup{}
        end
    },
    {
        'nvim-treesitter/nvim-treesitter',
        build = ':TSUpdate'
    }
}

Love2D: Framework para Videojuegos

Love2D es un framework 2D simple y poderoso para crear juegos.

Instalación

# macOS
brew install love

# Ubuntu
sudo add-apt-repository ppa:bartbes/love-stable
sudo apt-get update
sudo apt-get install love

Hello World

-- main.lua
function love.load()
    love.window.setTitle("My Game")
    player = {x = 400, y = 300, speed = 200}
end

function love.update(dt)
    if love.keyboard.isDown("right") then
        player.x = player.x + player.speed * dt
    end
    if love.keyboard.isDown("left") then
        player.x = player.x - player.speed * dt
    end
    if love.keyboard.isDown("down") then
        player.y = player.y + player.speed * dt
    end
    if love.keyboard.isDown("up") then
        player.y = player.y - player.speed * dt
    end
end

function love.draw()
    love.graphics.circle("fill", player.x, player.y, 20)
end

Ejecutar:

love .

Juego Completo: Pong

-- main.lua
function love.load()
    love.window.setMode(800, 600)

    ball = {x = 400, y = 300, dx = 200, dy = 200, radius = 10}

    player1 = {x = 30, y = 250, width = 10, height = 100, speed = 300}
    player2 = {x = 760, y = 250, width = 10, height = 100, speed = 300}

    score = {p1 = 0, p2 = 0}
end

function love.update(dt)
    -- Mover jugador 1
    if love.keyboard.isDown("w") then
        player1.y = math.max(0, player1.y - player1.speed * dt)
    end
    if love.keyboard.isDown("s") then
        player1.y = math.min(500, player1.y + player1.speed * dt)
    end

    -- Mover jugador 2
    if love.keyboard.isDown("up") then
        player2.y = math.max(0, player2.y - player2.speed * dt)
    end
    if love.keyboard.isDown("down") then
        player2.y = math.min(500, player2.y + player2.speed * dt)
    end

    -- Mover pelota
    ball.x = ball.x + ball.dx * dt
    ball.y = ball.y + ball.dy * dt

    -- Rebote en paredes superior/inferior
    if ball.y < ball.radius or ball.y > 600 - ball.radius then
        ball.dy = -ball.dy
    end

    -- Colisión con palas
    if ball.x < player1.x + player1.width + ball.radius and
       ball.y > player1.y and ball.y < player1.y + player1.height then
        ball.dx = math.abs(ball.dx)
    end

    if ball.x > player2.x - ball.radius and
       ball.y > player2.y and ball.y < player2.y + player2.height then
        ball.dx = -math.abs(ball.dx)
    end

    -- Puntos
    if ball.x < 0 then
        score.p2 = score.p2 + 1
        ball.x, ball.y = 400, 300
    end
    if ball.x > 800 then
        score.p1 = score.p1 + 1
        ball.x, ball.y = 400, 300
    end
end

function love.draw()
    -- Palas
    love.graphics.rectangle("fill", player1.x, player1.y,
                           player1.width, player1.height)
    love.graphics.rectangle("fill", player2.x, player2.y,
                           player2.width, player2.height)

    -- Pelota
    love.graphics.circle("fill", ball.x, ball.y, ball.radius)

    -- Puntuación
    love.graphics.print(score.p1, 350, 50)
    love.graphics.print(score.p2, 430, 50)
end

Defold: Motor de Juegos Profesional

Defold es un motor completo usado por King (Candy Crush) y otros.

Características

Ejemplo de Script

-- player.script
function init(self)
    self.velocity = vmath.vector3()
    self.speed = 200
end

function update(self, dt)
    -- Input
    if input.is_pressed(hash("left")) then
        self.velocity.x = -self.speed
    elseif input.is_pressed(hash("right")) then
        self.velocity.x = self.speed
    else
        self.velocity.x = 0
    end

    -- Movimiento
    local pos = go.get_position()
    pos = pos + self.velocity * dt
    go.set_position(pos)
end

function on_message(self, message_id, message, sender)
    if message_id == hash("collision") then
        -- Manejar colisión
    end
end

Otros Casos de Uso

Adobe Lightroom

Lightroom usa Lua para plugins y automatización.

-- lightroom_plugin.lua
local LrTasks = import 'LrTasks'
local LrDialogs = import 'LrDialogs'

LrTasks.startAsyncTask(function()
    local photos = catalog:getTargetPhotos()

    for _, photo in ipairs(photos) do
        photo:requestJpegThumbnail(256, function(jpg)
            -- Procesar thumbnail
        end)
    end

    LrDialogs.message("Processed " .. #photos .. " photos")
end)

Wireshark

Wireshark usa Lua para disectores de protocolos personalizados.

-- custom_protocol.lua
local myproto = Proto("myproto", "My Custom Protocol")

local f_type = ProtoField.uint8("myproto.type", "Type")
local f_length = ProtoField.uint16("myproto.length", "Length")

myproto.fields = {f_type, f_length}

function myproto.dissector(buffer, pinfo, tree)
    pinfo.cols.protocol = "MYPROTO"

    local subtree = tree:add(myproto, buffer())
    subtree:add(f_type, buffer(0,1))
    subtree:add(f_length, buffer(1,2))
end

DissectorTable.get("tcp.port"):add(1234, myproto)

Kong API Gateway

Kong usa OpenResty (Lua) para su core y plugins.

-- custom-plugin.lua
local kong = kong

local CustomPlugin = {
    PRIORITY = 1000,
    VERSION = "1.0.0",
}

function CustomPlugin:access(conf)
    local api_key = kong.request.get_header("X-API-Key")

    if not api_key then
        return kong.response.exit(401, {
            message = "Missing API Key"
        })
    end

    -- Validar key en Redis/DB
    local valid = validate_key(api_key)
    if not valid then
        return kong.response.exit(403, {
            message = "Invalid API Key"
        })
    end
end

return CustomPlugin

Conclusión

Lua está en todas partes:

Su simplicidad, velocidad, y facilidad de embedding lo hacen perfecto para extender aplicaciones existentes.


Fin del Tutorial “Lua Fluido”

¡Felicitaciones por llegar hasta aquí! Ahora tienes el conocimiento para escribir código Lua idiomático y aprovecharlo en proyectos reales.

Recursos adicionales: