La C API de Lua: embeber Lua en aplicaciones C/C++ y extenderlo con código nativo

Lo que diferencia a Lua de la mayoría de lenguajes de scripting es que fue diseñado desde el primer día para vivir dentro de otra aplicación. Python, Ruby o JavaScript tienen su propia ejecutable y su propio ciclo de vida; Lua es una biblioteca. La función main la pone la aplicación anfitriona, y Lua se limita a ejecutar los scripts que esa aplicación le encarga. La C API es el puente entre los dos mundos.

La pila: el mecanismo de comunicación

La C API de Lua usa una pila (stack) para pasar valores entre C y Lua. Cuando Lua llama a una función C, los argumentos están en la pila con índices positivos (1 = primero) o negativos (-1 = último). La función C empuja sus resultados a la pila y devuelve el número de valores que ha empujado.

Este diseño es explícito pero predecible, y permite que la VM gestione la memoria sin necesidad de referencias externas ni garbage collector manual en el lado de C.

Embeber Lua en una aplicación C

#include <stdio.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

int main(void) {
    /* Crear el estado de la VM */
    lua_State *L = luaL_newstate();
    if (!L) { fprintf(stderr, "Error creando lua_Staten"); return 1; }

    /* Cargar la biblioteca estándar (io, math, string, table...) */
    luaL_openlibs(L);

    /* Ejecutar un fichero .lua */
    int resultado = luaL_dofile(L, "script.lua");
    if (resultado != LUA_OK) {
        /* El mensaje de error está en la cima de la pila */
        fprintf(stderr, "Error Lua: %sn", lua_tostring(L, -1));
        lua_pop(L, 1);   /* limpiar el error de la pila */
    }

    /* Evaluar un fragmento de código inline */
    luaL_dostring(L, "print('Hola desde C')");

    /* Cerrar la VM y liberar toda la memoria */
    lua_close(L);
    return 0;
}

Para compilar, hay que enlazar con la biblioteca de Lua:

gcc main.c -o programa -llua5.4 -lm
# O con pkg-config si está disponible:
gcc main.c -o programa $(pkg-config --cflags --libs lua5.4)

Llamar funciones Lua desde C

lua_pcall llama a una función Lua de forma protegida: si la función lanza un error, lua_pcall lo captura y lo deja en la pila en lugar de abortar el proceso:

/* Ejecutar la función Lua: resultado = suma(3, 4) */
lua_getglobal(L, "suma");   /* empujar la función */
lua_pushinteger(L, 3);      /* argumento 1 */
lua_pushinteger(L, 4);      /* argumento 2 */

/* Llamar: 2 argumentos, 1 resultado esperado */
if (lua_pcall(L, 2, 1, 0) != LUA_OK) {
    fprintf(stderr, "Error: %sn", lua_tostring(L, -1));
    lua_pop(L, 1);
} else {
    lua_Integer res = lua_tointeger(L, -1);
    printf("Resultado: %lldn", (long long)res);
    lua_pop(L, 1);   /* limpiar el resultado de la pila */
}

Registrar funciones C en Lua

Para que un script Lua pueda llamar a una función C, hay que registrarla en el estado de la VM. La firma de toda función C llamable desde Lua es int nombre(lua_State *L):

/* Función C que suma dos números y devuelve el resultado */
static int c_suma(lua_State *L) {
    /* Comprobar que hay exactamente 2 argumentos numéricos */
    luaL_checkinteger(L, 1);
    luaL_checkinteger(L, 2);

    lua_Integer a = lua_tointeger(L, 1);
    lua_Integer b = lua_tointeger(L, 2);

    lua_pushinteger(L, a + b);
    return 1;   /* número de valores que devolvemos */
}

/* Función C que devuelve dos valores (módulo y cociente) */
static int c_divmod(lua_State *L) {
    lua_Integer a = luaL_checkinteger(L, 1);
    lua_Integer b = luaL_checkinteger(L, 2);
    if (b == 0) luaL_error(L, "división por cero");

    lua_pushinteger(L, a / b);    /* cociente */
    lua_pushinteger(L, a %% b);    /* resto */
    return 2;   /* devolvemos 2 valores */
}

/* Registrar ambas funciones como globales de Lua */
lua_register(L, "suma",   c_suma);
lua_register(L, "divmod", c_divmod);

/* Ahora desde Lua se puede escribir: */
/* print(suma(10, 3))          -->  13  */
/* local q, r = divmod(10, 3)  -->  3, 1 */

Crear un módulo C para Lua

La forma más limpia de distribuir extensiones C para Lua es como módulo dinámico (.so en Linux, .dll en Windows). El módulo implementa la función luaopen_nombre que inicializa y devuelve la tabla del módulo:

#include <lua.h>
#include <lauxlib.h>

static int mi_funcion(lua_State *L) {
    const char *s = luaL_checkstring(L, 1);
    lua_pushfstring(L, "Procesado: %s", s);
    return 1;
}

/* Tabla de funciones del módulo */
static const luaL_Reg funciones[] = {
    {"procesar", mi_funcion},
    {NULL, NULL}   /* centinela */
};

/* Punto de entrada que llama require("mimodulo") */
int luaopen_mimodulo(lua_State *L) {
    luaL_newlib(L, funciones);
    return 1;
}
# Compilar como biblioteca compartida
gcc -shared -fPIC mimodulo.c -o mimodulo.so -llua5.4

# Desde Lua:
# local m = require("mimodulo")
# print(m.procesar("hola"))   -->  Procesado: hola

Gestionar tipos y errores

La API ofrece funciones de comprobación de tipo que lanzan errores descriptivos automáticamente (luaL_check*) y variantes que simplemente devuelven un valor por defecto si el tipo es incorrecto (lua_to*). En producción conviene usar las variantes luaL_check* para dar mensajes de error útiles al desarrollador Lua:

static int configurar(lua_State *L) {
    /* Obtener argumentos con comprobación estricta */
    const char *host  = luaL_checkstring(L, 1);
    lua_Integer puerto = luaL_checkinteger(L, 2);
    /* Tercer argumento opcional: timeout con valor por defecto 30 */
    lua_Integer timeout = luaL_optinteger(L, 3, 30);

    printf("Conectando a %s:%lld (timeout: %llds)n",
           host, (long long)puerto, (long long)timeout);
    return 0;
}

La C API es la razón por la que Lua aparece embebido en tantos sitios distintos: desde motores de juego como los que usan LÖVE 2D y Defold hasta el proxy inverso de OpenResty. Si has trabajado con la C API de Python, la de Lua te resultará más sencilla: menos magia, más control explícito de la pila.

Imagen: Pexels / Pixabay

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP