Libuv

Documento a partir del estudio de esta web.

Para programación asíncrona y conducida por eventos, proporciona el loop de eventos y el sistema de callbacks basado en notificaciones del sistema operativo, normalmente producidas por operaciones de I/O, aunque pueden ser producidas por otras actividades también. Para que el SO nos avise, la aplicación ha de pedirle que vigile un ente concreto (un socket en el que esperamos recibir información) y que coloque una notificación en la cola tras ocurrir el evento del cual queremos ser avisados.

Event Loop

De forma básica, lo importante es que, cuando registramos un callback asociado a un evento, cuando llega la notificación de que dicho evento se ha producido, se extrae del registro la función asociada y se ejecuta. De forma muy simplificada, este proceso es algo tal que así:

const callbackMap = {
  ev1: { run: callback1 },
  ev2: { run: callback2 },
};

const registerCallback = (ev, cb) => {
  callbackMap[ev] = { run: cb };
}

// event-loop
while (true) {
  const ev = get_next_event();
  if ev in callbackMap {
    callbackMap[ev].run();
  }
}

El event-loop está encapsulado en la función uv_run, que es el punto final cuando se usa esta librería. En su documentación leemos lo siguiente:

> uv_loop_t — Event loop > The event loop is the central part of libuv’s functionality. It takes care of polling for i/o and scheduling callbacks to be run based on different sources of events.

> Public members > void *uv_loop_t.data > Space for user-defined arbitrary data. libuv does not use and does not touch this field.

> int uv_run(uv_loop_t *loop, uv_run_mode mode) > This function runs the event loop.

El campo data en el struct uv_loop_t es donde se guardan el closure que se genera al ejecutarse la función.

typedef struct uv_loop_s uv_loop_t;
  struct uv_loop_s {
  void* data;                   /* User data - use this for whatever. */
  unsigned int active_handles;  /* Loop reference counting. */
  struct uv__queue handle_queue;
  union {
    void* unused;
    unsigned int count;
  } active_reqs;                /* Internal storage for future extensions. */
  void* internal_fields;        /* Internal flag to signal loop stop. */
  unsigned int stop_flag;
  UV_LOOP_PRIVATE_FIELDS
};

En la siguiente imagen podemos ver un diagrama de las etapas que tienen lugar en cada iteración del bucle:

Mecanismo de notificación

El mecanismo depende del SO. En linux, se usa epoll, en MacOS y BSD, kqueue, y en Windows IOCP. Este mecanismo se usa para las llamadas de red. Para I/O del sistema de archivos realmente se usa un thread pool. No hay primitivas específicas para los distintos SO de las que pueda depender libuv1. Así que lo que se hace es usar dicho pool y ejecutar en él las acciones sobre el fs, y cuando acaben se notifican.

Ejemplo básico

El siguiente ejemplo es muy bueno para ver la estructura de cómo ha de ser un programa que use libuv, como por ejemplo nodejs.

#include <stdio.h>
#include <stdlib.h>
#include <uv.h>

int main() {
    uv_loop_t *loop = malloc(sizeof(uv_loop_t));
    uv_loop_init(loop);

    printf("Now quitting.\n");
    uv_run(loop, UV_RUN_DEFAULT); // Node usa el loop por defecto.

    uv_loop_close(loop);
    free(loop);
    return 0;
}

Node usa como en el ejemplo el loop por defecto. Si queremos tener una referencia a él en cualquier momento, podemos usar la función uv_default_loop().

Registro de Eventos

La forma de trabajar con libuv es diciéndole al programa que tenemos interés en ciertos eventos mediante observadores (watchers). Estos están definidos en el código, se pueden ver aquí. Estos observadores o manejadores se setean en el bucle mediante:

uv_TYPE_init(uv_loop_t *, uv_TYPE_t *)

donde el primer parámetro es el bucle en el que queremos registrar nuestro manejador, y el segundo es el evento que queremos que dispare nuestro callback. Un ejemplo concreto (y comentado) se ve a continuación, en el que se usa un handle de un evento idle, los cuales se llaman en cada iteración del bucle.

#include <stdio.h>
#include <uv.h>

int64_t counter = 0;

void wait_for_a_while(uv_idle_t* handle) {
  if (++counter >= 10e6)
    // Al llamar a esta función se cierra el manejador del evento y el loop
    // deja de tener ningún observador registrado por lo que finaliza.
    uv_idle_stop(handle);
}

int main() {
  // Inicializamos un tipo de evento idle
  // typedef struct uv_idle_s uv_idle_t;
  uv_idle_t idler;

  // y lo conectamos al loop por defecto.
  uv_idle_init(uv_default_loop(), &idler);

  // Registramos el callback al que llamar cuando se reciba la notificación de ese evento.
  uv_idle_start(&idler, wait_for_a_while);

  printf("Idling...\n");

  // Ejecutamos el bucle, que no parará hasta que no haya más eventos registrados.
  // Como los eventos de tipo idle se llaman en cualquier iteración, hasta que no
  // se cierra con `uv_idle_stop`, el loop tendrá siempre registrado un evento y
  // no saldrá de este punto hasta que dicha función de cierre se invoque, lo cual
  // ocurre en el `callback` definido arriba.
  uv_run(uv_default_loop(), UV_RUN_DEFAULT);

  // Cerramos el bucle.
  uv_loop_close(uv_default_loop());

  return 0;
}

Manejo de errores

En función de si la llamada es síncrona o asíncrona:

  • síncrona: devuelve un código numérico negativo

  • asíncrona: el callback recibe un código de status

    A partir de estos valores podemos obtener el error llamando a uv_strerr(int) o a uv_err_name(int). Ambas nos devuelven un char* con la descripción o el nombre del error respectivamente.

Sistema de Archivos


  1. Realmente, como puede leerse en este enlace, si hay ciertas primitivas que igual podrían usarse, pero no son muy consistentes, malas APIs (según el artículo), etc. Dicho artículo es de 2012, pero sigue enlazado en la página de libuv, por lo que entiendo que aún sigue siendo válido. ↩︎