An Introduction to libuv

Basics of libuv

«  Introduction   ::   Contents   ::   Filesystem  »

Basics of libuv

libuv enforces an asynchronous, event-driven style of programming. Its core job is to provide an event loop and callback based notifications of I/O and other activities. libuv offers core utilities like timers, non-blocking networking support, asynchronous file system access, child processes and more.

Event loops

In event-driven programming, an application expresses interest in certain events and respond to them when they occur. The responsibility of gathering events from the operating system or monitoring other sources of events is handled by libuv, and the user can register callbacks to be invoked when an event occurs. The event-loop usually keeps running forever. In pseudocode:

while there are still events to process:
    e = get the next event
    if there is a callback associated with e:
        call the callback

Some examples of events are:

  • File is ready for writing
  • A socket has data ready to be read
  • A timer has timed out

This event loop is encapsulated by uv_run() – the end-all function when using libuv.

The most common activity of systems programs is to deal with input and output, rather than a lot of number-crunching. The problem with using conventional input/output functions (read, fprintf, etc.) is that they are blocking. The actual write to a hard disk or reading from a network, takes a disproportionately long time compared to the speed of the processor. The functions don’t return until the task is done, so that your program is doing nothing. For programs which require high performance this is a major roadblock as other activities and other I/O operations are kept waiting.

One of the standard solutions is to use threads. Each blocking I/O operation is started in a separate thread (or in a thread pool). When the blocking function gets invoked in the thread, the processor can schedule another thread to run, which actually needs the CPU.

The approach followed by libuv uses another style, which is the asynchronous, non-blocking style. Most modern operating systems provide event notification subsystems. For example, a normal read call on a socket would block until the sender actually sent something. Instead, the application can request the operating system to watch the socket and put an event notification in the queue. The application can inspect the events at its convenience (perhaps doing some number crunching before to use the processor to the maximum) and grab the data. It is asynchronous because the application expressed interest at one point, then used the data at another point (in time and space). It is non-blocking because the application process was free to do other tasks. This fits in well with libuv’s event-loop approach, since the operating system events can be treated as just another libuv event. The non-blocking ensures that other events can continue to be handled as fast they come in [1].

Note

How the I/O is run in the background is not of our concern, but due to the way our computer hardware works, with the thread as the basic unit of the processor, libuv and OSes will usually run background/worker threads and/or polling to perform tasks in a non-blocking manner.

Bert Belder, one of the libuv core developers has a small video explaining the architecture of libuv and its background. If you have no prior experience with either libuv or libev, it is a quick, useful watch.

Hello World

With the basics out of the way, lets write our first libuv program. It does nothing, except start a loop which will exit immediately.

helloworld/main.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <stdio.h>
#include <uv.h>

int main() {
    uv_loop_t *loop = uv_loop_new();

    printf("Now quitting.\n");
    uv_run(loop, UV_RUN_DEFAULT);

    return 0;
}

This program quits immediately because it has no events to process. A libuv event loop has to be told to watch out for events using the various API functions.

Default loop

A default loop is provided by libuv and can be accessed using uv_default_loop(). You should use this loop if you only want a single loop.

Note

node.js uses the default loop as its main loop. If you are writing bindings you should be aware of this.

Error handling

libuv functions which may fail return -1 on error. The error code itself is set on the event loop as last_err. Use uv_last_error(loop) to get a uv_err_t which has a code member with the error code. code is an enumeration of UV_* as defined here:

