La doctrina del tamaño
Por qué el intérprete de Lua cabe en unos cientos de kilobytes, qué se dejó fuera de forma deliberada, qué significa preferir mecanismos a políticas, y qué gana de verdad un lenguaje por ser pequeño y portable.
El tamaño de Lua se cita siempre como un dato curioso, y es un error: no es una consecuencia agradable de su diseño, es su restricción de partida. Un lenguaje que se compromete a caber dentro del ejecutable de otro programa está aceptando un presupuesto fijo, y cada característica que quiera entrar tiene que desalojar a otra. Lo interesante no es el número de kilobytes. Es la disciplina que ese número impone y las decisiones que fuerza.
- Cuantificar el tamaño real de Lua en fuente y en binario, y saber de dónde sale esa cifra.
- Enumerar lo que Lua dejó fuera de forma deliberada y el criterio que se usó para decidirlo.
- Explicar la doctrina de mecanismos en lugar de políticas con ejemplos concretos del lenguaje.
- Justificar qué gana un sistema real por incrustar un lenguaje pequeño, portable y auditable.
Qué cabe en unos cientos de kilobytes
Los números concretos ayudan a fijar la escala. El paquete completo de fuentes de una versión reciente de Lua ocupa menos de medio megabyte comprimido y ronda las treinta mil líneas de C contando el núcleo, las bibliotecas estándar, el compilador de bytecode y el intérprete de línea de comandos. La biblioteca compilada en una máquina de sesenta y cuatro bits queda del orden de doscientos a trescientos kilobytes, y el binario del intérprete es de ese mismo orden. Un estado de Lua recién creado ocupa unos pocos kilobytes de memoria dinámica.
Esas cifras significan tres cosas prácticas. La primera es que Lua cabe en la caché de instrucciones de un procesador moderno con holgura, y eso se nota en el rendimiento de forma difícil de replicar con un intérprete grande. La segunda es que una sola persona puede leer el código fuente entero en unas semanas, lo cual convierte la auditoría en una posibilidad real y no en un eslogan. La tercera es que el coste de incrustarlo es despreciable frente a casi cualquier aplicación anfitriona: añadir Lua a un programa en C no cambia su categoría de despliegue.
La portabilidad es la otra mitad de la doctrina. El núcleo se escribe en C estricto y conforme, sin llamadas al sistema operativo más allá de la biblioteca estándar de C. Las versiones actuales exigen C99, las anteriores se conformaban con C89. La consecuencia es que Lua compila sin parches en cualquier plataforma con un compilador de C decente, incluidas las que no tienen sistema operativo. Cuando algo depende de la plataforma, no se resuelve con condicionales dispersos sino en un único archivo de configuración, luaconf.h, que es el sitio donde se ajustan tipos numéricos, rutas por defecto y modos de compilación.
# el archivo que gobierna todas las decisiones dependientes de plataforma
grep -c '#define' src/luaconf.h
# compilar en modo de 32 bits, con enteros y flotantes mas pequenos
make all MYCFLAGS=-DLUA_32BITS
Lo que se dejó fuera a propósito
La lista de ausencias es más reveladora que la de características. Ninguna de estas cosas falta por descuido.
Un modelo de objetos
No hay clases, ni herencia, ni this implícito. Hay metatablas, con las que se construye la orientación a objetos en veinte líneas. El lenguaje no impone qué modelo de objetos usas.
Expresiones regulares
En su lugar hay un lenguaje de patrones propio, más limitado y sin retroceso exponencial, que se implementa en unos centenares de líneas frente a las decenas de miles de un motor de regex serio.
Hilos preemptivos
No los hay porque C portable no los tenía. Hay corrutinas, que son cooperativas, deterministas y se implementan sin tocar el sistema operativo.
Una biblioteca gigante
No hay red, ni procesos, ni fechas más allá de lo mínimo, ni compresión, ni criptografía. Todo eso es responsabilidad del anfitrión o de LuaRocks.
A eso hay que sumar ausencias más profundas: no hay sistema de tipos estático, no hay excepciones con clases de error ni bloques de captura, no hay sistema de módulos en la gramática, no hay operadores definidos por el usuario más allá de los metamétodos previstos, y no hay recolector de basura configurable por política declarativa. El manejo de errores entero se reduce a error para señalar y pcall para capturar, con el idioma cultural de devolver nil acompañado de un mensaje cuando el fallo es esperable.
-- todo el manejo de errores del lenguaje cabe aqui
local ok, resultado = pcall(funcion_que_puede_fallar, argumento)
if not ok then
-- resultado es el valor lanzado por error, de cualquier tipo
end
-- y el idioma cultural para lo que no es excepcional
local f, err = io.open("config.lua")
if not f then return nil, err end
Compáralo con el aparato de un lenguaje grande: jerarquía de clases de excepción, bloques con varias cláusulas de captura, cláusula final, encadenamiento de causas, excepciones declaradas en la firma. Todo eso resuelve problemas reales y todo eso cuesta código en el intérprete y páginas en el manual. Lua decidió que el noventa por ciento del valor estaba en dos funciones, y se quedó con las dos funciones.
El criterio que ordena todas esas exclusiones es sencillo de enunciar y difícil de aplicar: si se puede construir en Lua sobre un mecanismo más general, no entra en el lenguaje. Las clases se construyen con metatablas. Los generadores se construyen con corrutinas. Los espacios de nombres se construyen con tablas. La carga de módulos se construye con funciones de biblioteca que consultan package.path. Cada vez que el equipo pudo mover algo del núcleo a la biblioteca, o de la biblioteca al código de usuario, lo hizo.
Mecanismos en lugar de políticas
Esta es la formulación que los propios autores usan y merece desmenuzarse, porque explica la sensación inicial de que a Lua le faltan cosas. Un lenguaje con políticas te dice cómo se hacen los objetos. Un lenguaje con mecanismos te da la primitiva con la que puedes hacerlos de tres maneras distintas y te deja elegir.
El ejemplo canónico es la herencia. Lua no la tiene. Lo que tiene es un metamétodo que se consulta cuando una clave no está en la tabla:
local Animal = {}
Animal.__index = Animal
function Animal.new(nombre)
return setmetatable({ nombre = nombre }, Animal)
end
function Animal:hablar()
return self.nombre .. " hace un ruido"
end
-- herencia: la metatabla de la clase hija apunta a la clase madre
local Perro = setmetatable({}, { __index = Animal })
Perro.__index = Perro
function Perro.new(nombre)
return setmetatable(Animal.new(nombre), Perro)
end
function Perro:hablar()
return self.nombre .. " ladra"
end
print(Perro.new("Nube"):hablar())
Ahí no hay ninguna característica de orientación a objetos. Hay una tabla, un metamétodo de indexación y azúcar sintáctico de dos puntos para pasar self. Con el mismo mecanismo se construyen tablas de solo lectura, objetos con propiedades calculadas, proxies que registran todos los accesos, o sistemas de componentes en los que un objeto delega en varios padres. El precio es real y hay que decirlo: no hay una única forma canónica de hacer objetos en Lua, así que cada base de código inventa la suya y leer código ajeno cuesta más.
flowchart TD A[Restriccion: caber dentro de otra aplicacion] --> B[C portable estricto, sin dependencias del sistema] A --> C[Presupuesto fijo de codigo] C --> D[Un unico tipo compuesto: la tabla] C --> E[Mecanismos en vez de politicas] E --> F[Metatablas en vez de clases] E --> G[Corrutinas en vez de hilos] E --> H[Patrones en vez de expresiones regulares] B --> I[Compila en cualquier sitio, incluso sin sistema operativo] D --> J[Interprete de unos cientos de kilobytes] style A fill:#f9e2af,color:#11111b style E fill:#a6e3a1,color:#11111b style J fill:#89b4fa,color:#11111b
Qué gana un lenguaje por ser pequeño
El beneficio no es estético. Es una lista de capacidades que los lenguajes grandes no pueden ofrecer a ningún precio.
Se puede incrustar sin negociar. Añadir Lua a una aplicación en C es enlazar una biblioteca y llamar a media docena de funciones. No hay proceso separado, ni protocolo, ni serialización en la frontera. El anfitrión sigue siendo el dueño del bucle principal y de la memoria.
Se puede multiplicar. Un estado de Lua no tiene estado global oculto ni singleton de proceso: puedes crear miles de estados independientes en el mismo programa, uno por petición, por entidad del juego o por conexión. Esa propiedad, que parece menor, es la razón de que OpenResty pueda dar una corrutina por petición HTTP y de que un motor de juego pueda dar un entorno por NPC.
Se puede restringir. Como todo el entorno global es una tabla, sandboxear consiste en construir una tabla con lo que quieres permitir y ejecutar el código dentro. Añade un contador de instrucciones mediante un gancho de depuración y un asignador de memoria propio, y tienes un aislamiento que en lenguajes grandes requiere un proceso separado o una máquina virtual del sistema.
/* el anfitrion decide incluso de donde sale cada byte */
static void *mi_alloc(void *ud, void *ptr, size_t osize, size_t nsize);
lua_State *L = lua_newstate(mi_alloc, &mi_presupuesto);
Se puede auditar y se puede portar. Que el fuente sea legible entero significa que un fallo de seguridad se localiza en horas y que un equipo puede mantener un parche propio sin miedo. Que sea C conforme significa que aparece en un microcontrolador, en un enrutador doméstico y en una consola de videojuegos sin una capa de compatibilidad de por medio.
Y hay una ganancia menos obvia: la superficie de aprendizaje. El manual de referencia completo de Lua es un documento de menos de cien páginas que un profesional puede leer entero en una tarde y consultar por el resto de su carrera. Eso cambia por completo la economía de introducir el lenguaje en un equipo, porque el coste de formación deja de ser un argumento en la reunión de decisión.
Casi todos los lenguajes que usarás crecen por acumulación, porque nada les impide crecer. Añadir una característica tiene un coste difuso y un beneficio visible, así que el gradiente empuja siempre en la misma dirección y treinta años después tienes cuatro maneras de declarar una función y ninguna forma de quitar ninguna. Lua está sometido a la única fuerza que invierte ese gradiente: un presupuesto de tamaño que sus usuarios verifican, porque quien incrusta un intérprete en un firmware mide el binario. Con ese presupuesto, añadir algo obliga a justificar por qué no puede construirse sobre lo que ya hay, y esa pregunta es exactamente la que produce los mecanismos generales. Las metatablas no existen porque alguien tuviera una teoría elegante de la delegación, existen porque implementar clases habría costado más código que implementar el gancho que permite construirlas. El corolario para tu propio trabajo es incómodo y comprobable: la calidad de una API depende menos del talento de quien la diseña que de si alguien la obliga a pagar por crecer. Si en tu sistema añadir un endpoint, una opción de configuración o un campo no le cuesta nada a nadie, tu sistema va a crecer hasta que nadie lo entienda, y ninguna cantidad de buen criterio individual lo va a evitar.
- Descarga el fuente de Lua, cuenta las líneas de C del núcleo y de las bibliotecas por separado, y localiza el archivo más largo. Explica por qué es ese y no otro.
- Compila el intérprete dos veces, con y sin la opción de treinta y dos bits, y compara el tamaño del binario y el tamaño de un número en memoria.
- Implementa un objeto con propiedades calculadas usando solo
__indexy__newindex, sin ninguna biblioteca. Después haz que la tabla sea de solo lectura sin copiar los datos. - Escribe un sandbox mínimo: una tabla de entorno con acceso a
stringymathpero no aioni aos, y ejecuta dentro un fragmento de código que intente escaparse. - Elige un lenguaje grande que uses a diario y localiza tres características suyas que en Lua se construirían sobre un mecanismo más general. Argumenta en cada caso qué se gana y qué se pierde con cada opción.