🐧 Ciel, Yiwei Gong

Asynchronous and Event-driven Programming in Lua

Mon Jul 30, 2018

Asynchronous and event-driven are the key concepts in nodejs. It wraps I/O operation into an asynchronous event and uses callback function as the event handler. libuv is the low-level C API that provides the event driven mechanism to nodejs environment. libuv provides a set of generic API for different operating systems. It either uses OS signal or thread pool to monitor an event in the background, then callbacks to the corresponding handlers when the event happens.

Lua has a very well designed language syntax. Functions in Lua are first-class type as well. The developer is allowed to pass an anonymous function as the parameter to another function. This article demonstrates how to bind libuv API to Lua environment. Eventually, Lua is able to implement a nodejs liked HTTP server.

To do so, instead of using the official Lua host, we use a new host C program that runs both Lua and libuv’s event loop.

lua_State *L = luaL_newstate(); /* create state */
if (L == NULL)
{
    fprintf(stderr, "cannot create state: not enough memory\n");
    return 1;
}
luaL_openlibs(L);
luaL_requiref(L, "async", luaopen_async, 1);
int result = luaL_dofile(L, argv[1]);
uv_run(uv_default_loop(), UV_RUN_DEFAULT);

So first of all, we need to create a Lua state for running Lua code. Then, we bind the Lua standard library to the Lua environment. The async module is the C module we use to bind libuv API to Lua environment. Finally, we start the Lua code and libuv’s event loop. The host function will keep looping until there is no more event in the loop.

In this async module, we register two C functions, settimeout and cleartimeout.

typedef struct
{
    int cb_ref;
    lua_State *L;
    int is_interval;
} time_handler_data;

void free_handler(uv_timer_t *handler)
{
    free(handler->data);
    free(handler);
}

void uv_timer_cb_handler(uv_timer_t *handler)
{
    time_handler_data *handler_data = (time_handler_data *)(handler->data);
    lua_rawgeti(handler_data->L, LUA_REGISTRYINDEX, handler_data->cb_ref);
    lua_pcall(handler_data->L, 0, 0, 0);
    if (!handler_data->is_interval)
    {
        free_handler(handler);
    }
}

static int settimeout(lua_State *L)
{
    size_t timeout = luaL_checkinteger(L, 1);
    time_handler_data *handler_data = malloc(sizeof(time_handler_data));
    handler_data->cb_ref = luaL_ref(L, LUA_REGISTRYINDEX);
    handler_data->L = L;
    handler_data->is_interval = 0;
    uv_timer_t *handler = malloc(sizeof(uv_timer_t));
    handler->data = handler_data;
    uv_timer_init(uv_default_loop(), handler);
    uv_timer_start(handler, uv_timer_cb_handler, timeout, 0);
    lua_pushlightuserdata(L, handler);
    return 1;
}

static int cleartimeout(lua_State *L)
{
    uv_timer_t *handler = (uv_timer_t *)lua_touserdata(L, 1);
    uv_timer_stop(handler);
    free(handler->data);
    free(handler);
    return 0;
}

In the settimeout function, we first create a record in the memory to keep track of the Lua callback function and the Lua stack. Then we attach this record to libuv timer handler uv_timer_t and trigger the timer start event. So when the timer is triggered, we push the Lua closure to Lua state again and invoke the callback function. Last but not least, free the record memory.

We can achieve the setinterval and clearinterval in a similar way. So in our Lua script, we can write

async.settimeout(2000, function() print("timeout 2000 --> this shall print last") end)
async.settimeout(1000, function() print("timeout 1000 --> this shall print second") end)
local handler = async.settimeout(3000, function() print("timeout 3000 --> this shall never call") end)
print("timeout 0    --> this shall print first")
async.cleartimeout(handler)

handler = async.setinterval(1000, function() print("interval 1000 --> this shall print every second") end)
async.settimeout(10000, function() 
    print("after 10s, we clear interval")
    async.clearinterval(handler)
end)

and the output will be

gcc -luv -llua -Wall -fPIC -o2 -o lua_async lua_async.c async.c
./lua_async example.lua
timeout 0    --> this shall print first
interval 1000 --> this shall print every second
timeout 1000 --> this shall print second
interval 1000 --> this shall print every second
timeout 2000 --> this shall print last
interval 1000 --> this shall print every second
interval 1000 --> this shall print every second
interval 1000 --> this shall print every second
interval 1000 --> this shall print every second
interval 1000 --> this shall print every second
interval 1000 --> this shall print every second
interval 1000 --> this shall print every second
interval 1000 --> this shall print every second
after 10s, we clear interval

Source code.