wandres.dev
METATABLAS II · el catálogo completo

Protección y metatablas: __metatable, __name y el patrón del tipo opaco

Cómo ocultar la metatabla para que nadie la lea ni la reemplace, cómo darle a tu tipo un nombre que aparezca en los mensajes de error y en la representación por defecto, y cómo combinar ambas cosas con estado privado para construir un valor cuya representación interna es inaccesible desde Lua.

⏱ 18 min

Todo lo visto en este nivel presupone algo que nunca hemos cuestionado: que la metatabla es pública. Cualquiera puede pedírtela, leerla, copiar tus metamétodos, sustituirlos por otros y cambiar el comportamiento de tus objetos desde fuera. Para un módulo interno eso es indiferente; para una biblioteca que expone un tipo con invariantes, o para un anfitrión que ejecuta código de terceros, es una puerta abierta de par en par. Los dos metamétodos que cierran esta lección no calculan nada: uno esconde la metatabla y otro mejora los mensajes de error. Juntos, y con una pizca de estado privado, producen el patrón del tipo opaco, que es la forma en que Lua hace encapsulación de verdad.

🎯 Al terminar esta lección sabrás
  • Blindar una metatabla con __metatable y conocer el error que produce al intentar cambiarla.
  • Dar nombre a un tipo con __name y ver dónde aparece ese nombre.
  • Construir un tipo opaco con estado privado, métodos y escritura prohibida.
  • Saber exactamente hasta dónde protege el patrón y qué lo atraviesa.

Ocultar la metatabla

El metamétodo __metatable no responde a un operador: responde a las dos funciones que manipulan metatablas. Si está presente, getmetatable devuelve su valor en lugar de la metatabla real, y setmetatable se niega a actuar lanzando el error cannot change a protected metatable.

local mt = {
  __index = { saludar = function() return "hola" end },
  __metatable = "protegida",       -- cualquier valor sirve; una cadena es lo habitual
}

local obj = setmetatable({}, mt)

print(getmetatable(obj))           -- protegida   (no la tabla real)
print(obj.saludar())               -- hola        (los metametodos siguen activos)

local ok, err = pcall(setmetatable, obj, {})
print(ok, err)                     -- false  cannot change a protected metatable

Nada de esto afecta al funcionamiento interno: el intérprete sigue consultando la metatabla real para todos los eventos. Lo único que cambia es el acceso desde el código Lua. Conviene poner ahí un valor informativo —una cadena con el nombre del tipo, o una tabla de solo lectura con metadatos— porque getmetatable sigue siendo la vía por la que otros comprobarán de qué tipo es tu objeto.

⚠️
Protegerla también te la quita a ti

Una vez blindada, tu propio código tampoco podrá recuperar la metatabla real por la vía normal. Guarda la referencia en una variable local del módulo antes de exponer nada, porque después ya no hay forma de volver a ella sin recurrir a la biblioteca de depuración.

Un nombre para los mensajes

El campo __name es simplemente una cadena, y su efecto es cosmético hasta el día en que depuras un fallo ajeno. Cuando existe y el objeto no define __tostring, la representación por defecto deja de ser el genérico seguido de una dirección y pasa a llevar tu nombre delante. Y, sobre todo, aparece en los mensajes de error que genera la biblioteca de comprobación de tipos de la API C, que es la que usan todas las bibliotecas escritas en C para validar sus argumentos.

local mt = { __name = "Conexion" }
local c = setmetatable({}, mt)
print(c)          -- Conexion: 0x5583a1c2e380   en vez de table: 0x...

La diferencia entre leer bad argument number 1 to close, userdata expected, got table y leer bad argument number 1 to close, Conexion expected, got Fichero es la diferencia entre media hora de rastreo y ninguna. Cuesta una línea. Si además defines __tostring, ese metamétodo gana y __name queda solo para los errores, así que lo razonable es poner los dos.

El patrón del tipo opaco

Un tipo es opaco cuando quien lo usa recibe un valor que solo puede manipular a través de las funciones que tú expones: no puede leer su representación interna, no puede escribirla y no puede sustituir su comportamiento. Se construye combinando cuatro piezas.

flowchart TB
A[Constructor del modulo] --> B[Crea una tabla vacia como asa publica]
B --> C[Guarda el estado real en una tabla privada con claves debiles]
B --> D[Asigna la metatabla blindada]
D --> E[Index apunta a la tabla de metodos]
D --> F[Newindex rechaza toda escritura]
D --> G[Metatable oculta la metatabla real]
D --> H[Name da nombre a los mensajes de error]
local Pila = {}
local privado = setmetatable({}, { __mode = "k" })   -- claves debiles: no retiene
local metodos = {}

local mt = {
  __index    = metodos,
  __newindex = function() error("Pila es de solo lectura", 2) end,
  __len      = function(self) return #privado[self] end,
  __tostring = function(self) return "Pila con " .. #privado[self] .. " elementos" end,
  __name     = "Pila",
  __metatable = "Pila",
}

function Pila.nueva()
  local asa = setmetatable({}, mt)
  privado[asa] = {}                   -- el estado vive fuera del objeto visible
  return asa
end

