wandres.dev
EL LENGUAJE · minimalismo como doctrina

Dónde vive Lua

Motores de juego, nginx y OpenResty, Redis, Neovim y hardware embebido: un recorrido por los anfitriones reales de Lua, qué tienen todos en común y por qué ese nicho no lo ocupó ningún otro lenguaje.

⏱ 14 min

Lua es probablemente el lenguaje más usado que nadie declara usar. No aparece en los rankings de contratación ni en las encuestas de lenguaje principal, y sin embargo está dentro del enrutador de tu casa, del servidor que sirve una parte considerable del tráfico web, de la base de datos que cachea tus sesiones, de tu editor y de casi cualquier juego con modding. Ese patrón de invisibilidad no es casual: describe con precisión el nicho que ocupa, y ese nicho tiene una definición técnica exacta.

🎯 Al terminar esta lección sabrás
  • Situar los anfitriones reales de Lua en videojuegos, servidores, bases de datos, editores y hardware.
  • Identificar el patrón arquitectónico común a todos ellos y enunciarlo con precisión.
  • Explicar por qué las propiedades del intérprete habilitan cada uno de esos usos.
  • Argumentar por qué otros lenguajes de scripting no ocuparon ese espacio y cuál es el precio del modelo.

Cuatro nichos y un mismo patrón

🌙

Motores de juego

Grim Fandango en 1998 abrió el camino. Hoy hay Lua en World of Warcraft para las extensiones de interfaz, en Garry’s Mod, en Defold, en LÖVE, en las reglas de Civilization, y Roblox mantiene Luau, un derivado propio de Lua 5.1 con tipado gradual. El motor está en C o C++ y es el dueño del bucle de fotogramas; Lua describe comportamiento, misiones, interfaz y reglas.

🌙

nginx y OpenResty

El módulo de Lua para nginx incrusta LuaJIT dentro del proceso de trabajo y asigna una corrutina por petición. Los sockets cooperativos permiten escribir código de aspecto secuencial que en realidad cede el control al bucle de eventos. Kong, la pasarela de API, es esencialmente OpenResty con opinión.

🌙

Redis y Valkey

Scripting con EVAL desde 2012 y funciones registradas en el servidor desde Redis 7. El script se ejecuta de forma atómica dentro del servidor, lo que convierte varias operaciones en una sola transacción sin pagar viajes de red. El intérprete embebido es la rama 5.1.

🌙

Neovim y el escritorio

Neovim usa LuaJIT como lenguaje de configuración y de extensión desde la versión 0.5. Fuera del editor, el mismo patrón se repite en los dissectors de Wireshark, en el motor de scripts de Nmap, en las reglas de HAProxy, en las plantillas de Wikipedia mediante Scribunto y en la interfaz de Adobe Lightroom.

Falta el nicho menos visible y quizá el más ilustrativo: el hardware embebido. Firmwares como NodeMCU ponen un intérprete de Lua encima de microcontroladores de la familia ESP con memoria medida en decenas de kilobytes. Ahí no hay sistema operativo, no hay asignador de memoria generoso y no hay posibilidad de recompilar el firmware para cambiar una regla. Lua entra porque es lo único de su categoría que cabe.

El mismo argumento explica su presencia en la infraestructura de red doméstica y profesional: enrutadores con firmware alternativo, sondas de monitorización, sistemas de detección de intrusiones que permiten escribir reglas sin recompilar el motor. En todos esos sitios el criterio de selección no fue la elegancia del lenguaje, sino que el binario cupiera y que un fallo en una regla de usuario no tumbara el dispositivo.

Qué tienen todos en común

Ponlos en fila y el patrón se ve solo. En los cinco casos hay:

  1. Una aplicación anfitriona escrita en C o C++ que ya existe, que es la dueña del bucle principal y de la memoria, y que nadie va a reescribir.
  2. Un punto de extensión que debe abrirse a terceros sin recompilar el anfitrión y sin que el tercero tenga que entender el interior del sistema.
  3. Un presupuesto duro, que según el caso es de latencia, de memoria o de ambas. El script de Redis bloquea el servidor entero mientras corre; el de nginx compite con miles de peticiones concurrentes; el del microcontrolador tiene kilobytes.
  4. Código de confianza variable. Una extensión de World of Warcraft, un script subido por un usuario a Roblox o una plantilla de Wikipedia son código ajeno ejecutándose dentro de tu proceso.
  5. Volumen pequeño de código de guion. La lógica pesada está en el anfitrión; Lua es pegamento, configuración y reglas, no el grueso del sistema.

Hay una sexta condición implícita que conviene nombrar porque es la que descarta a los candidatos más obvios: el anfitrión no puede permitirse una dependencia grande. Un motor de juego que se distribuye en consolas, un firmware que cabe en una memoria flash de un megabyte y un servidor que arranca miles de procesos de trabajo tienen presupuestos de tamaño que no admiten arrastrar un runtime completo. Esa condición no aparece en ninguna comparativa de lenguajes y decide todas las adopciones de esta lista.

