Apéndice C: Lua en el Mundo Real
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?
- Nginx con módulo Lua integrado
- LuaJIT para máxima performance
- Nonblocking I/O completo
- Usado por: Cloudflare, Shopify, GitHub, Alibaba
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
- Atómico: El script completo se ejecuta sin interrupciones
- Menos round-trips: Una sola llamada en lugar de múltiples comandos
- 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
- Editor visual
- Cross-platform (iOS, Android, Web, Desktop)
- Lua como lenguaje de scripting
- 2D y 3D
- Live reload
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:
- Web: OpenResty, Kong, Lapis
- Videojuegos: Love2D, Defold, Roblox, WoW addons
- Editores: Neovim, Hammerspoon
- Databases: Redis, Tarantool
- Aplicaciones: Lightroom, Wireshark, VLC
- Embebido: IoT devices, routers
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:
- lua.org - Sitio oficial
- luarocks.org - Repositorio de paquetes
- lua-users.org - Wiki comunitaria
- /r/lua - Comunidad en Reddit