function metodos:apilar(v)
  local d = privado[self] or error("no es una Pila", 2)
  d[#d + 1] = v
  return self
end

function metodos:desapilar()
  local d = privado[self] or error("no es una Pila", 2)
  local v = d[#d]
  d[#d] = nil
  return v
end

local p = Pila.nueva()
p:apilar(1):apilar(2)
print(p, #p, p:desapilar())          -- Pila con 2 elementos   2   2
print(next(p))                       -- nil: el asa esta vacia por dentro
print(pcall(function() p.x = 1 end)) -- false  Pila es de solo lectura
💡
La variante sin tabla privada: el asa como closure

Existe una segunda forma de opacidad, más radical, en la que el valor público no es una tabla sino una función que captura el estado en sus variables locales. No hace falta ni tabla privada ni claves débiles, porque el estado vive en el propio cierre y muere con él. A cambio pierdes la sintaxis de método, la longitud y los operadores, así que la elección real es entre un asa vacía con metatabla blindada —opaca y con protocolos— y un cierre puro —opaco y mudo—.

La clave del patrón es que el objeto que el usuario sostiene está vacío. Recorrerlo con el iterador de pares no revela nada, serializarlo produce una tabla vacía, y el estado real vive en una tabla privada del módulo indexada por el propio objeto. El modo de claves débiles evita que esa tabla privada convierta cada pila creada en una fuga de memoria permanente.

Hasta dónde protege

Conviene ser exacto sobre las garantías, porque el patrón se vende a veces como más de lo que es.

Ataque ¿Lo detiene?
Leer los campos internos del objeto Sí, el asa está vacía
Escribir un campo nuevo Sí, __newindex lanza error
Escribir un campo con rawset No, la escritura cruda salta el metamétodo
Obtener la metatabla con getmetatable Sí, devuelve el valor de __metatable
Sustituirla con setmetatable Sí, error de metatabla protegida
Usar debug.getmetatable y debug.setmetatable No, la biblioteca de depuración lo atraviesa todo

Merece la pena entender por qué la biblioteca de depuración lo atraviesa todo, porque no es un descuido. Su razón de ser es permitir que un depurador, un perfilador o un manejador de errores inspeccionen un programa en ejecución sin cooperación de ese programa, y una herramienta así no puede respetar barreras que el código bajo observación haya levantado. De ahí que el manual la describa como una biblioteca que puede romper cualquier suposición del código Lua y recomiende retirarla de cualquier entorno donde se ejecute algo que no controles.

La conclusión es que __metatable protege contra accidentes y contra terceros bienintencionados, no contra código hostil. Cualquiera con acceso a la biblioteca de depuración recupera la metatabla real en una línea, y con rawset escribe donde quiera. Si tu objetivo es ejecutar código no confiable, la protección no la da el metamétodo sino el entorno: retirar la biblioteca de depuración y las funciones crudas del entorno global del código invitado. El metamétodo es la valla del jardín; el aislamiento real es otra construcción.

Encapsular en Lua no es esconder campos, es no tenerlos

Aquí se cierra el nivel con una idea que revisa todo lo anterior. Cuando alguien llega desde un lenguaje con modificadores de acceso, busca en Lua el equivalente de la palabra que marca un campo como privado, no la encuentra y concluye que el lenguaje no soporta encapsulación. La conclusión es exactamente al revés: Lua no ofrece campos privados porque ofrece algo más fuerte, que es la posibilidad de que el campo no exista en el objeto. Un atributo privado en otros lenguajes sigue estando ahí, ocupando su hueco en la estructura, visible para la reflexión, para el depurador, para el serializador y para cualquiera que sepa dónde mirar; lo que hay es una regla del compilador que rechaza cierta sintaxis. El asa vacía del tipo opaco no tiene nada que ocultar porque no contiene nada: el estado vive en el ámbito léxico del módulo, y el ámbito léxico no es una convención sino la estructura misma del programa. Nadie puede leer una variable local de un fichero que no tiene delante. Esa es la razón profunda por la que el patrón funciona, y también la razón por la que __metatable es un complemento y no el núcleo: sin él, el usuario podría sustituir tus metamétodos y hacer que el asa se comportara de otro modo, pero seguiría sin poder llegar al estado. Y de ahí se sigue el principio general que gobierna todo este nivel: en Lua los metamétodos no imponen políticas, definen protocolos. Deciden qué preguntas puede hacer el resto del programa a tu valor y qué respuestas obtiene; la parte que decide qué se protege de verdad no está en la metatabla, está en dónde decidiste guardar los datos.

📝
Lo esencial

__metatable sustituye lo que devuelve getmetatable y hace que setmetatable falle con un error de metatabla protegida, sin alterar en nada el funcionamiento interno de los demás metamétodos. __name es una cadena que aparece en la representación por defecto cuando no hay __tostring y, sobre todo, en los mensajes de error de las bibliotecas escritas en C. El tipo opaco combina un asa vacía, una tabla privada con claves débiles, __index para los métodos, __newindex para prohibir escrituras y los dos metamétodos anteriores. Detiene accidentes y curiosos; no detiene la biblioteca de depuración ni el acceso crudo.

⚔️ Construye y ataca tu propio tipo opaco
  1. Escribe el tipo Pila completo y comprueba que el iterador de pares sobre una instancia no devuelve nada.
  2. Intenta cambiar la metatabla con setmetatable y lee el mensaje exacto. Después consíguelo con debug.setmetatable y saca la conclusión.
  3. Salta la protección de escritura con rawset y explica por qué el metamétodo no interviene.
  4. Quita el modo de claves débiles de la tabla privada, crea cien mil pilas, descarta las referencias y fuerza una recolección. Mide la memoria antes y después.
  5. Añade __name a un tipo sin __tostring y observa cómo cambia lo que imprime print. Luego añade __tostring y explica cuál gana.