libuv error codes

  XX( -1, UNKNOWN, "unknown error")                                           \
  XX(  0, OK, "success")                                                      \
  XX(  1, EOF, "end of file")                                                 \
  XX(  2, EADDRINFO, "getaddrinfo error")                                     \
  XX(  3, EACCES, "permission denied")                                        \
  XX(  4, EAGAIN, "resource temporarily unavailable")                         \
  XX(  5, EADDRINUSE, "address already in use")                               \
  XX(  6, EADDRNOTAVAIL, "address not available")                             \
  XX(  7, EAFNOSUPPORT, "address family not supported")                       \
  XX(  8, EALREADY, "connection already in progress")                         \
  XX(  9, EBADF, "bad file descriptor")                                       \
  XX( 10, EBUSY, "resource busy or locked")                                   \
  XX( 11, ECONNABORTED, "software caused connection abort")                   \
  XX( 12, ECONNREFUSED, "connection refused")                                 \
  XX( 13, ECONNRESET, "connection reset by peer")                             \
  XX( 14, EDESTADDRREQ, "destination address required")                       \
  XX( 15, EFAULT, "bad address in system call argument")                      \
  XX( 16, EHOSTUNREACH, "host is unreachable")                                \
  XX( 17, EINTR, "interrupted system call")                                   \
  XX( 18, EINVAL, "invalid argument")                                         \
  XX( 19, EISCONN, "socket is already connected")                             \
  XX( 20, EMFILE, "too many open files")                                      \
  XX( 21, EMSGSIZE, "message too long")                                       \
  XX( 22, ENETDOWN, "network is down")                                        \
  XX( 23, ENETUNREACH, "network is unreachable")                              \
  XX( 24, ENFILE, "file table overflow")                                      \
  XX( 25, ENOBUFS, "no buffer space available")                               \
  XX( 26, ENOMEM, "not enough memory")                                        \
  XX( 27, ENOTDIR, "not a directory")                                         \
  XX( 28, EISDIR, "illegal operation on a directory")                         \
  XX( 29, ENONET, "machine is not on the network")                            \
  XX( 31, ENOTCONN, "socket is not connected")                                \
  XX( 32, ENOTSOCK, "socket operation on non-socket")                         \
  XX( 33, ENOTSUP, "operation not supported on socket")                       \
  XX( 34, ENOENT, "no such file or directory")                                \
  XX( 35, ENOSYS, "function not implemented")                                 \
  XX( 36, EPIPE, "broken pipe")                                               \
  XX( 37, EPROTO, "protocol error")                                           \
  XX( 38, EPROTONOSUPPORT, "protocol not supported")                          \
  XX( 39, EPROTOTYPE, "protocol wrong type for socket")                       \
  XX( 40, ETIMEDOUT, "connection timed out")                                  \
  XX( 41, ECHARSET, "invalid Unicode character")                              \
  XX( 42, EAIFAMNOSUPPORT, "address family for hostname not supported")       \
  XX( 44, EAISERVICE, "servname not supported for ai_socktype")               \
  XX( 45, EAISOCKTYPE, "ai_socktype not supported")                           \
  XX( 46, ESHUTDOWN, "cannot send after transport endpoint shutdown")         \
  XX( 47, EEXIST, "file already exists")                                      \
  XX( 48, ESRCH, "no such process")                                           \
  XX( 49, ENAMETOOLONG, "name too long")                                      \
  XX( 50, EPERM, "operation not permitted")                                   \
  XX( 51, ELOOP, "too many symbolic links encountered")                       \
  XX( 52, EXDEV, "cross-device link not permitted")                           \
  XX( 53, ENOTEMPTY, "directory not empty")                                   \
  XX( 54, ENOSPC, "no space left on device")                                  \
  XX( 55, EIO, "i/o error")                                                   \
  XX( 56, EROFS, "read-only file system")                                     \
  XX( 57, ENODEV, "no such device")                                           \
  XX( 58, ESPIPE, "invalid seek")                                             \
  XX( 59, ECANCELED, "operation canceled")                                    \

You can use the uv_strerror(uv_err_t) and uv_err_name(uv_err_t) functions to get a const char * describing the error or the error name respectively.

Async callbacks have a status argument as the last argument. Use this instead of the return value.

Watchers

Watchers are how users of libuv express interest in particular events. Watchers are opaque structs named as uv_TYPE_t where type signifies what the watcher is used for. A full list of watchers supported by libuv is:

libuv watchers

typedef struct uv_udp_s uv_udp_t;
typedef struct uv_pipe_s uv_pipe_t;
typedef struct uv_tty_s uv_tty_t;
typedef struct uv_poll_s uv_poll_t;
typedef struct uv_timer_s uv_timer_t;
typedef struct uv_prepare_s uv_prepare_t;
typedef struct uv_check_s uv_check_t;
typedef struct uv_idle_s uv_idle_t;
typedef struct uv_async_s uv_async_t;
typedef struct uv_process_s uv_process_t;
typedef struct uv_fs_event_s uv_fs_event_t;
typedef struct uv_fs_poll_s uv_fs_poll_t;
typedef struct uv_signal_s uv_signal_t;

/* Request types. */
typedef struct uv_req_s uv_req_t;
typedef struct uv_getaddrinfo_s uv_getaddrinfo_t;
typedef struct uv_shutdown_s uv_shutdown_t;

Note

All watcher structs are subclasses of uv_handle_t and often referred to as handles in libuv and in this text.

Watchers are setup by a corresponding:

uv_TYPE_init(uv_TYPE_t*)

function.

Note

Some watcher initialization functions require the loop as a first argument.

A watcher is set to actually listen for events by invoking:

uv_TYPE_start(uv_TYPE_t*, callback)

and stopped by calling the corresponding:

uv_TYPE_stop(uv_TYPE_t*)

Callbacks are functions which are called by libuv whenever an event the watcher is interested in has taken place. Application specific logic will usually be implemented in the callback. For example, an IO watcher’s callback will receive the data read from a file, a timer callback will be triggered on timeout and so on.

Idling

Here is an example of using a watcher. An idle watcher’s callback is repeatedly called. There are some deeper semantics, discussed in Utilities, but we’ll ignore them for now. Let’s just use an idle watcher to look at the watcher life cycle and see how uv_run() will now block because a watcher is present. The idle watcher is stopped when the count is reached and uv_run() exits since no event watchers are active.

idle-basic/main.c

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

int64_t counter = 0;

void wait_for_a_while(uv_idle_t* handle, int status) {
    counter++;

    if (counter >= 10e6)
        uv_idle_stop(handle);
}

int main() {
    uv_idle_t idler;

    uv_idle_init(uv_default_loop(), &idler);
    uv_idle_start(&idler, wait_for_a_while);

    printf("Idling...\n");
    uv_run(uv_default_loop(), UV_RUN_DEFAULT);

    return 0;
}

void *data pattern

note about not necessarily creating type structs on the stack


[1]Depending on the capacity of the hardware of course.

«  Introduction   ::   Contents   ::   Filesystem  »