Cada una de esas cinco condiciones se corresponde con una propiedad concreta del intérprete, y esa correspondencia es el motivo del monopolio. El punto uno lo cubre la API en C, porque Lua es una biblioteca y no un programa. El punto dos lo cubre que la unidad de carga sea una cadena de texto compilable en tiempo de ejecución. El punto tres lo cubre el tamaño y el hecho de que el anfitrión pueda inyectar su propio asignador de memoria. El punto cuatro lo cubre que el entorno global sea una tabla ordinaria, así que restringirlo es construir otra tabla. Y el punto cinco explica por qué la ausencia de tipos estáticos y de biblioteca estándar amplia no duele: en trescientas líneas de reglas, nadie echa de menos un sistema de módulos.

-- el patron del anfitrion, visto desde el lado de Lua
-- 1. el anfitrion expone su mundo como una tabla
local motor = require("motor")

-- 2. el guion registra comportamiento y devuelve el control
motor.on("entidad_dañada", function(entidad, daño)
  if entidad.vida - daño <= 0 then
    motor.reproducir_sonido("muerte")
    return motor.destruir(entidad)
  end
  entidad.vida = entidad.vida - daño
end)

Ese esquema, con nombres distintos, es literalmente el mismo en un motor de juego, en un manejador de peticiones de nginx y en un autocomando de Neovim. Lua nunca es el programa: siempre es la superficie configurable de un programa.

El lado del anfitrión es igual de uniforme, y verlo aclara por qué las cuatro propiedades de arriba son inseparables. Restringir lo que el código ajeno puede tocar no requiere una infraestructura de permisos: requiere construir una tabla.

-- un sandbox completo en seis lineas, porque el entorno es solo una tabla
local permitido = {
  print = print, pairs = pairs, ipairs = ipairs,
  string = string, math = math, table = table,
}
local f = assert(load(codigo_del_usuario, "=extension", "t", permitido))
local ok, err = pcall(f)

Añade un gancho de depuración que aborte pasadas cierto número de instrucciones y un asignador de memoria con presupuesto, y tienes aislamiento de código no confiable dentro del proceso. Ningún lenguaje de scripting grande ofrece eso sin recurrir a un proceso separado o a un contenedor.

flowchart TD
H[Aplicacion anfitriona en C o C plus plus] --> L[Biblioteca Lua enlazada dentro del proceso]
H --> M[El anfitrion es dueno del bucle y de la memoria]
L --> S1[Un estado por peticion, entidad o sesion]
L --> S2[Entorno global como tabla: sandbox barato]
L --> S3[Asignador inyectado: presupuesto de memoria]
L --> S4[Gancho de depuracion: presupuesto de instrucciones]
S1 --> R[Codigo de terceros ejecutado con seguridad y sin recompilar]
S2 --> R
S3 --> R
S4 --> R
style H fill:#f9e2af,color:#11111b
style L fill:#89b4fa,color:#11111b
style R fill:#a6e3a1,color:#11111b

Por qué ese hueco no lo llenó otro

La pregunta interesante no es por qué se eligió Lua, sino por qué las alternativas obvias fracasaron en ese nicho concreto pese a ser lenguajes mucho más populares.

El caso de Python es el más instructivo. Su intérprete asume que es el dueño del proceso: durante décadas el estado del intérprete fue esencialmente global, arrancar cuesta un orden de magnitud más, la biblioteca estándar da por supuesto un sistema operativo completo con sistema de archivos y red, y el binario más la biblioteca ocupan decenas de megabytes. Nada de eso es un defecto: es coherente con ser el lenguaje principal de una aplicación. Simplemente lo descalifica para ser la superficie de extensión de otra. Crear diez mil intérpretes de Python independientes en un proceso servidor no es una operación razonable; crear diez mil estados de Lua sí lo es.

JavaScript llegó a ese terreno mucho más tarde y con motores que son proezas de ingeniería del tamaño de un sistema operativo pequeño. V8 da un rendimiento que Lua no alcanza, a cambio de decenas de megabytes de binario y de una complejidad de integración que solo compensa si el anfitrión es grande. Los motores compactos de JavaScript han recuperado parte del terreno en dispositivos, pero llegaron a un nicho que ya tenía dueño desde hacía quince años.

Incrustar el propio lenguaje del anfitrión, es decir, exponer C o C++ mediante recarga de bibliotecas dinámicas, resuelve el rendimiento y destruye todo lo demás: no hay aislamiento, un error de puntero en la extensión tumba el proceso, y el usuario que quería cambiar una regla ahora necesita un compilador y una cadena de herramientas.

Y está el competidor histórico que casi todo el mundo ha olvidado: Tcl. En 1993, cuando aparece Lua, Tcl era el lenguaje embebible por defecto y tenía todas las ventajas de la ventaja inicial. Perdió por su modelo de datos, en el que todo es una cadena de texto, lo que obliga a convertir sin parar y hace caro cualquier trabajo estructurado, y por una sintaxis que resultaba ajena a quien venía de C. Que un recién llegado sin comunidad ni marketing desplazara al ocupante es la mejor evidencia de que en este nicho las propiedades técnicas pesan más que la popularidad, porque quien decide es un arquitecto midiendo restricciones, no una comunidad votando.

