Basics#
libuv offers 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 responds 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 live objects that could generate events do
let e = get the next event in
if there is a callback waiting on e then
call the callback
done
Some examples of events are:
A file is ready for writing
A socket has data ready to be read
A timer has timed out
This event loop is inside Luv.Loop.run, which
binds to uv_run
, the main function of 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 blocked
doing nothing until then. For programs which require high performance this is a
major obstacle, 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 into a queue.
The application can inspect the events at its convenience and grab the data,
perhaps doing some number crunching in the meantime to use the processor to the
maximum. It is asynchronous because the application expressed interest at
one point, then used the data at another point in its execution sequence. It is
non-blocking because the application process was free to do other tasks. in
between. This fits in well with libuv’s event-loop approach, since the operating
system’s events can be treated as libuv events. The non-blocking ensures that
other events can continue to be handled as fast as they come in (and the
hardware allows).
Hello, world!#
With the basics out of the way, let’s write our first libuv program. It does nothing, except start a loop which will exit immediately.
1let () =
2 print_endline "Hello, world!";
3 ignore (Luv.Loop.run () : bool)
This program exits immediately because it has no events to process. A libuv event loop has to be told to watch out for events using the various libuv API functions.
Here’s a slightly more interesting program, which waits for one second, prints “Hello, world!” and then exits:
1let () =
2 let timer = Luv.Timer.init () |> Result.get_ok in
3
4 ignore (Luv.Timer.start timer 1000 (fun () ->
5 print_endline "Hello, world!"));
6
7 print_endline "Waiting...";
8 ignore (Luv.Loop.run () : bool)
Console output#
The first two examples used OCaml’s own print_endline
for console output. In
a fully asynchronous program, you may want to use libuv to write to the console
(or STDOUT) instead. Luv offers at least three ways of doing this.
Using the pre-opened file Luv.File.stdout:
1let () =
2 Luv.File.(write stdout [Luv.Buffer.from_string "Hello, world!\n"]) ignore;
3 ignore (Luv.Loop.run () : bool)
Wrapping Luv.File.stdout in a pipe with Luv.Pipe.open_:
1let () =
2 let pipe = Luv.Pipe.init () |> Result.get_ok in
3 Luv.Pipe.open_ pipe Luv.File.stdout |> Result.get_ok;
4
5 Luv.Stream.write pipe [Luv.Buffer.from_string "Hello, world!\n"]
6 (fun _ _ -> ());
7
8 ignore (Luv.Loop.run () : bool)
Wrapping Luv.File.stdout in a TTY handle with Luv.TTY.init:
1let () =
2 let tty = Luv.TTY.init Luv.File.stdout |> Result.get_ok in
3
4 Luv.Stream.write tty [Luv.Buffer.from_string "Hello, world!\n"]
5 (fun _ _ -> ());
6
7 ignore (Luv.Loop.run () : bool)
As you can see, all of these are too low-level and verbose for ordinary use.
They would need to be wrapped in a higher-level library. The examples will
continue to use print_endline
, which is fine for most purposes.
Error handling#
When functions fail, they produce one of the error codes in Luv.Error.t, wrapped in the Error
side of a result
. In
the last example above, we used Luv.Timer.init, which has signature
Luv.Timer.init : unit -> (Luv.Timer.t, Luv.Error.t) result
If a call to Luv.Timer.init
succeeds, it will produce Ok timer
. If it
fails, it might produce Error `ENOMEM
.
Asynchronous operations that can fail pass the result
to their callback,
instead of returning it:
Luv.File.unlink : string -> ((unit, Luv.Error.t) result -> unit) -> unit
You can get the error name and description as strings by calling Luv.Error.err_name and Luv.Error.strerror, respectively.
libuv has several different conventions for returning errors. Luv translates all
of them into the above scheme: synchronous operations return a result
, and
asynchronous operations pass a result
to their callback.
There is only a handful of exceptions to this, but they are all easy to understand. For example, Luv.Timer.start can fail immediately, but if it starts, it can only call its callback with success. So, it has signature
Luv.Timer.start :
Luv.Timer.t -> int -> (unit -> unit) -> (unit, Luv.Error.t) result
Luv does not raise exceptions. In addition, callbacks you pass to Luv APIs
shouldn’t raise exceptions at the top level (they can use exceptions interally).
This is because such exceptions can’t be allowed to go up the stack into libuv.
If a callback passed to Luv raises an exception, Luv catches it, prints a stack
trace to STDERR
, and then calls exit 2
. You can change this behavior by
installing your own handler:
Luv.Error.set_on_unhandled_exception (fun exn -> (* ... *))
Generally, only applications, rather than libraries, should call this function.
Handles#
libuv works by the user expressing interest in particular events. This is usually done by creating a handle to an I/O device, timer or process, and then calling a function on that handle.
The following handle types are available:
Luv.Timer — timers
Luv.Signal — signals
Luv.Process — subprocesses
Luv.TCP — TCP sockets
Luv.UDP — UDP sockets
Luv.Pipe — pipes
Luv.TTY — consoles
Luv.FS_event — filesystem events
Luv.FS_poll — filesystem polling
Luv.Poll — file descriptor polling
Luv.Async — inter-loop communication
Luv.Prepare — pre-I/O callbacks
Luv.Check — post-I/O callbacks
Luv.Idle — per-iteration callbacks
In addition to the above true handles, there is also Luv.File, which has a similar basic interface, yet is not a proper libuv handle kind.
Example#
The remaining chapters of this guide will exercise many of the modules mentioned above, but let’s work with a simple one now — that is, in addition to the timer “Hello, world!” example we’ve already seen!
Here is an example of using a Luv.Idle.t handle. It adds a callback, that is to be called once on every iteration of the event loop:
1let () =
2 let idle = Luv.Idle.init () |> Result.get_ok in
3
4 let counter = ref 0 in
5
6 ignore @@ Luv.Idle.start idle begin fun () ->
7 counter := !counter + 1;
8 print_endline "Loop iteration";
9
10 if !counter >= 10 then
11 ignore (Luv.Idle.stop idle)
12 end;
13
14 ignore (Luv.Loop.run () : bool)
The error handling in this example is not robust! It is using Result.get_ok
and ignore
to avoid dealing with potential Error
. If you are writing a
robust application, or a library, please use a better error-handling mechanism
instead.
Requests#
libuv also has requests. Whereas handles are long-lived, a request is a short-lived representation of one particular instance of an asynchronous operation. Requests are used by libuv, for example, to return complex results. Luv manages requests automatically, so the user generally does not have to deal with them at all.
A few request types are still exposed, however, because they are cancelable, and can be passed, at the user’s discretion, to Luv.Request.cancel. Explicit use of these requests is still optional, however — they are always taken through optional arguments. So, when using Luv, it is possible to ignore the existence of requests entirely.
See Luv.File.Request for a typical request type, with example usage. This represents filesystem requests, which are cancelable. Functions in Luv.File have signatures like
Luv.File.unlink :
?request:Luv.File.Request.t ->
string ->
((unit, Luv.Error.t) result -> unit) ->
unit
…in case the user needs the ability to cancel the request. This guide will
generally omit the ?request
argument, because it is rarely used, and always
has only this one purpose.
Here are all the exposed request types in Luv:
Everything else#
libuv, and Luv, also offer a large number of other operations, all implemented in a cross-platform fashion. See the full module listing in the API index. For example, one can list all environment variables using
Luv.Env.environ : unit -> ((string * string) list, Luv.Error.t) result
Buffers#
Luv functions use data buffers of type Luv.Buffer.t. These are ordinary OCaml bigstrings (that is, chunks of data managed from OCaml, but stored in the C heap). They are compatible with at least the following bigstring libraries:
System calls are usually able to work with part of a buffer, by taking a pointer to the buffer, an offset into the buffer, and a length of space, starting at the offset, to work with. Luv functions instead take only a buffer. To work with part of an existing buffer, use Luv.Buffer.sub to create a view into the buffer, and then pass the view to Luv.
Many libuv (and Luv) functions work with lists of buffers. For example, Luv.File.write takes such a list. This simply means that libuv will pull data from the buffers consecutively for writing. Likewise, Luv.File.read takes a list of buffers, which it will consecutively fill. This is known as scatter-gather I/O. It could be said that libuv (and Luv) expose an API more similar to readv and writev than read and write.
Luv has a couple helpers for manipulating lists of buffers:
Luv.Buffer.total_size returns the total number of bytes across all the buffers in a buffer list.
Luv.Buffer.drop returns a new buffer list, with the first N bytes removed from its front. This is useful to advance the buffer references after a partial read into, or write from, a buffer list, before beginning the next read or write.
Integer types#
Luv usually represents C integer types with ordinary OCaml integers, such as
int
, sometimes int64
. However, in some APIs, to avoid losing precision,
it seems important to preserve the full width and signedness of the original C
type. In such cases, you will encounter types from ocaml-integers, such as
Unsigned.Size_t.t
. You can use module Unsigned.Size_t
to perform
operations at that type, or you can choose to convert to an OCaml integer with
Unsigned.Size_t.to_int
.