El precio del modelo

Conviene no idealizar. Vivir en ese nicho tiene costes que aparecen invariablemente en los proyectos grandes.

El primero, y el más caro, es la fragmentación de dialectos. Como cada anfitrión congela la versión que le convenía, hoy conviven Lua 5.1 en Redis y en las extensiones de juegos, LuaJIT con semántica de 5.1 en OpenResty y en Neovim, Luau en Roblox y la rama 5.4 y 5.5 en el mundo de PUC-Rio. Una biblioteca no es portable entre ellos sin trabajo.

Un mismo fragmento aparentemente trivial ilustra el problema completo:

-- correcto en 5.3 en adelante, error de sintaxis en LuaJIT y en 5.1
local mascara = valor & 0xFF
-- correcto en LuaJIT y en 5.1, inexistente en 5.4
local mascara = require("bit").band(valor, 0xFF)

No hay ninguna forma de escribir ese archivo para que se analice en ambos mundos, porque la diferencia está en la gramática y no en las bibliotecas. Esa es la textura real de la fragmentación.

El segundo es que la API que ves no es Lua, es la del anfitrión. Saber Lua no te capacita para escribir un plugin de Neovim ni una regla de HAProxy: el lenguaje es la parte pequeña, y el modelo de objetos, los eventos y las reglas de reentrada del anfitrión son la parte grande y la que no está documentada en el manual de referencia.

El tercero es la falta de un lugar donde ejecutar código pesado. Todo el patrón se apoya en que el guion es corto. Un script de Redis que tarda cien milisegundos bloquea el servidor entero. Una función en un manejador de nginx que hace trabajo de CPU consume el bucle de eventos. La disciplina de ceder el control pronto no la impone el lenguaje, la impone el anfitrión, y equivocarse ahí produce incidentes difíciles de diagnosticar.

El cuarto es de herramientas. Depurar código que corre dentro de otro proceso, en un estado que tú no creaste y con un bucle principal que no controlas, es sustancialmente más difícil que depurar un programa normal. Los mensajes de error viajan por la API en C hasta el anfitrión, que decide si los muestra, los registra o los ignora, y una traza de pila que cruza varias veces la frontera entre Lua y C se lee con esfuerzo. La herramienta que salva ese escenario es el gancho de depuración de la biblioteca debug, que permite instrumentar la ejecución desde dentro, y conviene aprender a usarla antes de necesitarla.

Y el quinto, el más humano: la documentación que buscas casi nunca está en el manual de Lua. Cuando algo no funciona en un plugin de Neovim o en una regla de HAProxy, la probabilidad de que la causa sea el lenguaje es baja y la de que sea el contrato del anfitrión es alta. Aprender a distinguir esas dos capas rápido es lo que separa a quien depura de quien prueba combinaciones.

Los lenguajes no ganan mercados, ganan posiciones

La lección estratégica de Lua es que llevas treinta años oyendo mal la pregunta. No compitió por ser el lenguaje en el que se escriben aplicaciones, una competición con decenas de participantes y un ganador cambiante cada década. Ocupó una posición estructural distinta: la de ser la superficie configurable de aplicaciones escritas en otra cosa. Y en esa posición las propiedades que deciden no son las que se discuten en internet. No importa la expresividad, ni el sistema de tipos, ni la calidad de las bibliotecas. Importa el tamaño del binario, importa que no haya estado global, importa que el asignador se pueda sustituir, importa que el entorno sea una tabla que se pueda vaciar. Son propiedades aburridas, invisibles en un tutorial y decisivas en una reunión de arquitectura. El resultado es que Lua tiene una cuota de mercado ridícula en la métrica que todo el mundo mide y un dominio prácticamente total en la que nadie mide. Aplícalo a tus propias decisiones técnicas: antes de elegir una herramienta por su popularidad, escribe las tres restricciones duras de tu sistema y comprueba si la herramienta popular las cumple. Casi siempre la posición que ocupas define la elección mucho mejor que la comparativa general, y quien elige por comparativa general acaba peleando contra una herramienta diseñada para otro sitio.

⚔️ Cartografía del anfitrión
  1. Toma un programa que uses a diario y averigua si incrusta Lua. Localiza dónde viven sus scripts y qué tabla global usa el anfitrión para exponer su mundo.
  2. Escribe un script de Redis con EVAL que lea y escriba varias claves de forma atómica, y razona qué habría hecho falta sin scripting.
  3. Instala OpenResty y sirve una respuesta desde un manejador en Lua. Después mete un bucle de cómputo puro dentro y observa qué le pasa a la concurrencia.
  4. Compara las funciones globales disponibles dentro de un script de Redis con las de un intérprete normal y explica cada ausencia en términos de determinismo y seguridad.
  5. Elige dos de los anfitriones de esta lección y escribe, para cada uno, cuál de las cinco condiciones comunes es la más restrictiva y cómo se manifiesta en el estilo de código que se escribe allí.